eslint

Why We Need Eslint?

在团队配合中,有的人是Mac开发,有的人是Windows来发,使用Eslint来强制开发人员统一代码
格式,比如常见的tab以及空格,以及windows和Mac的回车标识,也可强制开发人员删除无用的
垃圾代码,比如alert(1212), console.log(111)等等,或者条件语句中使用if(true){},
总之它就是用来检测开发人员代码书写规范的一个集成工具。so beautiful!

了解Vue

介绍

vue是一个用来构建交互式web界面的库。它的核心集中于视图层,它很容易与其他库或现有的项目集成。
另一方面,Vue完全有能力服务员复杂的单页面应用。但是Vue和其它的框架有什么不同呢?

和React的比较

React和Vue有很多相似之处:

  • 利用虚拟DOM
  • 提供了reactive(反应性)和可组合的视图组件
    在代码核心库中,保持对相关库管理的routing,全局state的关注。作用域也很相似。我们想保证的不仅
    有技术上的准确也要确保技术的平衡,我们找出React哪里优于Vue,比如,React庞大的生态圈以及
    自定义渲染器的丰富性。
性能(渲染性能)

Vue胜过React,当渲染UI时,DOM的操作通常是非常昂贵的,没有库可以使那些原生的操作更快,我们能做的
最好的就是:

  1. 尽量减少必要的DOM变化的数量,React和Vue都使用了虚拟DOM来解决这个问题。
  2. 尽可能在顶层的DOM操作上添加较小的开销(纯js计算),这里是React和Vue的区别。
    javascript开销和计算必要的DOM操作机制有直接关系,Vue和React都利用虚拟DOM来达到它,但是Vue的实现更
    轻量级,因此比React的开销更少。
    React和Vue都提供了功能组件,他们是stateless,instanceless的因此需要的开销很少。当它们用于性能关键
    的场景中,Vue更快,为了证明这个,我们渲染1万个item100次,强烈建议自己实践,因为它和软件、浏览器相关。
    相关比较如下:
1
2
3
4
5
6
                  Vue       React
Fatest 23ms 63ms
Mediam 42ms 81ms
Average 51ms 94ms
95thPerc. 73ms 164ms
Slowest 343ms 453ms
更新性能

在React中,当一个组件的state改变了,它会触发整个组件的sub-tree的重新渲染,为了避免child组件不必要
的渲染,你需要在该组件中使用shouldComponentUpdate并定义不变的结构,而在Vue中,在渲染过程中,会自
动的追踪组件的依赖项,所以系统会精确的知道哪个组件需要正真的重新渲染。这就意味着未优化的Vue比未优化的
React要快。由于Vue的加强了渲染性能,即使React进行了全部的优化操作,通常也比Vue慢。

在开发过程中

虽然在生产环境中的性能指标是非常重要的,因为这和用户体验相关联。但是在开发时的性能仍然重要,因为这与
我们开发者相关联.
Vue和React在大多数程序中开发可以保持足够快速。但是,在高帧速率的可视化或动画时,Vue每秒处理10帧,而
React每秒只有1帧。这是因为在开发环境中React需要检查大量的不变量,这是用来提示一些重要的错误或警告用的。
当然在Vue中也是同意这是很重要的,但是当执行这些检查时,我们对性能保持密切关注。

HTML & CSS

在React中,所有的东西是javascript,虽然听起来很简单且高雅但是当你向下挖掘时,在javascript中写HTML和CSS,
当解决问题时,会很痛苦。而在Vue中,我们信奉web的技术并将它们,将css写在html顶部。

####### JSX vs Templates
在React中,所有的组件在JSX中来表示他们的UI,下面是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render () {
let { items } = this.props
let children
if (items.length > 0) {
children = (
<ul>
{items.map(item =>
<li key={item.id}>{item.name}</li>
)}
</ul>
)
} else {
children = <p>No items found.</p>
}
return (
<div className='list-container'>
{children}
</div>
)
}

JSX有以下优点:

  • 使用编程语言(js)构建你的视图。
  • 在某些方式下,对JSX的工具支持比vue的模板更先进。
    在Vue中,我们有render functions甚至还有支持jsx

HTML & CSS

在Vue中,有渲染器函数并且支持jsx,默认情况下,我们提供模板作为选择方案:

1
2
3
4
5
6
7
8
9
10
<template>
<div class="list-container">
<ul v-if="items.length">
<li v-for="item in items">
{{ item.name }}
</li>
</ul>
<p v-else>No items found.</p>
</div>
</template>

它的优势:

  • 模板中有少量的实现并且代码风格优雅
  • 模板一直是称述性质的
  • 任何有效的HTML在模板中也是有效的
  • 读起来很像英语
  • 不需要再高版本的javascript中增加可读性。
    另外一个好处就是你可以使用预处理器处理HTML-compliant模板,比如Pug书写你的Vue模板:
1
2
3
4
div.list-container
ul(v-if="items.length")
li(v-for="item in items") {{ item.name }}
p(v-else) No items found.

Component-Scoped CSS(组件范围的CSS)
除非你的组件分布在多个文件中(比如CSS Modules),范围性的CSS在React中通常是在js中。而在
Vue中,你完全可以在单个文件中访问到CSS:

1
2
3
4
5
6
7
<style scoped>
@media (min-width: 250px) {
.list-container:hover {
background: orange;
}
}
</style>

scoped属性自动的对组件范围内的元素通过添加一个唯一的属性来编译。

规模

React社区在状态管理上引入了flux/redux,状态管理方式可以在Vue很容易的集成进来。但是尽管
如此,React的生态圈还是要比Vue丰富。Vue提供了一个十分简单生成Vue项目的命令行工具

Scaling Down

React的学习曲线比较陡峭,在你学习之前你需要了解JSX以及ES2015.Vue中可以简单的通过在html引入vue库
来使用vue,React也行啊!!!

1
<script src="https://unpkg.com/vue/dist/vue.js"></script>

Native Rendering

React Native可以已React组件的形式写原生的IOS、Android程序,在这方面,Vue已经与Weex合作了。
Weex是由阿里巴巴开发的一个跨平台UI框架,这意味着使用Weex,你可以以Vue的组件语法规则开发出的程序可以在浏览器,IOS,Android运行。
额,这点我承认确实比react-native强悍啊。。。。,当然Weex正在活跃的开发,和react-native一样不成熟。但是Vue和Weex合作开发啊,选择
React社区还是选择Vue社区呢?如果Weex和Vue成熟稳定的话选择Vue生态圈我觉得还是不错的。

phbricator的安装

Phabricator

Phabricator是一个开发软件工具的集成,包含任务管理,代码review,管理git,svn等等,持续集成的构建,
内部通道的讨论等等。

在我们局域网内安装phabricator

Phabricator是一个LAMP应用程序,所以:

  • 一台linux或Mac OS电脑。(ps: windows不行奥,因为一些命令行在windows无法执行。。。)
  • 一个域名(可有可无,在内网上访问就行了,除非你需要家里办公也访问)(格式:phabricator.xxxxx.com).
  • 一些基本的系统管理员的技能(linux命令得熟悉啊!)
  • 一个web容器,Apache或Nginx(常用的两种方式)
  • PHP(version: >= 5.2, 版本7不支持), MySql(version: >= 5.5), git.
    系统管理员应该会的一些技能(也就是linux的一些指令),比如:在操作系统安装软件,文件系统的操作,进程
    的管理,权限的处理,修改配置文件,设置环境变量等等。

安装需要的组件

  • git(在包管理系统中一般称为’git’)
  • Apache(一般称为”httpd”或”apache2”)或者nginx
  • MySQL Server(一般是”mysqlId”或”mysql-server”)
  • PHP(一般是”php”)
  • 需要的PHP扩展,比如”php-mysql”, “php5-mysql”
    如果已经安装好了这些,那么获取Phrbricator以及它的依赖:
1
2
3
4
//选择你要安装的文件夹,并在该文件夹下执行以下命令
git clone https://github.com/phacility/libphutil.git
git clone https://github.com/phacility/arcanist.git
git clone https://github.com/phacility/phabricator.git

APC是建议安装的,所以没有深究。

Apache容器的配置

  • 配置web容器(Apache, nginx)
  • 在浏览器访问phabricator
    Apache的httpd.conf
1
2
3
4
5
6
7
8
9
10
11
12
<VirtualHost *>
# Change this to the domain which points to your host.
ServerName phabricator.example.com

# Change this to the path where you put 'phabricator' when you checked it
# out from GitHub when following the Installation Guide.
#
# Make sure you include "/webroot" at the end!
DocumentRoot /path/to/phabricator/webroot
RewriteEngine on
RewriteRule ^(.*)$ /index.php?__path__=$1 [B,L,QSA]
</VirtualHost>

如果Apache的配置文件目录不是Phabricator的目录,那么你需要添加<Directory />块,这个块
标示依赖于你的Apache的版本,通过运行httpd -v得到当前运行Apache的版本,如果版本号小于2.4,

1
2
3
4
<Directory "/path/to/phabricator/webroot">
Order allow,deny
Allow from all
</Directory>

如果大于2.4:

1
2
3
<Directory "/path/to/phabricator/webroot">
Require all granted
</Directory>

配置完成之后,请重启Apache,并继续接下来的配置.

Nginx的配置

nginx.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server {
server_name phabricator.example.com;
#你的phabricator安装目录
root /path/to/phabricator/webroot;

location / {
index index.php;
rewrite ^/(.*)$ /index.php?__path__=/$1 last;
}

location /index.php {
fastcgi_pass localhost:9000;
fastcgi_index index.php;

#required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

#variables to make the $_SERVER populate in PHP
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;

fastcgi_param SCRIPT_NAME $fastcgi_script_name;

fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;

fastcgi_param REMOTE_ADDR $remote_addr;
}
}

配置完成之后重启nginx,并继续接下来的配置

配置Mysql

当MySQL运行时,需要加载Phabricator的schemata,所以要在phabricator目录下执行:./bin/storage upgrade
如果你配置的用户不是连接数据库的特权用户,所以需要覆盖默认的用户,所以更改schema,通过一下命令更改
./bin/storage upgrade --user <user> --password <password>并且./bin/storage upgrade --force
注意:当修改了phabricator后,运行storage upgrade来应用新的修改。

配置账号及注册

phabricator提供了很多的登录体系,你可以配置谁可以访问或者注册你安装的phabricator以及用户用存在的账号登录phabricator。
登录方式称之为授权给予,比如可用的”用户名/密码”授权,允许你通过传统的用户名密码登录,其余的支持用证书登录。比如:

  • 用户名密码:使用用户名密码登录注册
  • LDAP:使用LDAP证书登录注册
  • OAuth:用户使用支持OAuth2的协议登录,比如(Facebook,Google,GitHub)
  • 其余的提供程序:有许多可用的支持,Phabricator可以扩展,相关知识请自行了解。
    默认情况下,没有可用的提供程序,你必须在安装完成之后使用”Auth”程序添加一种或多种提供程序。在你添加供应程序之后,
    你可以使用存在的账号连接phabricator(比如你可以使用GitHub的OAuth账号登录Phabricator)或者用户使用它注册新的账
    号(假设你启用这些选项)
    恢复管理员账号如果你意外的在phabrication中锁住了,你可以使用bin/auth脚本恢复管理员账号的访问,恢复访问,请使用:
    ./bin/auth recover <username>,username是你想恢复的管理员的账号。
    通过web页面管理账号使用管理员账号登录phabricator并路由到/people,点击”People”,如果你是管理员,你可以看见创建
    及修改账号的选项。
    手动的创建新账号,有两种手工创建账号的方式,一种是通过web网页另一种是通过命令行,./bin/accountadamin,一些选项
    (如设置密码,更改账号标记)只能在命令行中可见。你可以使用此命令来使一个用户成为管理员(比如你意外的移除了你管理员的标记)
    或者创建一个管理员账号。

TODO(配置文件存储)

TODO(配置邮件发送)

内部实例的伪实现(思考)

为什么需要内部实例呢?
React的主要特性是你可以重新渲染任何东西,并且你不会重复创建DOM以及重置state.

1
2
3
ReactDOM.render(<App />, rootEl);
//下面这一行会重新使用已存在的DOM
ReactDOM.render(<App />, rootEl);

当组件需要更新时,如何存储必要的信息呢?比如说publicInstances,DOM元素与组件之间的关系。
堆栈协调器代码库通过在类中加了一个mount()函数解决这个问题,这种方式有点缺点,正在改进。
mountHost()mountComposite()分离出来的替代方案是创建了两个类DOMComponentCompositeComponent
这两个类都有一个接收element的构造函数,当然还有一个mount()方法返回已镶嵌的节点,用一个工厂方法实例化正确的
class来替代顶级mount()方法。

1
2
3
4
5
6
7
8
function instantiateComponent(element) {
var type = element.type;
if(typeof type === 'function') {
return new CompositeComponent(type);
}else if(typeof type === 'string') {
return new DOMComponent(type);
}
}

CompositeComponent(复杂组件)的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class CompositeComponent {
constructor(element) {
this.currentElement = element;
this.renderedComponent = null;
this.publicInstance = null;
}

getPublicInstance() {
// For composite components, expose the class instance.
return this.publicInstance;
}

mount() {
var element = this.currentElement;
var type = element.type;
var props = element.props;

var publicInstance;
var renderedElement;
if (isClass(type)) {
// Component class
publicInstance = new type(props);
// Set the props
publicInstance.props = props;
// Call the lifecycle if necessary
if (publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
renderedElement = publicInstance.render();
} else if (typeof type === 'function') {
// Component function
publicInstance = null;
renderedElement = type(props);
}

// Save the public instance
this.publicInstance = publicInstance;

// Instantiate the child internal instance according to the element.
// It would be a DOMComponent for <div /> or <p />,
// and a CompositeComponent for <App /> or <Button />:
var renderedComponent = instantiateComponent(renderedElement);
this.renderedComponent = renderedComponent;

// Mount the rendered output
return renderedComponent.mount();
}
}

DOMComponent类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DOMComponent {
constructor(element) {
this.currentElement = element;
this.renderedChildren = [];
this.node = null;
}

getPublicInstance() {
// For DOM components, only expose the DOM node.
return this.node;
}

mount() {
var element = this.currentElement;
var type = element.type;
var props = element.props;
var children = props.children || [];
if (!Array.isArray(children)) {
children = [children];
}

// Create and save the node
var node = document.createElement(type);
this.node = node;

// Set the attributes
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, props[propName]);
}
});

// Create and save the contained children.
// Each of them can be a DOMComponent or a CompositeComponent,
// depending on whether the element type is a string or a function.
var renderedChildren = children.map(instantiateComponent);
this.renderedChildren = renderedChildren;

// Collect DOM nodes they return on mount
var childNodes = renderedChildren.map(child => child.mount());
childNodes.forEach(childNode => node.appendChild(childNode));

// Return the DOM node as mount result
return node;
}
}

结果是,每一个内部实例都不论是复合型的还是平台特有的,都指向他的child的内部实例,比如:
<App>组件渲染了一个<Button>,<Button>渲染了一个<div>,内部实例树将是下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
[object CompositeComponent] {
currentElement: <App />,
publicInstance: null,
renderedComponent: [object CompositeComponent] {
currentElement: <Button />,
publicInstance: [object Button],
renderedComponent: [object DOMComponent] {
currentElement: <div />,
node: [object HTMLDivElement],
renderedChildren: []
}
}
}

在DOM中只看见<div>,但是内部实例可以包含混合型及平台特有的内部实例二者。混合型内部实例
需要存储:

  • 当前元素
  • 如果元素类型是类,则包含公共实例
  • 单独的渲染的内部实例,既可以是DOMComponent,也可以是CompositeComponent.
    平台特有内部实例需要存储:
  • 当前元素
  • DOM节点
  • 所有的子内部实例,它们既可以是DOMComponent也可以是CompositeComponent.

设计原则

我们写这篇文章的目的是为了让你对React有一个更好的认知,什么能做和什么不能做以及React的设计理念
虽然我们很乐意看到社区的贡献,但是我们也不希望悠然违反一些原则。
注意:这篇文章是假设你对React有一个很深的理解,它描述了React的设计原则,而不是React组件及程序
对React的简介请阅读Thinking in React

组成

React的主要特性是组建的组成,不同人编写的组件可以一起更好的工作,你可以给一个组件添加功能而不会影响整个代码库的更改.
比如:在一个组件中引入一些本地state而不会对使用它的组件造成影响。同样,如果有必要可以添加初始化以及拆卸的代码到任何组件中。
在组建中使用state或生命周期挂钩也不是坏的,比如一些功能强大的特性,它们应该有节制的使用,但是我们没有移除它们的打算,相反的,
我们认为他们是使得的React更有用的组成部分。我们将来会使更多的功能模式
但是,本地state和生命周期挂钩将为该模型的一部分。
组件经常被描述为’just functions’,但是在我们的视角中,他们需要变得更加有用,在React中,组件描述了任何组成的行为,包括rendering
生命周期,以及state,一些外部的库,像Relay增加组件了其他的责任,比如描述data依赖。

Common Abstraction(通用的抽象)

通常情况下我们抵制在用户级就可以实现的行为,我们不想用无用的库膨胀你的应用,但是也有些例外,比如:如果React对本地state和生命周期
挂钩不提供支持,用户会创建通用抽象给他们,当有多个抽象竞争时React不能充分利用他们的属性执行,它不得不以最低的标准工作。这也是为什么
我们有时会给React添加一些特性,如果我们注意到许多组件不兼容或低效的执行某些功能,我们可能更愿意考到React。我们不会轻易的干这件事。
当我们做它时,因为我们有信心提高抽象层次使整个生态系统受益。state,生命周期挂钩跨浏览器事件正常化是个很好的例子。我们经常与社区讨论
改善建议,你可以在big picture中找到这些讨论

Escape Hatches(逃生舱口)

React是实用主义的,它是由写Facebook的产品需求而驱动的,虽然它受一些范例的影响,但也不是完全主流。比如,函数式编程,对不同技能的开发
保持可访问是项目的一个明确的目标。如果我们想反对一个我们不喜欢的模式,在反对它之前我们考虑所有存在的场景。如果一些模式对于构建app和有用
但是很难表达,我们会为它提供必要的API。

Stability(稳定性)

我们重视API的稳定性,Facebook有两万个组件在使用React,这也是我们为什么通常不愿意改变公共API或行为。但是”nothing changes”稳定性的意义有点被
高估了,它迅速停滞了。相反,我们更喜欢”在生产环境大量使用并且一些东西变化时有一个明确的迁移路径”的稳定意义。当我们反对一种形式时,我们在Facebook研究
它的内部使用并添加反对警告。这让我们评估改变的重大影响。有时,我们发现它太简单的话我们会退出,并且我们需要战略性的思考关于这次改动代码库的要点是否准备好。
如果我们自行这次的更改不是太具有破坏性并且此次战略性转移对用户场景是可实施的,我们会在社区中释放反对警告。我们会和外部使用React的用户密切联系并且我们会
监视流行的开源项目并指引他们修理这些警告。由于React代码库规模庞大,内部迁移成功是一个很好的指标并且其余公司也不会有问题。

Scheduling(时间表 进度表)

在React中即使你的组件被描述为函数,你也不能直接调用它们。每一个组件返回一个需要渲染什么的描述
并且这个描述包括用户自定义组件及主机组件。通过组件的递归得到render结果去更改UI树。这是一个不明显但是很强大的区别,因为你不会调用组件函数但是让React自己调用.
这意味着如果有必要React有权延迟调用它。在当下的实现中,在单个刻度期间,React递归的走着树并调用所有的要修改的树的render函数。在反应的设计中这是一个共同的主题,
一些流行的库实现的是”push”方法,当新数据可用时再执行计算。然而React坚持”pull”方法,计算会被推迟,除非有必要。
React不是一个普通的数据处理库,它是用来构建用户界面的。我们认为在app中有独特的优势,因为它知道哪些计算是有意义的哪些没有。如果有些多东西在屏幕外,我们可以
延迟它的数据逻辑处理,如果数据到达的速度快于frame的速度,我们可以批量更新,我们可以优先考虑用户交互的工作(点击button的动画效果)来避免dropping frame,
不太重要的(从网络获取数据)后台工作

实现说明

实现说明

这一节主要是对堆栈协调器的实现说明,对React的API有一个技术性的理解,以及React是如何将
它分为core,renders,reconciler是很重要的,如果你对React的代码库不是很熟悉,请阅读React
代码库的概览。stack reconciler(堆栈协调器)供应所有React的生产代码。它位于src/renders/shared/stack/reconciler目录下
并且ReactDOM和ReactNative都使用它。

概述

协调器没有一个公共的API,ReactDOM和ReactNative的渲染器有效的使用它有效的更新用户接口,这些接口是用户编写的用户组件。
以递归的过程镶嵌
你第一次镶嵌一个组件时:

1
ReactDOM.render(<App />, rootEl);

ReactDOM会顺着reconciler(协调器)传递<App />,自助<App />是一个React元素,下面的是一个渲染什么的描述,你可以认为它是
一个简单的对象:

1
2
console.log(<App />);
// { type: App, props: {} }

协调器会检查<App/>是一个class还是一个函数,如果App是一个函数,协调器会调用App(props)来获取要渲染的元素。
如果App是一个类,协调器会用new App(props)实例化这个App,调用componentWillMount()生命周期方法,然后调用render()方法
来获取渲染的元素.
无论哪种方式,协调器会知道App要渲染的元素。这个过程是递归的,App有可能渲染到<Greeting />, Greeting可能渲染到<Button / >等等,
协调器会递归的通过用户定义的组件向下挖去,知道它知道每一个组件会渲染成什么。你可以假设这个过程的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function isClass(type) {
//React.Component的子类都有这个标记
return (Boolean(type.prototype) && Boolean(type.prototype.isReactComponet));
}

//这个函数接受一个React元素,比如(<App/>)
//返回一个表示为镶嵌的树的DOM或原生节点
function mount(element) {
var type = element.type;
var props = element.props;
//我们需要查明/确定渲染的元素,将type作为函数运行或者创建一个实例并调用render
var renderedElement;
if(isClass(type)) {
//组件类
var publicInstance = new type(props);
//设置属性
publicInstance.props = props;
//如果有必要则调用生命周期
if(publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
//通过调用render()得到渲染的元素
renderedElement = publicInstance.render();
}else{
//组件函数
renderedElement = type(props);
}

//这个过程是递归的因为一个组件返回的元素有可能是一个别的类型的组件
return mount(renderedElement);
//注意:这个实现是没有完成的而且会无限循环,它只会处理类似<App />,<Button>的元素
//它还没有支持处理像<div />或<p/>
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

注意:这仅仅是一段伪代码,它不是真正的实现,它会导致栈溢出,因为我们没有讨论终止递归的条件。
我们回顾一下上面例子的几个关键概念:

  • React元素是一些简单的对象来代表组件类型和属性(比如<App/>)。
  • 用户自定义的组件(比如<App />)可以是一个类也可以是一个函数但是他们最终都会被渲染为元素。
  • “mounting”是一个递归的过程它用来创建DOM以及Native树,给React的顶级元素(比如<App>).

Mounting Host Elements(镶嵌主机元素)

如果我们最终不向屏幕渲染任何东西这个过程将毫无作用,除了用户自定义的组件,React元素也可以表示特定
平台的组件.比如,Button可能在render方法中会返回一个<div />。如果一个元素的类型是字符串,这样处理
主机元素:

1
2
console.log(<div />);
// { type: 'div', props: {} }

没有用户定义与主机元素相联系的代码。
当协调器遇到一个主机元素时,协调器会让render小心的镶嵌它,比如,ReactDOM会创建一个DOM节点,如果这个主机元素
有子节点,协调器会按照上面的算法递归的镶嵌,子元素是否是主机元素都没有关系(<div><hr></div>),复杂的组件(<div><Button/></div>
子组件生产出来的DOM节点将会追加到父DOM节点,递归的方式,组装为完整的DOM结构。
注意:协调器本身不依赖于DOM,完整的镶嵌过程依赖于render(有时候,在源码中称之为镶嵌快照),最终结果可以是DOM node(React DOM)
一个字符串(ReactDOM Server)或者是原声的视图(React Native).
如果我们想扩展去处理主机元素的话,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function isClass(type) {
// React.Component子类都有这个标记
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}

//这个函数只处理复杂类型的元素
//它只处理类似于<App/>和<Button />这样的,而不是<div/>
function mountComposite(element) {
var type = element.type;
var props = element.props;

var renderedElement;
if (isClass(type)) {
//组件类
var publicInstance = new type(props);
// 设置属性
publicInstance.props = props;
//如果有必要,调用生命周期
if (publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
renderedElement = publicInstance.render();
} else if (typeof type === 'function') {
//组件函数
renderedElement = type(props);
}

// This is recursive but we'll eventually reach the bottom of recursion when
//这是个递归的过程,但是当元素是主机元素时(比如<div/>而不是复杂的组件)我们最终会到达递归的底部
return mount(renderedElement);
}

//这个函数只处理主机类型元素
//比如,它处理<div/>,<p/>不会出来<App/>
function mountHost(element) {
var type = element.type;
var props = element.props;
var children = props.children || [];
if (!Array.isArray(children)) {
children = [children];
}
children = children.filter(Boolean);

//这一块的代码不会再协调器中
//不同的渲染器可能会初始化不同的节点
//比如,React Native可能会创建IOS或者Android的视图
var node = document.createElement(type);
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, props[propName]);
}
});

//镶嵌子元素
children.forEach(childElement => {
// Children may be host (e.g. <div />) or composite (e.g. <Button />).
//子元素们可能是主机元素(比如<div/>)或复杂性元素(<Button/>)
//我们会递归的装载他们
var childNode = mount(childElement);

//这行代码也是渲染器特有的
//不同的渲染器有不同的结果
node.appendChild(childNode);
});

//将DOM节点的装载结果返回
//这儿递归结束
return node;
}

function mount(element) {
var type = element.type;
if (typeof type === 'function') {
// User-defined components
return mountComposite(element);
} else if (typeof type === 'string') {
// Platform-specific components
return mountHost(element);
}
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

虽然这也能工作但里协调器的真正实现还有很远的距离,主要是漏掉了对更新的支持。

介绍(引入)内部实例

React的关键特性是你可以重复渲染任何东西,并且他不会创新创建DOM或重置state:

1
2
3
ReactDOM.render(<App />, rootEl);
//会重新使用存在的DOM
ReactDOM.render(<App />, rootEl);

然而,我们上面的实现知识装载初始状态的树,不能对他执行更新操作,因为没有存储一些必要的信息,
如publicInstance,或DOM节点对应于哪些组件。stack reconciler代码库通过将mount()函数和一个方法
放在一个类上。这种方式有一些缺陷,我们现在正在一相反的方向解决这个问题,重写reconciler正在进行中
不管怎样,现在它的工作方式就是这样的。
mountHostmountComposite函数拆分出来的替代方案就是,我们将创建两个类:DOMComponentCompositeComponent
两个类都有一个接收elements的constructor,还有一个返回镶嵌了dom节点的mount()方法,我们将会使用工厂模式代替类中
顶级的mount()函数。

1
2
3
4
5
6
7
8
9
10
function instantiateComponent(element) {
var type = element.type;
if(typeof type === 'function') {
//用户定义的组件
return new CompositeComponent(element);
}else if(typeof === 'string') {
//平台组件
return new DOMComponent(element);
}
}

首先,让我们思考CompositeComponent的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class CompositeComponent {
constructor(element) {
this.currentElement = element;
this.renderedElement = null;
this.publicInstance = null;
}

getPublicInstance() {
//对应复杂的组件,暴露出这个类实例
return this.publicInstance;
}

mount() {
var element = this.currentElement;
var type = element.type;
var props = element.props;
var publicInstance;
var renderedElement;
if(isClass(type)) {
//组件类
publicInstance = new type(props);
//设置属性
publicInstance.props = props;
//如果有必要,调用生命周期
if(publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
renderedElement = publicInstance.render();
}else if(typeof type === 'function') {
publicInstance = null;
renderedElement = type(props);
}

//保存公共实例
this.publicInstance = publicInstance;
//根据元素实例化子组件的内部实例
//<div>,<p>将为一个DOM节点
//<App /> <Button>将为一个复杂组件
var renderedComponent = instantiateComponent(renderedElement);
this.renderedComponent = renderedComponent;

//装载renderd的输出
return renderedComponent.mount();
}
}

重构mountHost()之后的主要区别是,我们可以保持this.node以及this.renderedChildren于内部的组件实例相关联。
我们也可以在未来将它们用于非破坏性的更新。因此,每一个内部实例,复杂的以及主机的,现在指向其孩子内部实例。
为了帮助构思它,如果一个<App>组件函数渲染了一个<Button>类组件,并且Button类渲染了一个<div>,内部实例树
长得是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
[object CompositeComponent] {
currentElement: <App />,
publicInstance: null,
rendererComponent: [object CompositeComponent] {
currentElement: <Button />,
publicInstance: [object Button],
renderedComponent: [object DOMComponent] {
currentElement: <div />,
node: [object HTMLDivElement],
renderedChildren: []
}
}
}

在DOM中你仅仅能看见div,但是内部实例树既包含复杂的内部实例也包含主机的内部实例。
复杂的内部实例需要被存储:

  • 当前的元素
  • 如果元素类型是个类,public实例
  • 单个的内部实例的渲染结果,它既可以是DOMComponent也可以是CompositeComponent
    主机内部实例也需要被存储:
  • 当前的元素
  • DOM节点
  • 所有的子内部实例,它们既可以是DOMComponent也可以是CompositeComponent.
    如果你努力设想在复杂的程序中一个内部实例是什么样的结构,(React DevTools)[https://github.com/facebook/react-devtools]
    可以给你一个近似的结果,主机实例是灰色的,复杂实例是紫色的。
    要完成这个重构,我们要引入一个函数镶嵌完整的树到节点容器。比如ReactDOM.render(),它返回一个公共实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mountTree(element, containerNode) {
//创建顶层的内部实例
var rootComponent = instantiateComponent(element);

//装载顶层组件到containetr
var node = rootComponent.mount();
containerNode.appendChild(node);
//返回它提供的公共实例
var publicInstance = rootComponent.getPublicInstance();
return publicInstance;
}

var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);

Unmounting(正在解除挂载)

现在我们有内部实例维持他们的字元素与DOM节点,我们可以实现unmounting,d对于一个复杂的组件,unmounting调用生命周期的钩子
然后递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CompositeComponent {

//....
unmount() {
//如果有必要调用生命周期的钩子
var publicInstance = this.publicInstance;
if(publicInstance) {
if(publicInstance.componentWillUnMount) {
publicInstance.componentWillUnMount();
}
}

//卸载单个渲染的组件
var renderedComponent = this.renderedComponent;
renderedComponent.unmount();
}
}

对于DOMComponent,unmounting告诉每一个子元素卸载:

1
2
3
4
5
6
7
8
9
class DOMComponent {
//...

unmount() {
//卸载所有children
var renderedChildren = this.renderedChildren;
renderedChildren.forEach( child => child.unmount());
}
}

在实践中,卸载DOM组件也会移除事件监听器并且清楚缓存,在这里跳过这些细节。
现在我们添加一个新的顶级函数调用unmountTree(containerNode),这和ReactDOM.unmountComponentAtNode()很相似:

1
2
3
4
5
6
7
8
9
10
function unmountTree(containerNode) {
//从DOM节点中读取内部实例
//(这个还不能工作,我们需要改变mountTree来来储存它)
var node = containerNode.firstChild;
var rootComponent = node._internalInstance;

//卸载这个树并且情况这个container
rootComponent.unmount();
containerNode.innerHTML = '';
}

为了使它工作,我们需要从DOM节点中获取内部根实例,我们将会修改mountTree()添加_internalInstance属性到根DOM节点。
我们也会告诉mountTree()销毁任何存在的树,所以它可以被调用多次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mountTree(element, containerNode) {
//销毁任何存在的树
if(containerNode.firstChild) {
unmountTree(containerNode);
}

//创建顶层内部实例
var rootComponent = instantiateComponent(element);

//装载顶层内部实例到container
var mode = rootComponent.mount();
containerNode.appendChild(node);

//保存一个到内部实例的引用
node._internalInstance = rootComponent;

//返回它所提供的公共实例
var publicInstance = rootComponent.getPublicInstance();
return publicInstance;
}

现在,运行unmountTree()或者运行mountTree()多次,移除旧的树然后运行组件的生命周期方法componentWillUnmount.

Updating

在上一节,我们实现了unmounting,但是如果每一个属性的变化都会改变unmounted和mounted整个树,这对于React来说没有用,
协调器的目的是尽可能的保持和重用现有实例:

1
2
3
4
var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);
//这里应返回已存在的DOM
mountTree(<App />, rootEl);

我们将会扩展我们的内部实例建立一个或多个方法,除了mount()unmount()DOMComponentCompositeComponent将会
实现一个新的receive(nextElement)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CompositeComponent {
//...

receive(nextElement) {
//...
}
}

class DOMComponent {
//...

receive(nextElement) {
//...
}
}

它的职责是根据下一个元素提供的说明做任何有必要的组建更新,这部分通常称之为’虚拟dom差异比较’,尽管真正发生的是内部树的递归然后
让每个内部实例接收到更新。

更新复杂组件(Updating Composite Component)

当一个复杂组件接收到一个新的元素时,我们运行componentWillUpdate()生命周期钩子。然后我们根据新的属性重新渲染组件,然后得到
下一个渲染的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CompositeComponent {

//...

receive(nextElement) {
var prevProps = this.currentElement.props;
var publicInstance = this.publicInstance;
var prevRenderedComponent = this.renderedComponent;
var prevRenderedElement = this.renderedElement;

//修改*自己的*元素
this.currentElement = nextElement;
var type = nextElement.type;
var nextProps = nextElement.props;

//解决下一次的render()输出的是什么
var nextRenderedElement;
if(isClass(type)) {
//组件类
//如果有必要调用生命周期
if(publicInstance.componentWillUpdate) {
publicInstance.componentWillUpdate(prevProps);
}
//修改属性
publicInstance.props = nextProps;
//重新渲染
nextRenderedElement = publicInstance.render();
}else if(typeof type === 'function') {
//组件函数
nextRenderedElement = type(nextProps);
}
}
}

接下来,我们可以看一下渲染的元素的类型,如果在最后一次的渲染中type没有发生变化,下面的
组件可以就地更新。例如:如果第一次返回<Button color='red' />,第二次返回<Button color='blue'>
我们只需要告诉相关联的内部实例接收下一个元素:

1
2
3
4
5
6
7
8
9
//...


//如果渲染的元素的类型没有变化
//复用已存在的组建实例并退出
if(prevRenderedElement.type === nextRenderedElement.type) {
prevRenderedComponent.receive(nextRenderedElement);
return;
}

但是,如果下一次渲染的组件类型和上一次渲染的组件类型不同,我们不会修改内部实例,一个<button>不会变为
一个<input>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...

//如果我们达到这一点,我们需要卸载先前的
//装载组件,装载新的,交换他们的节点

//找到老节点因为它将被替换
var prevNode = prevRenderedComponent.getHostNode();

//卸载老child并装载新的child
prevRenderedComponent.unmount();
var nextRenderedComponent = instantiateComponent(nextRenderedElement);
var nextNode = nextRenderedComponent.mount();

//替换child的引用
this.renderedComponent = nextRenderedComponent;

//用新的替换老节点
//注意:这是渲染器特定的代码并且在理想情况下,因位于CompositeComponent的外面
prevNode.parentNode.replaceChild(nextNode, prevNode);
}
}

综上所述,当一个复杂的组件接收到一个新的元素时,要么委托去修改内部实例,要么卸载它并在原来
的位置新建一个。
这里有另外一种情况,一个组件将会重新装载而不是接收一个元素,当元素的key改变时,我们不讨论
key的处理,因为它会增加教程的复杂性。注意:我们需要在内部实例添加一个叫getHostNode的方法,
它可能位于特定平台的节点并取代它的更新过程。它的实现简单明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CompositeComponent {
// ...

getHostNode() {
// Ask the rendered component to provide it.
// This will recursively drill down any composites.
return this.renderedComponent.getHostNode();
}
}

class DOMComponent {
// ...

getHostNode() {
return this.node;
}
}

更新主机组件

主机组件实现,比如DOMComponent已不同的方式更新,当它们接收到新的元素时,他们需要更新基于平台特有的视图,
在React DOM的情景中,这意味着要更新DOM属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DOMComponent {
//...
receive(nextElement) {
var node = this.node;
var prevElement = this.currentElement;
var prevProps = prevElement.props;
var nextProps = nextElement.props;
this.currentElement = nextElement;

// Remove old attributes.
Object.keys(prevProps).forEach(propName => {
if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
node.removeAttribute(propName);
}
});
// Set next attributes.
Object.keys(nextProps).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, nextProps[propName]);
}
});

// ...
}

接着,主机组件需要更新它们的children,和复杂组件不同的是,他们可能包含不止一个child,在下面这个简单的例子中,
我们使用内部实例数组并遍历它,是更新还是替换内部实例依赖于接收到的type是否与他们先前的type一致,真正的reconciler
也会也需要元素的key来进行插入还是删除,但是我们省略相关的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// ...

//这是React元素的数组
var prevChildren = prevProps.children || [];
if (!Array.isArray(prevChildren)) {
prevChildren = [prevChildren];
}
var nextChildren = nextProps.children || [];
if (!Array.isArray(nextChildren)) {
nextChildren = [nextChildren];
}
//这是内部实例的数组
var prevRenderedChildren = this.renderedChildren;
var nextRenderedChildren = [];

//当我们遍历children时,我们要添加对这个数组的一些操作
var operationQueue = [];

//注意:下面的部分非常简单,他不会处理重新排序,它的存在只是为了说明整体流程,但不具体
for (var i = 0; i < nextChildren.length; i++) {
// Try to get an existing internal instance for this child
var prevChild = prevRenderedChildren[i];

// If there is no internal instance under this index,
// a child has been appended to the end. Create a new
// internal instance, mount it, and use its node.
if (!prevChild) {
var nextChild = instantiateComponent(nextChildren[i]);
var node = nextChild.mount();

// Record that we need to append a node
operationQueue.push({type: 'ADD', node});
nextRenderedChildren.push(nextChild);
continue;
}

// We can only update the instance if its element's type matches.
// For example, <Button size="small" /> can be updated to
// <Button size="large" /> but not to an <App />.
var canUpdate = prevChildren[i].type === nextChildren[i].type;

// If we can't update an existing instance, we have to unmount it
// and mount a new one instead of it.
if (!canUpdate) {
var prevNode = prevChild.node;
prevChild.unmount();

var nextChild = instantiateComponent(nextChildren[i]);
var nextNode = nextChild.mount();

// Record that we need to swap the nodes
operationQueue.push({type: 'REPLACE', prevNode, nextNode});
nextRenderedChildren.push(nextChild);
continue;
}

// If we can update an existing internal instance,
// just let it receive the next element and handle its own update.
prevChild.receive(nextChildren[i]);
nextRenderedChildren.push(prevChild);
}

// Finally, unmount any children that don't exist:
for (var j = nextChildren.length; j < prevChildren.length; j++) {
var prevChild = prevRenderedChildren[j];
var node = prevChild.node;
prevChild.unmount();

// Record that we need to remove the node
operationQueue.push({type: 'REMOVE', node});
}

// Point the list of rendered children to the updated version.
this.renderedChildren = nextRenderedChildren;

// ...

最后一步,我们执行DOM操作,真实的reconciler是很复杂的因为它还得处理移动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Process the operation queue.
while (operationQueue.length > 0) {
var operation = operationQueue.shift();
switch (operation.type) {
case 'ADD':
this.node.appendChild(operation.node);
break;
case 'REPLACE':
this.node.replaceChild(operation.nextNode, operation.prevNode);
break;
case 'REMOVE':
this.node.removeChild(operation.node);
break;
}
}
}
}

主机组件更新也是这样。

顶级更新

现在CompositeComponentDOMComponent实现了receive(nextElement)方法,当元素类型同最后一次相同时,
我们可以改变顶层mountTree()来使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mountTree(element, containerNode) {
// Check for an existing tree
if (containerNode.firstChild) {
var prevNode = containerNode.firstChild;
var prevRootComponent = prevNode._internalInstance;
var prevElement = prevRootComponent.currentElement;

// If we can, reuse the existing root component
if (prevElement.type === element.type) {
prevRootComponent.receive(element);
return;
}

// Otherwise, unmount the existing tree
unmountTree(containerNode);
}

// ...

}

现在调用mountTree()两次非破坏性的

1
2
3
4
5
var rootEl = document.getElementById('root');

mountTree(<App />, rootEl);
// Reuses the existing DOM:
mountTree(<App />, rootEl);

这是React内部工作的基本原理。

What We Left Out

这篇文章和真实的代码库进行了一个简单的比较,这儿有几个我们没有解决的问题:

  • 组件可以返回null,reconciler(协调器)可以处理数组中的”空插槽(empty slots)”并渲染出去。
  • 协调器会从元素中读取key,并且用它建立内部实例和数组中元素之间的关系,在真实的React实现中,大量的复杂性和它有关。
  • 除了复杂的以及主机的内部实例,还有一些类对应空元素和文本组件。可以通过null代表文本节点和数组的空插槽。
  • renders(渲染器)使用注入来传递主机内部类给reconciler(协调器),比如,React DOM告诉协调器使用ReactDOMComponent作为主机内部实例来实现.
  • 更新子机列表的逻辑被提取到一个叫ReactMultiChild 的一个mixin,它被用与ReactDOM和React Native的内部实例类的实现。
  • 协调器也实现了在复杂组件中对setState()的支持,在事件处理中,大量的更新会成批的放入单个更新。
  • 协调器会小心处理附加及分离到复杂组件和主机节点的refs
  • 生命周期的钩子,会在DOM准备好之后调用,像componentDidMount(),componentDidUpdate(),获取’回调队列’并批处理执行。
  • React将当前的更新的信息放到一个叫transaction的内部对象中,transactions用于追踪挂起的生命周期钩子队列。当前DOM的
    内部警告以及其余”global”会给到一个特定的更新,transactions用来确保React更新后清空一切,例如,transactions提供
    React DOM恢复输入选择后的更新。

Jumping into the Code

  • ReactMountis where the code like mountTree() and unmountTree() from this tutorial lives,它负责装载和卸载顶级组件
    ReactNativeMount是ReactNative的模拟。
  • 本教程中ReactDOMComponentDOMComponent是同等的,它实现了ReactDOM渲染器的主机组件类。ReactNativeBaseComponent
    React Native的模拟。
  • ReactCompositeComponentCompositeComponent是同等的,它处理调用用户自定义组建以及维持state。
  • instantiateReactComponent包含了一个开关用来选择一个正确的内部实例类用来构造一个元素。在此教程中和instantiateComponent()是同等的。
  • ReactReconcilermountComponentreceiveComponent以及unmountComponent方法的包装。它在内部实例上调用底层的实现。当然它也
    包含所有内部实例实现的共享代码。
  • ReactChildReconciler实现了通过元素的key,mountingupdating,unmountingchildren的逻辑。
  • ReactMultiChild实现了渲染器对操作队列的处理。child的插入,移动,删除。
  • mount(),receive(),unmount因为一些遗留原因在React代码库中,实际上被称为mountComponent(),receiveComponent,
    unmountComponent()
  • 内部实例的属性以下划线开头,比如,_currentElement,在代码库中自始至终它们被认为是只读的。

Future Directions(未来方向)

stack reconciler具有的局限性,如同步,不能中断工作或chunks的拆分。在新的Fiber reconciler中有一个完全不同的结构,在未来我们打算使用
它代替现有的reconciler,但是目前还很遥远。

React Reconciler Algorithm

Reconciliation(协调器)

React提供了一个声明式的API,所以你不用担心在每次更新时到底发生了什么变化,这使得写代码变得简单了许多
但是React是如何实现的对我们来说是透明的不明显的。这篇文章阐述了React的’diffing’算法所做的选择,因此
组件的更新是可预见的并且是高性能的。

Motivation(动机)

当你使用React时,你可以认为在一个单一的时间点render()函数创建了一个React元素的树,当下一个props或state
更新时,render()函数将会得到一个新的不同的React元素的树,React需要解决如何高效的更新UI以匹配最新的树。
一些通用方案有性能问题,时间复杂度会为(O(n3))如果在React使用这些通用方案的话,100个元素将会进行一百万次
的比较,话费太昂贵了,替代方案是,React实现了一种基于以下两种假设的时间复杂度为O(n)的算法:

  1. 两种不同类型的元素将会产出不同的树。
  2. 开发人员可以暗示在render时维持一个稳定的key属性。
    在实践中,这些假设在实际使用的情况下都有效。

The Diffing Algorithm(Diff算法)

当比较两个树时,React首先会比较两者的根元素,该行为取决于根元素的类型。
不同类型的元素
每当根元素有不同类型时,React将会销毁该树并且重新构建新的树,从<a><img>,<Article>Coment,
<Button><div>都会进行全新的重新构建过程。当拆卸一个树时,旧的DOM节点会被销毁,组件实例收到componentWillUnmount()
当构建新树的时候,新的DOM节点将会被插入到DOM中,组件实例会收到componentWillMount()接着会componentDidMount()
任何和旧树相关的状态都会丢失.任何在root节点之下的组件以及他们的state都会被销毁,比如:

1
2
3
4
5
6
7
<div>
<Counter />
</div>

<span>
<Counter />
</span>

这将会销毁<Counter>并重新镶嵌一个新的<Counter>.

相同类型的DOM元素

当比较两个相同类型的DOM元素时,React会查看两个组件的属性,保持相同的DOM节点,只会更新改变的属性,例如:

1
2
3
<div className="before" title="stuff" />

<div className="after" title="stuff" />

当比较这两个DOM元素时,React知道只修改基础DOM节点的类名。当更新style时,React同样知道只改变的变化了的属性
例如:

1
2
3
<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

当在两个元素之间转换时,React知道仅修改color样式。不会去修改fontWeight样式。处理完DOM节点之后,React会递归操作
子节点。

相同类型的组件节点

当一个组件更新时,该实例保持不变,所以在render时state是被维护的,React修改基础组件实例的属性,以匹配新的元素,然后调用
基础组件实例的componentWillReceiveProps()componentWillUpdate()
接下来,render()方法被调用,然后diff算法(diff algorithm)递归上次的结果和最新的结果.

子组件的递归

默认情况下,当递归一个DOM节点的子节点时,React在同一时间仅仅是迭代子组件们的列表并且每当有差异的时候会生成一个突变。
例如:当在子组件的最后添加一个新的元素时,这两个树的之间的转变,工作的很好。

1
2
3
4
5
6
7
8
9
10
<ul>
<li>first</li>
<li>second</li>
</ul>

<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>

React将匹配两个<li>first</li>树,匹配<li>second</li>树,然后去插入<li>third</li>树。
如果你天真的实现,当插入在最开始的位置,那么会有很糟糕的性能问题。比如,以下的这两课树之间的转换很差:

1
2
3
4
5
6
7
8
9
10
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>

<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>

React将会改变每一个子节点而不是实现它,它可以保持<li>Duke</li><li>Villanova</li>的子节点的完整性,
这种低效性是一个问题.

Keys

为了解决这一个问题,React支持了一个key的属性,当子组件有key属性时,React使用这个key,来匹配原始树的子节点与
随后的树中的子节点,通过添加key使树的转换更加高效。

1
2
3
4
5
6
7
8
9
10
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>

<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>

现在React知道持有’2014’的key是新的节点,而持有20152016的节点仅仅移动就可以了,在实践中,发现一个key并不是
很难,你将要陈列的元素也许已经有一个唯一的id了,所以这个key值来源于你的data:

1
<li key={item.id}>{item.name}</li>

当不是这种场景时,你可以添加一个key或生成一个key,这个key只需要和他相邻的节点不同就可以了,不需要全局唯一。
作为最后的手段,你可以传递数组的下标作为key,如果列表不需要排序这工作的很好,但是重新排序会很慢。

Tradeoffs(权衡)

记住reconciliation算法的实现细节很重要,React可以在每一个动作下重新render整个App,最终结果相同,我们会定期的改善
使常见的场景更加快速。
在最近的实现中,你可以表达一个事实,一个子组件已经从它的兄弟节点中移除了,但是你不能告诉它被移到哪里了。算法会重新
render整个子组件们。
因为React依赖于启发式算法,如果不能满足它背后的假设,性能会受影响。

  1. 该算法不会视图去匹配不同类型节点的子树,如果你发现两个类型的组件的输出很相似,你可以让他们变为相同的类型,在实践中
    我们还没发现这会成为一个问题。
  2. keys应该是稳定的,可预测的,以及唯一的,不稳定的key(比如:Math.random())将会引起,组件实例和DOM节点不必要的重新创建,
    从而导致性能退化和状态丢失.

React代码库概述

代码库概述

这一节主要是给我们一个React代码库的概述,他的一些规范以及实现,如果你想对React有所贡献的话
这个指南将会使你更加舒服的更改。我们不一定推荐在React应用程序的任何这些惯例,有些惯例由于一些
历时原因而存在但是后期也许会更改.

自定义模块系统(Custom Module System)

在Facebook,内部的我们使用的是一个名叫’Haste’的自定义模块,他和commonjs和类似并且也使用require()
但是有一些重要的不同而正是这些使外部的贡献者很困惑。在Commonjs中,当你导入一个模块时,你需要明确指定
它的相对路径:

1
2
3
4
5
6
7
8
// Importing from the same folder:
var setInnerHTML = require('./setInnerHTML');

// Importing from a different folder:
var setInnerHTML = require('../utils/setInnerHTML');

// Importing from a deeply nested folder:
var setInnerHTML = require('../client/utils/setInnerHTML');

然而,在Haste中所有的文件名都是全局唯一的,在React代码库中,你可以按照文件名导入其他的任何模块。

1
var setInnerHTML = require('setInnerHTML');

Haste最初是用来开发大型app的比如像Facebook,很容易将文件移动到不同的目录并且无需担心他们的相对路径
在任何编辑器的模糊查询会让你得到正确的位置,而这归功于全局唯一的文件名.
React是从Facebook的代码库中提取出来的,所以使用Haste是历时原因,在将来,我们有可能会移植Commonjs或ES6
的模块化方式同社区更好的对齐,但是这需要Facebook内部基础架构做出很大改变,所以也不是一朝一夕就能完成的.
如果你记得一下这些规则,Haste会对你更有意义:

  1. 在React原生代码库中的所有文件名都是全局唯一的,这也是为什么有些文件名繁长啰嗦的原因。
  2. 当你添加了一个新文件时,确认你包含了许可证的标头,你可以从别的地方复制一份,一个许可证标头的格式一般是:
1
2
3
4
5
6
7
8
9
10
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule setInnerHTML
*/

记得更改@providesModule后面的名字,匹配你新创建的文件.
当我们为npm编译React时,一个script脚本将会复制所有的模块到一个单一的没有子目录的目录,命名为lib
预制所有的require()路径为./.,在这种方式下,Node,Browserify,Webpack,等工具将会理解React的生成输出而无需
关心Haste。
如果你正在github上读react源码,并且想跳到某一个文件,请按’t’
这是Github用来在当前仓库模糊搜索文件的快捷方式,输入你要搜索的文件名,它将会显示匹配的文件。

外部依赖

React几乎没有外部依赖,通常情况下,require()指向的是React自己代码库的文件,但是也有几个比较少见的例外:
如果你发现require()的文件在React仓库中没有所对应的文件,那么你可以在fbjs
这个仓库中找到,例如require('warning')将会从fbjs的warning模块中解析。
fbjs仓库存在是因为React共享了一些工具类比如Relay,我们保持了他们之间的同步关系.
我们不依赖Node生态系统的等效小模块,因为我们希望Facebook的工程师在有需要的时候都可以做出改变,所有fbjs内部的实用程序不会被
作为开放的API,它们的意图仅仅是用来服务于Facebook的项目,比如React。

顶级文件夹

在克隆React仓库之后,你会发现有几个顶级文件夹在里面:

  1. src是React的源码,如果你要修改相关的代码,src文件夹将是你花费时间最长的.
  2. docs是React文档网站,当你修改了API时,请确认修改了相应的markdown文件
  3. examples包含了一些生成不同配置的React例子
  4. packages包含了元数据用来对应React仓库中的所有包(比如package.json),尽管如此,他们的源码还是位于src文件夹中。
  5. build是React的生成输出,他不会再React仓库中出现,但是他会出现在你运行生成命令之后.
    还有一些顶级目录,但是他们仅仅是工具类,很有可能你在贡献代码时,根本不会碰到他们。

协同定位测试

我们没有一个顶级的测试目录,我们将测试用例放在一个__tests__的目录下代替。
例如,setInnerHTML.js的测试用例放在__tests__/setInnerHTML-test.js.

共享的代码

尽管Haste允许我们导入React仓库的任何模块,我们得遵循一个规范用来避免循环依赖以及其他的怪异现象,按照规范,一个文件仅能导入相同
目录以及子目录下的文件。
例如:在src/renders/dom/client下的文件可以导入相同目录以及该目录的所有子目录中的文件.但是他们不能导入src/renders/dom/server
下的文件因为它不是src/renders/dom/client的子目录。
此规则的例外情况,有时候,我们需要在不同组的模块里共享一些公共的函数,在这种情况下,我们提升共享模块到这些目录最近的父目录里的一个叫
shared的模块里.
比如:src/renders/dom/clientsrc/renders/dom/server共享一段代码,那么这段代码应该位于src/renders/shared.
按照同样的逻辑,如果src/renders/dom/clientsrc/renders/native共享一个工具类,那么此工具类应该位于src/renders/shared.
这个规范虽然不是强制的,但是当我们遇到pull request时会检查.

警告及不变量

React代码库用warning模块来展示警告:

1
2
3
4
5
6
var warning = require('warning');

warning(
2 + 2 === 4,
'Math is not working today.'
);

warning条件为false时,警告将会显示
这种思考方式是条件响应的是正常情况而不是异常情况。
这是一种避免垃圾邮件以及控制台重复输出的好主意。

1
2
3
4
5
6
7
8
9
var warning = require('warning');
var didWarnAboutMath = false;
if(!didWarnAboutMath) {
warning(
2 + 2 === 4,
'Math is not working today'
);
didWarnAbout = true;
}

警告仅会在开发环境下可用,在正是环境下,他们会被完全削去,如果你需要禁止一些代码路径的执行
使用invariant模块代替.

1
2
3
4
5
var invariant = require('invariant');
invariant(
2 + 2 === 4,
'You shall not pass!'
);

invariant为false的时候,将引发
“invariant”是这种情况一直为true的一种说明方式,你可以把它当做断言。
保持开发和正是环境行为一直是很重要的,所以invariant可以在正式及开发环境引发,警告信息会自动
的被替换为错误码以避免字节数大小的影响。

开发和正式

你可以在代码库使用__DEV__伪全局变量,守卫只针对开发环境的代码块。
在编译阶段中,在CommonJS构建时,它将会被转变为process.env.NODE_ENV !== 'production'
在独立构建时,在未压缩的构建时它变为true,在压缩构建的时候它将会被剔除。

1
2
3
if (__DEV__) {
// This code will only run in development.
}

JSDoc

一些内部的以及公共的方法以JSDoc形式的注释注释:

1
2
3
4
5
6
7
8
9
10
/**
* Updates this component by updating the text content.
*
* @param {ReactText} nextText The next text content
* @param {ReactReconcileTransaction} transaction
* @internal
*/
receiveComponent: function(nextText, transaction) {
// ...
},

我们试图让存在的注释更新但我们不是强制的,我们没有在新写的代码中使用JSDoc,替代方案是
我们使用Flow强制类型.

Flow

我们最近引入Flow来检查代码库,在许可证标头用@flow标记的文件
将会执行类型检查.
我们接受给已存在代码添加@flow的pull请求,Flow注释长这样:

1
2
3
4
5
6
ReactRef.detachRefs = function(
instance: ReactInstance,
element: ReactElement | string | number | null | false,
): void {
// ...
}

只要有可能,新提交的代码应该使用Flow注解,你可以在本地运行npm run flow来检查你的代码.

类和Mixins

React原来是用ES5编写的,我们已经用babel支持了ES6的特性。包含classes,
但是,许多React代码还是用ES5写的。
尤其是,你可能经常看到以下的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Constructor
function ReactDOMComponent(element) {
this._currentElement = element;
}

// Methods
ReactDOMComponent.Mixin = {
mountComponent: function() {
// ...
}
};

// Put methods on the prototype
Object.assign(
ReactDOMComponent.prototype,
ReactDOMComponent.Mixin
);

module.exports = ReactDOMComponent;

这段代码的Mixin和React的mixins特性并无关系,这只是一种分组在一个对象的方法的方式,这些方法有可能
在后期获取并附加到别的class上,我们使用了这种方式尽管我们尝试在新代码中避免使用。同等的ES6代码长得是
这个样子的:

1
2
3
4
5
6
7
8
9
10
11
class ReactDOMComponent {
constructor(element) {
this._currentElement = element;
}

mountComponent() {
// ...
}
}

module.exports = ReactDOMComponent;

有时候我们会转换ES5代码为ES6代码,然而这对我们来说不是非常重要,因为有一个正在进行的努力,React协调器的实现带有
少量的面向对象的方法,我们完全不需要使用class。

动态注入

React在有些模块中使用了动态注入,虽然它易于理解,但是不幸的是,它阻碍了我对代码的理解,它存在的主要原因是React起初
只支持DOM作为target,React Native是已React的分支开始的,我们不得不添加动态注入让React Native覆盖一些行为.你有可能
会看见一些模块声明动态依赖是以下面的这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Dynamically injected
var textComponentClass = null;

// Relies on dynamically injected value
function createInstanceForText(text) {
return new textComponentClass(text);
}

var ReactHostComponent = {
createInstanceForText,

// Provides an opportunity for dynamic injection
injection: {
injectTextComponentClass: function(componentClass) {
textComponentClass = componentClass;
},
},
};

module.exports = ReactHostComponent;

injection区域无论如何不会被特殊的处理,但是按照惯例,它意味着此模块在运行的时候想注入一些模块,在React DOM中,
ReactDefaultInjection注入了DOM细节的实现ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent);,
在React Native中ReactNativeDefaultInjection注入了它自己的实现,在代码库中有多个注射点,在未来,我们打算拜托动态注入的这种机制,在静态构建时串起所有的碎片文件。

大量的包

React是一个monorepo,它的仓库包含了大量的独立的包,所以他们的改变可以协调在一起,并且文档以及问题是在一起的,
npm元数据比如package.json文件在位于顶层目录package文件夹下的,但是,这里面几乎没有代码。
比如,packages/react/react.js再导出src/isomorphic/React.js,真实的npm入口,其余的包大多数重复相同的规则,所有重要的代码都位于src文件夹下。
虽然在源码结构里代码都是独立的,确切的包的边界还是和npm的以及browser构建略有不同。

React Core

React的核心包含了所有顶级的React API,比如:

  • React.createElement()
  • React.createClass()
  • React.Component
  • React.Children
  • React.PropTypes
    React核心仅仅包含了定义模块的API,不包括reconciliation算法以及具体平台的代码,它被用于React DOM以及React Native,React的核心代码位于src/isomorphic
    它可以在npm的react包中获取,相应的browser中是react.js,它导出的全局变量是React。

Renders

React最初是唯DOM创建的但是后来改编用来适应原生平台React Native,在这里介绍React内部构建的概念。
Renders管理一个React树如何变成底层平台的调用,Renders位于src/renders文件夹下:

  • React DOM Render渲染React组建为DOM。它实现了顶层的ReactDOM API并且可以在npm的react-dom中获取到,它可以
    被用来做单独的browser bundle(react-dom.js)它导出了ReactDOM全局变量。
  • React Native Render渲染React组建为原生视图,它是在React Native内部使用的,通过react-native-render npm包
    将来可能会将它copy一份到React Native的仓库中,这样React Native可以在自己的工作空间中修改React了。
  • React Test Render渲染React组建为JSON树,它使用了Jest的快照测试功能,在npm的react-test-render包中获取。
    剩下的官方支持的render是react-art,为了避免我们在修改react时意外的破坏它,我们要在src/renders/art目录中运行测试运例
    虽然如此,它的github仓库still acts as the source of truth。
    虽然在技术上,我们可能会自定义render,目前不会官方支持,自定义render没有一个公共的稳定的协议,这就是我们将它放在单独的
    一个地方的原因。
    注意:在技术上,native render是很薄的一层用来让React和React Native相互作用,真正具体管理原生视图的代码是在React
    Native仓库中

Reconciler(协调器)

甚至完全不同的render比如React DOM和React Native需要共享相同的逻辑,尤其是reconciliation算法应尽可能相似,声明的渲染,
自定义组建,状态,生命周期方法,以及refs始终跨平台工作。
为了解决这一问题,不同的renders共享一些代码。我们称这些代码为reconciler(协调器),当一个更新(setUpdate())被安排时,
reconciler(协调器)调用了组建中的render(),mounts,updates,或者unmount.
reconciler(协调器)没有被单独的分离出来,因为它们目前没有公共的API,它们会被renders使用,比如React DOM,React Native。

Reconciler栈

‘stack’reconciler供应于所有的线上React代码,它位于src/renducers/shared/stack/reconciler文件夹中,用于React和React Native
它是以面向对象的形式书写的(object-oriented)并且维护一个内部实例的树,(该树对应于所有React组件)。这个内部实例存在于自定义组件
及平台组件,这个内部实例对应用户来说不能直接访问,并且他们的树不会被暴露出来。当一个组件mounts,updates,unmounts,这个stack >reconciler将会调用内部实例的一个方法,这些方法是mountComponent(element),receiveComponent(element),和unmountElement(element).
Host Components
平台特定组件,比如<div>,’View’,例如,React Dom通知stack reconciler使用DOM组件的ReactDOMComponent去处理mounting,updates,
以及unmounting。不论什么平台,<div>,<View>处理管理多子组件的方式是类似的,为了方便,reconciler提供了一个叫ReactMultiChild
来供DOM和Native render使用。
复杂的组件
用户定义的组件和所有的renders表现形式是一样的,这就是为什么reconciler提供了一个叫ReactCompositeComponent的公共实现,
它使用于各平台的renderer。复杂的组件也要实现mounting,updating,unmounting,但是不同于平台组件,ReactCompositeComponent
需要依赖于不同的代码,这就是为什么在用户定义的类中有render(),componentDidMount()方法的原因。
在更新期间中,ReactCompositeComponent检查render()在最后一次输出的type,’key’是否不同,如果typekey有改变,他会委派
去更新已存在的内部实例,另外,它会卸载老的子组件,并镶嵌新的,这个会在Reconciliation algorithm(协调器算法)中描述。
Recursion(递归)
在一次更新中,stack reconciler会一直深入复杂的组件,运行他们的render()方法,并决定是否更新以及替换他们的唯一的组件。通过平台组件
(div View)执行平台代码,Host组件可能会有很多子组件,通常会递归处理。stack reconciler经常处理同步的单个处理组件树,当个别的树处理完成了
stack reconciler不会停止,所以当更新很深以及CPU有限时它不是最优的。

Fiber reconciler

‘fiber’ reconciler是一个新的努力用来解决stack reconciler里的继承问题以及长期存在的一些问题。
它是完全重写的reconciler,目前正在被开发
它的主要目标是:

  • 在chunks中可拆分中断的能力。
  • 在进程中确定优先次序、重订、复用的工作能力。
  • Ability to yield back and forth between parents and children to support layout in React。
  • render()返回多标签的能力
  • 更好的支持错误边界
    你可以阅读来更好的理解React Fiber Architecture,此刻,它处于试验阶段。
    它的代码位于src/renders/shared/fiber

Event System

React实现了一个综合型的事件系统,在ReactDOM和React Native都可工作,它的代码位于src/renders/shared/shared/event.

Add-ons(附加组件)

每个React的附件组件在npm都以react-addons-的前缀的包单独的分离出来。它们的代码位于src/addons.
另外,我们提供了一个单独的叫react-with-addons.js的输出文件,它包含了React代码以及所有的addons暴露出的对象。

redux

为什么要用react-redux?

在单页应用中,服务器的相应,UI状态,缓存数据,被选中的标签,是否加载动画效果等等这些都可以理解为state,当应用变得庞
大复杂时传统的javascript代码处理这些状态 ,只会让维护变得更加困难,而用redux的原因就是将应用程序中的state的变化变得
可预测

redux的三大原则

  1. 单一的store,整个应用的state存放在一个object tree中,并且这个store是唯一的
  2. state是只读的,唯一改变state的方法就是触发action,action就是一个描述你要干什么的js对象

    1
    2
    3
    4
    var actionName = {
    type: typeName,//必须为type表示你将要执行什么动作,
    desc: '' //自己定义,你将希望通过这个动作告知其余组建通过这个动作发生的事
    }
  3. 使用reducer修改state,reducer是一个纯函数,它接收的参数为先前的state和将要执行的action,并返回新的state

Action

Action是把数据从应用中传到store的有效载荷,它是store数据的唯一来源,一般通过store.dispatch( {type: 'xx', desc: 'xx'} )将action传到store

ActionCreator

ActionCreator就是生成action对象的函数,返回一个action对象

1
2
3
4
5
6
function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}



在store里调用action创建函数: store.dispatch(addTodo('learn redux'))
在React组建中如何调用呢?需要用到react-redux中提供的connect()(ComponentName)将dispatch函数注入到组建的props中然后通过
this.props.dispatch(addTodo(text))调用
bindActionCreators()待定


Reducer

Action只是描述了事件发生了而已,但是并没有指明应用如何更新state,而更新state正是reducer做的事情。

设计state的结构

在redux应用中,所有的state都被保存在一个单一对象中。reducer就是一个函数,接收旧的state和action,返回新的state.
不要在reducer里做以下操作:

  1. 修改传入参数
  2. 执行有副作用的操作,如API请求和路由跳转
  3. 调用非纯函数,如Date.norw()或Math.random(),每个reducer只负责全局state中它负责的一部分。每个reducer的state
    参数都不同,分别对应它管理的那部分的state数据,combineReducers()所做的只是生成一个函数,这个函数来调用你的一系列
    reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成函数将所有reducer的结果合并成一个大的对象。

Store

action用来描述发生了什么,使用reducer根据action更新state,Store就是将它们联系到一起的对象,Store的职责:

  1. 维持应用程序的state
  2. 提供getState()获取state
  3. 提供dispatch(action)方法更新state
  4. 提供subscribe(listener)注册监听器
    Redux应用中只有唯一的一个store,通过redux的createStore(reducers, initialState)创建store
    createStore()的第二个参数用来设置初始状态

数据流

严格的单项数据流是redux架构的设计核心,Redux应用中数据的生命周期遵循下面4个步骤:

  1. 调用store.dispatch(action),你可以在任何地方调用store.dispatch(action),组件中,定时器中
  2. Redux store调用传入的reducer函数.Store会把连个参数传入reducer:当前的state树和action。
    reducer是一个纯函数。它仅仅用于计算下一个state。它应该是完全可以预测的:多次传入相同的输入
    必须产生相同的输出。它不应该做有副作用的操作,如API调用或路由跳转。这些应该在dispatch action前发生
  3. 根reducer应该用combineReducers()把多个reducer输出合并成单一的一个state树
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function todos(state=[], action) {
    //...
    return nextState;
    }

    function visibleTodoFilter(state='SHOW_ALL', action) {
    //...
    return nextState;
    }

    const reducers = combineReducers({
    todos,
    visibleTodoFilter
    });



当你触发action后,combineReducers返回的reducers会负责调用两个reducer,然后把两个结果集合并成一个state树:

1
2
3
4
return{
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}

  1. Redux Store保存了根reducer返回的完整的state树,这个新的树就是应用的下一个state!所有调用store.subscribe(listener)
    的监听器都将被调用;监听器里可以调用store.getState()获取当前的state
    现在,可以应用新的state来更新UI,在组建中的componentDidMount生命周期中调用this.setState()来更新

搭配React

Redux和React之间没有关系。Redux支持React、Angular、jQuery甚至纯javascript

  1. Redux的React绑定包含了容器组件和展示组件相分离的开发思想,明智的做法就是只在最顶层组件(路由操作)
    里使用Redux。其余内部组件仅仅是展示性的,所有数据都通过props传入
  2. 连接到Redux,通过react-redux提供的connect()方法将包装好的组件连接到redux
    **任何一个从connect()包装好的组件都可以得到一个dispatch方法作为组件的props,以及得到全局state中所需的内容**
    connect()的唯一参数是selector。此方法可以从Redux store中接收到全局state,然后返回组件中需要的props
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class App extends React.Component {
    render() {
    return(
    //...
    );
    }
    }

    //基于全局state,哪些是我们想要注入props的
    //https://github.com/reactjs/reselect这个待研究,用这个注入效果更好?
    function select(state) {
    return {
    xxx: state.xxx
    }
    }

    //包装component
    export default connect(select)(App);

异步Action

前面所述的是同步action,每当dispatch action时,state会理解被更新。那么Redux如何操作异步数据流?
Action

当调用异步API时,有两个非常关键的时刻:发起请求的时刻,和接收响应的时刻(也可能是超时)。


这两个时刻都可以更改应用的state;为此,你需要dispatch普通的同步action。一般情况下,每个api请求都至少需要dispatch三个不同的action:

  • 一个通知reducer请求开始的action
    对于这种action,reducer可能会切换一下state中的isFeching标记.以此来告诉UI来显示进度条.

  • 一个通知reducer请求成功结束的action
    对于这种action,reducer可能会把接收到的新数据合并到state中,并重置isFetching。UI则会隐藏进度条,并显示接收到的数据

  • 一个通知reducer请求失败的action
    对于这种action,reducer可能会重置isFetching.或者,有些reducer会保存这些失败信息,并在UI显示出来.


异步Action Creator

如何将不同的action creator和网络请求结合起来?使用Redux Thunk这个中间件,通过使用中间件,action creator除了返回action对象
外还乐意返回函数,当action creator返回函数时,这个函数会被Redux Thunk middleware执行。这个函数并不需要保持纯净;它可以带有
副作用,包括异步执行、API请求。这个函数还可以dispatch action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//thunk action creator
//使用方式和同步cation一样 dispatch(fetchPosts('xxx'))
export function fetchPosts(xxx) {
//Thunk middleware知道如可处理函数
//这里把dispatch方法通过参数的形式传给函数,以此来让它自己也能dispatch action

return function(dispatch) {

dispatch(action);

//执行api请求使用isomorphic-fetch库替代XMLHttpRequest
}

}



我们如何在dispatch机制中引入Redux Thunk middleware?使用appluMiddleware(),thunk的一个优点就是它的结果可以再次被dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//**index.js代码**
import thunkMiddleware from 'react-thunk';
import createLogger from 'redux-logger';
import { createStore, applyMiddleware } from 'redux';

const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware, //允许我们dispatch()函数
createLogger
)(createStore);

const reducers = combineReducers({
//拆分的单个reducer函数
})

const store = createStoreWithMiddleware(reducers);



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//**action.js代码**
export function fetchPosts(xxx) {
return (dispatch) => {
//...
dispatch(action)
}
}

export function fetchPostsIfNeeder(xxx) {
//可接收getState()方法
return (fetch, getState) => {

}

}



异步数据流

如果不使用middleware的话,Redux的store只支持同步数据流。而这也是createStore()所默认提供的创建方式,可以使用applyMiddleware()
来增强createStore(),使用redux-thunk这样支持异步的middleware都包装了store的dispatch()方法,以此让你dispatch一些除了action以外
的内容。当niddleware链中的最后一个middleware dispatch action 时,这个action必须是一个普通对象。

Middleware

middleware是指可以被嵌入在框架接收请求道产生相应过程之中的代码,它提供的是位于action被发起之后,到达reducer之前的扩展点
可以利用Redux middleware来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

使用Redux的一个益处就是它让state的变化过程变得可预知和透明。每当一个action发起后,新的state就会被计算保
存下来。state不能自身修改,只能由特定的action引起变化


减少样板代码

  • Action的type用常量,可以将所有type放在一个文件中,然后引入

  • Action Creators创建生成action的函数

  • 生产Action Creators写简单的action creator函数,尤其是数量巨大的时候,代码不易于维护,可以写一个用于生成action creator的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type };
argNames.forEach(arg, index) {
action[argNames[index]] = args[index];
}
return action;
}
}

const ADD_TODO = 'ADD_TODO';
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO';

export const addTodo = makeActionCreator(ADD_TODO, 'todo');
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'todo');
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id');

redux-actions可以帮助生成action creator,这个待研究

  • 异步Action Creators
    中间件让你在每个action对象分发出去之前,注入一个自定义的逻辑来解释你的action对象。异步action是中间件
    最常见用例。如果没有中间件,dispatch只能接收一个普通对象。因此我们必须在components里面进行AJAX调用:

actions.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
};
}

export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
};
}

export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
};
}

component.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';

class Posts extends Component {
loadData(userId) {
// 调用 React Redux `connect()` 注入 props :
let { dispatch, posts } = this.props;

if (posts[userId]) {
// 这里是被缓存的数据!啥也不做。
return;
}

// Reducer 可以通过设置 `isFetching` 反应这个 action
// 因此让我们显示一个 Spinner 控件。
dispatch(loadPostsRequest(userId));

// Reducer 可以通过填写 `users` 反应这些 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
);
}

componentDidMount() {
this.loadData(this.props.userId);
}

componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.loadData(nextProps.userId);
}
}

render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}

let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);

return <div>{posts}</div>;
}
}

export default connect(state => ({
posts: state.posts
}))(Posts);

redux-thunk中间件可以把action creators写成thunks,也就是返回函数的函数

使用react-redux修改上面的代码:

actions.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export function loadPosts(userId) {
// 用 thunk 中间件解释:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 这里是数据缓存!啥也不做。
return;
}

dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});

// 异步分发原味 action
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
respone
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}



component.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';

class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId));
}

componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(nextProps.userId));
}
}

render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}

let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);

return <div>{posts}</div>;
}
}

export default connect(state => ({
posts: state.posts
}))(Posts);



计算衍生数据

Reselect库可以创建可记忆的可组合的selector函数,Reselect selectors可以高效的计算
Redux store里的衍生数据

不使用reselect当state发生变化,组件更新时,会如果state tree非常大,会带来性能问题

  • 创建可记忆的Selector:
    只有在我们关注的state发生变化时才重新计算此state,而在其他非相关state的变化不会引起
    此state重新计算。
    Reselect提供的creatSelector函数创建可记忆的selector,createSelector接收一个input-selectors
    数组和一个转换函数作为参数。如果state tree的改变会引起input-selectors值变化,那么selector会调用
    转换函数,传入input-selectors作为参数,并返回结果,如果input-selectors的值和前一次一样,它将会直接
    返回前一次计算的数据,而不重新调用转换函数

selectors/TodoSelectors.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createSelector } from 'reselect';
import { VisibilityFilters } from './actions';

function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}

const visibilityFilterSelector = (state) => state.visibilityFilter;
const todosSelector = (state) => state.todos;

export const visibleTodosSelector = createSelector(
[visibilityFilterSelector, todosSelector],
(visibilityFilter, todos) => {
return {
visibleTodos: selectTodos(todos, visibilityFilter),
visibilityFilter
};
}
);



在上例中,visibilityFilterSelector 和 todosSelector 是 input-selector。因为他们并不转换数据,
所以被创建成普通的非记忆的 selector 函数。但是,visibleTodosSelector 是一个可记忆的 selector。
他接收 visibilityFilterSelector 和 todosSelector 为 input-selector,还有一个转换函数来计算过
滤的 todos 列表。

  • 组合 Selector
    可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面
    的 visibleTodosSelector 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。    
    
1
2
3
4
5
6
7
8
const keywordSelector = (state) => state.keyword

const keywordFilterSelector = createSelector(
[ visibleTodosSelector, keywordSelector ],
(visibleTodos, keyword) => visibleTodos.filter(
todo => todo.indexOf(keyword) > -1
)
)


  • 连接 Selector 和 Redux Store
    在react-redux中,使用 connect 来连接可记忆的 selector 和 Redux store

containers/App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addTodo, completeTodo, setVisibilityFilter } from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'
import { visibleTodosSelector } from '../selectors/todoSelectors'

class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter } = this.props
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
)
}
}

App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
}

// 把 selector 传递给连接的组件
export default connect(visibleTodosSelector)(App)

bindActionCreators

bindActionCreators(actionCreators, dispatch)

使用场景

需要把action creator往下传到一个组件上,却不想让这个组建觉察到Redux的存在,而且不希望把Redux store
或dispatch传给它



参数

  1. actionCreators(Fuction Or Object):一个action creator,或者键值是action creators的对象

  2. dispatch(Function):一个dispatch函数,由store实例提供

返回值

(Function or Object):一个与原对象类似的对象,只不过这个对象中的每个函数值都可以直接dispatch
action。如果传入的是一个函数,返回的也是一个函数。


示例

TodoActionCreationCreator.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}



SomeComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as TodoActionCreators from './TodoActionCreators';

class TodoListContainer extends React.Component {

componentDidMount() {
//由react-redux注入
let { dispatch } = this.props;

let action = TodoActionCreators.addTodo('Use Redux');
dispatch(action);
}

render() {

let { todos, dispatch } = this.props;

//应用bindActionCreator
let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch);

return (
<TodoList todos={todos}
{...boundActionCreators}
/>
);

//一种可以替换bindActionCreators的做法是直接把dispatch函数和action creators当作props传递给组件
//return <TodoList todos={todos} dispatch={dispatch} />
}
}

export default connect(state => ({todos: state.todos}))(TodoListContainer);