Why We Need Eslint?
在团队配合中,有的人是Mac开发,有的人是Windows来发,使用Eslint来强制开发人员统一代码
格式,比如常见的tab
以及空格,以及windows和Mac的回车标识,也可强制开发人员删除无用的
垃圾代码,比如alert(1212), console.log(111)
等等,或者条件语句中使用if(true){}
,
总之它就是用来检测开发人员代码书写规范的一个集成工具。so beautiful!
vue是一个用来构建交互式web界面的库。它的核心集中于视图层,它很容易与其他库或现有的项目集成。
另一方面,Vue完全有能力服务员复杂的单页面应用。但是Vue和其它的框架有什么不同呢?
React和Vue有很多相似之处:
- 利用虚拟DOM
- 提供了reactive(反应性)和可组合的视图组件
在代码核心库中,保持对相关库管理的routing,全局state的关注。作用域也很相似。我们想保证的不仅
有技术上的准确也要确保技术的平衡,我们找出React哪里优于Vue,比如,React庞大的生态圈以及
自定义渲染器的丰富性。
Vue胜过React,当渲染UI时,DOM的操作通常是非常昂贵的,没有库可以使那些原生的操作更快,我们能做的
最好的就是:
- 尽量减少必要的DOM变化的数量,React和Vue都使用了虚拟DOM来解决这个问题。
- 尽可能在顶层的DOM操作上添加较小的开销(纯js计算),这里是React和Vue的区别。
javascript开销和计算必要的DOM操作机制有直接关系,Vue和React都利用虚拟DOM来达到它,但是Vue的实现更
轻量级,因此比React的开销更少。
React和Vue都提供了功能组件,他们是stateless,instanceless的因此需要的开销很少。当它们用于性能关键
的场景中,Vue更快,为了证明这个,我们渲染1万个item100次,强烈建议自己实践,因为它和软件、浏览器相关。
相关比较如下:
1 | Vue React |
在React中,当一个组件的state改变了,它会触发整个组件的sub-tree的重新渲染,为了避免child组件不必要
的渲染,你需要在该组件中使用shouldComponentUpdate
并定义不变的结构,而在Vue中,在渲染过程中,会自
动的追踪组件的依赖项,所以系统会精确的知道哪个组件需要正真的重新渲染。这就意味着未优化的Vue比未优化的
React要快。由于Vue的加强了渲染性能,即使React进行了全部的优化操作,通常也比Vue慢。
虽然在生产环境中的性能指标是非常重要的,因为这和用户体验相关联。但是在开发时的性能仍然重要,因为这与
我们开发者相关联.
Vue和React在大多数程序中开发可以保持足够快速。但是,在高帧速率的可视化或动画时,Vue每秒处理10帧,而
React每秒只有1帧。这是因为在开发环境中React需要检查大量的不变量,这是用来提示一些重要的错误或警告用的。
当然在Vue中也是同意这是很重要的,但是当执行这些检查时,我们对性能保持密切关注。
在React中,所有的东西是javascript,虽然听起来很简单且高雅但是当你向下挖掘时,在javascript中写HTML和CSS,
当解决问题时,会很痛苦。而在Vue中,我们信奉web的技术并将它们,将css写在html顶部。####### JSX vs Templates
在React中,所有的组件在JSX中来表示他们的UI,下面是个例子:
1 | render () { |
JSX有以下优点:
- 使用编程语言(js)构建你的视图。
- 在某些方式下,对JSX的工具支持比vue的模板更先进。
在Vue中,我们有render functions
甚至还有支持jsx
。
在Vue中,有渲染器函数并且支持
jsx
,默认情况下,我们提供模板作为选择方案:
1 | <template> |
它的优势:
- 模板中有少量的实现并且代码风格优雅
- 模板一直是称述性质的
- 任何有效的HTML在模板中也是有效的
- 读起来很像英语
- 不需要再高版本的javascript中增加可读性。
另外一个好处就是你可以使用预处理器处理HTML-compliant
模板,比如Pug书写你的Vue模板:
1 | div.list-container |
Component-Scoped CSS(组件范围的CSS)
除非你的组件分布在多个文件中(比如CSS Modules),范围性的CSS在React中通常是在js中。而在
Vue中,你完全可以在单个文件中访问到CSS:
1 | <style scoped> |
scoped
属性自动的对组件范围内的元素通过添加一个唯一的属性来编译。
React社区在状态管理上引入了
flux/redux
,状态管理方式可以在Vue很容易的集成进来。但是尽管
如此,React的生态圈还是要比Vue丰富。Vue提供了一个十分简单生成Vue项目的命令行工具。
React的学习曲线比较陡峭,在你学习之前你需要了解JSX以及ES2015.Vue中可以简单的通过在html引入vue库
来使用vue,React也行啊!!!
1 | <script src="https://unpkg.com/vue/dist/vue.js"></script> |
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生态圈我觉得还是不错的。
Phabricator是一个开发软件工具的集成,包含任务管理,代码review,管理git,svn等等,持续集成的构建,
内部通道的讨论等等。
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 | //选择你要安装的文件夹,并在该文件夹下执行以下命令 |
APC是建议安装的,所以没有深究。
- 配置web容器(Apache, nginx)
- 在浏览器访问phabricator
Apache的httpd.conf
1 | <VirtualHost *> |
如果Apache的配置文件目录不是Phabricator的目录,那么你需要添加
<Directory />
块,这个块
标示依赖于你的Apache的版本,通过运行httpd -v
得到当前运行Apache的版本,如果版本号小于2.4,
1 | <Directory "/path/to/phabricator/webroot"> |
如果大于2.4:
1 | <Directory "/path/to/phabricator/webroot"> |
配置完成之后,请重启Apache,并继续接下来的配置.
nginx.conf:
1 | server { |
配置完成之后重启nginx,并继续接下来的配置
当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
,一些选项
(如设置密码,更改账号标记)只能在命令行中可见。你可以使用此命令来使一个用户成为管理员(比如你意外的移除了你管理员的标记)
或者创建一个管理员账号。
为什么需要内部实例呢?
React的主要特性是你可以重新渲染任何东西,并且你不会重复创建DOM以及重置state.
1 | ReactDOM.render(<App />, rootEl); |
当组件需要更新时,如何存储必要的信息呢?比如说
publicInstances
,DOM元素与组件之间的关系。
堆栈协调器代码库通过在类中加了一个mount()
函数解决这个问题,这种方式有点缺点,正在改进。
把mountHost()
和mountComposite()
分离出来的替代方案是创建了两个类DOMComponent
和CompositeComponent
这两个类都有一个接收element
的构造函数,当然还有一个mount()
方法返回已镶嵌的节点,用一个工厂方法实例化正确的
class来替代顶级mount()
方法。
1 | function instantiateComponent(element) { |
CompositeComponent(复杂组件)的实现:
1 | class CompositeComponent { |
DOMComponent
类的实现:
1 | class DOMComponent { |
结果是,每一个内部实例都不论是复合型的还是平台特有的,都指向他的child的内部实例,比如:
<App>
组件渲染了一个<Button>
,<Button>
渲染了一个<div>
,内部实例树将是下面的样子:
1 | [object CompositeComponent] { |
在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依赖。
通常情况下我们抵制在用户级就可以实现的行为,我们不想用无用的库膨胀你的应用,但是也有些例外,比如:如果React对本地state和生命周期
挂钩不提供支持,用户会创建通用抽象给他们,当有多个抽象竞争时React不能充分利用他们的属性执行,它不得不以最低的标准工作。这也是为什么
我们有时会给React添加一些特性,如果我们注意到许多组件不兼容或低效的执行某些功能,我们可能更愿意考到React。我们不会轻易的干这件事。
当我们做它时,因为我们有信心提高抽象层次使整个生态系统受益。state,生命周期挂钩跨浏览器事件正常化是个很好的例子。我们经常与社区讨论
改善建议,你可以在big picture中找到这些讨论
React是实用主义的,它是由写Facebook的产品需求而驱动的,虽然它受一些范例的影响,但也不是完全主流。比如,函数式编程,对不同技能的开发
保持可访问是项目的一个明确的目标。如果我们想反对一个我们不喜欢的模式,在反对它之前我们考虑所有存在的场景。如果一些模式对于构建app和有用
但是很难表达,我们会为它提供必要的API。
我们重视API的稳定性,Facebook有两万个组件在使用React,这也是我们为什么通常不愿意改变公共API或行为。但是”nothing changes”稳定性的意义有点被
高估了,它迅速停滞了。相反,我们更喜欢”在生产环境大量使用并且一些东西变化时有一个明确的迁移路径”的稳定意义。当我们反对一种形式时,我们在Facebook研究
它的内部使用并添加反对警告。这让我们评估改变的重大影响。有时,我们发现它太简单的话我们会退出,并且我们需要战略性的思考关于这次改动代码库的要点是否准备好。
如果我们自行这次的更改不是太具有破坏性并且此次战略性转移对用户场景是可实施的,我们会在社区中释放反对警告。我们会和外部使用React的用户密切联系并且我们会
监视流行的开源项目并指引他们修理这些警告。由于React代码库规模庞大,内部迁移成功是一个很好的指标并且其余公司也不会有问题。
在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 | console.log(<App />); |
协调器会检查
<App/>
是一个class还是一个函数,如果App是一个函数,协调器会调用App(props)
来获取要渲染的元素。
如果App是一个类,协调器会用new App(props)
实例化这个App,调用componentWillMount()
生命周期方法,然后调用render()
方法
来获取渲染的元素.
无论哪种方式,协调器会知道App要渲染的元素。这个过程是递归的,App有可能渲染到<Greeting />
, Greeting可能渲染到<Button / >
等等,
协调器会递归的通过用户定义的组件向下挖去,知道它知道每一个组件会渲染成什么。你可以假设这个过程的伪代码:
1 | function isClass(type) { |
注意:这仅仅是一段伪代码,它不是真正的实现,它会导致栈溢出,因为我们没有讨论终止递归的条件。
我们回顾一下上面例子的几个关键概念:
- React元素是一些简单的对象来代表组件类型和属性(比如
<App/>
)。- 用户自定义的组件(比如
<App />
)可以是一个类也可以是一个函数但是他们最终都会被渲染为元素。- “mounting”是一个递归的过程它用来创建DOM以及Native树,给React的顶级元素(比如
<App>
).
如果我们最终不向屏幕渲染任何东西这个过程将毫无作用,除了用户自定义的组件,React元素也可以表示特定
平台的组件.比如,Button
可能在render方法中会返回一个<div />
。如果一个元素的类型是字符串,这样处理
主机元素:
1 | console.log(<div />); |
没有用户定义与主机元素相联系的代码。
当协调器遇到一个主机元素时,协调器会让render小心的镶嵌它,比如,ReactDOM会创建一个DOM节点,如果这个主机元素
有子节点,协调器会按照上面的算法递归的镶嵌,子元素是否是主机元素都没有关系(<div><hr></div>
),复杂的组件(<div><Button/></div>
)
子组件生产出来的DOM节点将会追加到父DOM节点,递归的方式,组装为完整的DOM结构。
注意:协调器本身不依赖于DOM,完整的镶嵌过程依赖于render(有时候,在源码中称之为镶嵌快照),最终结果可以是DOM node(React DOM)
一个字符串(ReactDOM Server)或者是原声的视图(React Native).
如果我们想扩展去处理主机元素的话,可以这样:
1 | function isClass(type) { |
虽然这也能工作但里协调器的真正实现还有很远的距离,主要是漏掉了对更新的支持。
React的关键特性是你可以重复渲染任何东西,并且他不会创新创建DOM或重置state:
1 | ReactDOM.render(<App />, rootEl); |
然而,我们上面的实现知识装载初始状态的树,不能对他执行更新操作,因为没有存储一些必要的信息,
如publicInstance,或DOM节点对应于哪些组件。stack reconciler代码库通过将mount()
函数和一个方法
放在一个类上。这种方式有一些缺陷,我们现在正在一相反的方向解决这个问题,重写reconciler正在进行中
不管怎样,现在它的工作方式就是这样的。
将mountHost
和mountComposite
函数拆分出来的替代方案就是,我们将创建两个类:DOMComponent
和CompositeComponent
两个类都有一个接收elements的constructor
,还有一个返回镶嵌了dom节点的mount()
方法,我们将会使用工厂模式代替类中
顶级的mount()函数。
1 | function instantiateComponent(element) { |
首先,让我们思考
CompositeComponent
的实现方式:
1 | class CompositeComponent { |
重构
mountHost()
之后的主要区别是,我们可以保持this.node
以及this.renderedChildren
于内部的组件实例相关联。
我们也可以在未来将它们用于非破坏性的更新。因此,每一个内部实例,复杂的以及主机的,现在指向其孩子内部实例。
为了帮助构思它,如果一个<App>
组件函数渲染了一个<Button>
类组件,并且Button
类渲染了一个<div>
,内部实例树
长得是这个样子的:
1 | [object CompositeComponent] { |
在DOM中你仅仅能看见
div
,但是内部实例树既包含复杂的内部实例也包含主机的内部实例。
复杂的内部实例需要被存储:
- 当前的元素
- 如果元素类型是个类,public实例
- 单个的内部实例的渲染结果,它既可以是
DOMComponent
也可以是CompositeComponent
主机内部实例也需要被存储:- 当前的元素
- DOM节点
- 所有的子内部实例,它们既可以是
DOMComponent
也可以是CompositeComponent
.
如果你努力设想在复杂的程序中一个内部实例是什么样的结构,(React DevTools)[https://github.com/facebook/react-devtools]
可以给你一个近似的结果,主机实例是灰色的,复杂实例是紫色的。
要完成这个重构,我们要引入一个函数镶嵌完整的树到节点容器。比如ReactDOM.render()
,它返回一个公共实例:
1 | function mountTree(element, containerNode) { |
现在我们有内部实例维持他们的字元素与DOM节点,我们可以实现unmounting,d对于一个复杂的组件,unmounting调用生命周期的钩子
然后递归:
1 | class CompositeComponent { |
对于
DOMComponent
,unmounting告诉每一个子元素卸载:
1 | class DOMComponent { |
在实践中,卸载DOM组件也会移除事件监听器并且清楚缓存,在这里跳过这些细节。
现在我们添加一个新的顶级函数调用unmountTree(containerNode)
,这和ReactDOM.unmountComponentAtNode()
很相似:
1 | function unmountTree(containerNode) { |
为了使它工作,我们需要从DOM节点中获取内部根实例,我们将会修改
mountTree()
添加_internalInstance
属性到根DOM节点。
我们也会告诉mountTree()
销毁任何存在的树,所以它可以被调用多次:
1 | function mountTree(element, containerNode) { |
现在,运行
unmountTree()
或者运行mountTree()
多次,移除旧的树然后运行组件的生命周期方法componentWillUnmount
.
在上一节,我们实现了
unmounting
,但是如果每一个属性的变化都会改变unmounted和mounted整个树,这对于React来说没有用,
协调器的目的是尽可能的保持和重用现有实例:
1 | var rootEl = document.getElementById('root'); |
我们将会扩展我们的内部实例建立一个或多个方法,除了
mount()
和unmount()
,DOMComponent
和CompositeComponent
将会
实现一个新的receive(nextElement)
方法:
1 | class CompositeComponent { |
它的职责是根据下一个元素提供的说明做任何有必要的组建更新,这部分通常称之为’虚拟dom差异比较’,尽管真正发生的是内部树的递归然后
让每个内部实例接收到更新。
当一个复杂组件接收到一个新的元素时,我们运行
componentWillUpdate()
生命周期钩子。然后我们根据新的属性重新渲染组件,然后得到
下一个渲染的元素:
1 | class CompositeComponent { |
接下来,我们可以看一下渲染的元素的类型,如果在最后一次的渲染中
type
没有发生变化,下面的
组件可以就地更新。例如:如果第一次返回<Button color='red' />
,第二次返回<Button color='blue'>
我们只需要告诉相关联的内部实例接收下一个元素:
1 | //... |
但是,如果下一次渲染的组件类型和上一次渲染的组件类型不同,我们不会修改内部实例,一个
<button>
不会变为
一个<input>
:
1 | // ... |
综上所述,当一个复杂的组件接收到一个新的元素时,要么委托去修改内部实例,要么卸载它并在原来
的位置新建一个。
这里有另外一种情况,一个组件将会重新装载而不是接收一个元素,当元素的key
改变时,我们不讨论key
的处理,因为它会增加教程的复杂性。注意:我们需要在内部实例添加一个叫getHostNode
的方法,
它可能位于特定平台的节点并取代它的更新过程。它的实现简单明了:
1 | class CompositeComponent { |
主机组件实现,比如
DOMComponent
已不同的方式更新,当它们接收到新的元素时,他们需要更新基于平台特有的视图,
在React DOM的情景中,这意味着要更新DOM属性:
1 | class DOMComponent { |
接着,主机组件需要更新它们的children,和复杂组件不同的是,他们可能包含不止一个child,在下面这个简单的例子中,
我们使用内部实例数组并遍历它,是更新还是替换内部实例依赖于接收到的type
是否与他们先前的type
一致,真正的reconciler
也会也需要元素的key
来进行插入还是删除,但是我们省略相关的实现。
1 | // ... |
最后一步,我们执行DOM操作,真实的reconciler是很复杂的因为它还得处理移动:
1 | // Process the operation queue. |
主机组件更新也是这样。
现在
CompositeComponent
和DOMComponent
实现了receive(nextElement)
方法,当元素类型同最后一次相同时,
我们可以改变顶层mountTree()
来使用它:
1 | function mountTree(element, containerNode) { |
现在调用
mountTree()
两次非破坏性的
1 | var rootEl = document.getElementById('root'); |
这是React内部工作的基本原理。
这篇文章和真实的代码库进行了一个简单的比较,这儿有几个我们没有解决的问题:
- 组件可以返回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恢复输入选择后的更新。
ReactMount
is where the code like mountTree() and unmountTree() from this tutorial lives,它负责装载和卸载顶级组件ReactNativeMount
是ReactNative的模拟。- 本教程中
ReactDOMComponent
和DOMComponent
是同等的,它实现了ReactDOM渲染器的主机组件类。ReactNativeBaseComponent
是
React Native的模拟。ReactCompositeComponent
和CompositeComponent
是同等的,它处理调用用户自定义组建以及维持state。instantiateReactComponent
包含了一个开关用来选择一个正确的内部实例类用来构造一个元素。在此教程中和instantiateComponent()
是同等的。ReactReconciler
是mountComponent
和receiveComponent
以及unmountComponent
方法的包装。它在内部实例上调用底层的实现。当然它也
包含所有内部实例实现的共享代码。ReactChildReconciler
实现了通过元素的key
,mounting
,updating
,unmounting
children的逻辑。ReactMultiChild
实现了渲染器对操作队列的处理。child的插入,移动,删除。mount()
,receive()
,unmount
因为一些遗留原因在React代码库中,实际上被称为mountComponent()
,receiveComponent
,unmountComponent()
- 内部实例的属性以下划线开头,比如,_currentElement,在代码库中自始至终它们被认为是只读的。
stack reconciler具有的局限性,如同步,不能中断工作或chunks的拆分。在新的
Fiber reconciler
中有一个完全不同的结构,在未来我们打算使用
它代替现有的reconciler,但是目前还很遥远。
React提供了一个声明式的API,所以你不用担心在每次更新时到底发生了什么变化,这使得写代码变得简单了许多
但是React是如何实现的对我们来说是透明的不明显的。这篇文章阐述了React的’diffing’算法所做的选择,因此
组件的更新是可预见的并且是高性能的。
当你使用React时,你可以认为在一个单一的时间点render()函数创建了一个React元素的树,当下一个props或state
更新时,render()函数将会得到一个新的不同的React元素的树,React需要解决如何高效的更新UI以匹配最新的树。
一些通用方案有性能问题,时间复杂度会为(O(n3))如果在React使用这些通用方案的话,100个元素将会进行一百万次
的比较,话费太昂贵了,替代方案是,React实现了一种基于以下两种假设的时间复杂度为O(n)的算法:
- 两种不同类型的元素将会产出不同的树。
- 开发人员可以暗示在render时维持一个稳定的key属性。
在实践中,这些假设在实际使用的情况下都有效。
当比较两个树时,React首先会比较两者的根元素,该行为取决于根元素的类型。
不同类型的元素
每当根元素有不同类型时,React将会销毁该树并且重新构建新的树,从<a>
到<img>
,<Article>
到Coment
,
从<Button>
到<div>
都会进行全新的重新构建过程。当拆卸一个树时,旧的DOM节点会被销毁,组件实例收到componentWillUnmount()
当构建新树的时候,新的DOM节点将会被插入到DOM中,组件实例会收到componentWillMount()
接着会componentDidMount()
任何和旧树相关的状态都会丢失.任何在root节点之下的组件以及他们的state都会被销毁,比如:
1 | <div> |
这将会销毁
<Counter>
并重新镶嵌一个新的<Counter>
.
当比较两个相同类型的DOM元素时,React会查看两个组件的属性,保持相同的DOM节点,只会更新改变的属性,例如:
1 | <div className="before" title="stuff" /> |
当比较这两个DOM元素时,React知道只修改基础DOM节点的类名。当更新
style
时,React同样知道只改变的变化了的属性
例如:
1 | <div style={{color: 'red', fontWeight: 'bold'}} /> |
当在两个元素之间转换时,React知道仅修改
color
样式。不会去修改fontWeight
样式。处理完DOM节点之后,React会递归操作
子节点。
当一个组件更新时,该实例保持不变,所以在render时state是被维护的,React修改基础组件实例的属性,以匹配新的元素,然后调用
基础组件实例的componentWillReceiveProps()
和componentWillUpdate()
。
接下来,render()
方法被调用,然后diff算法(diff algorithm)递归上次的结果和最新的结果.
默认情况下,当递归一个DOM节点的子节点时,React在同一时间仅仅是迭代子组件们的列表并且每当有差异的时候会生成一个突变。
例如:当在子组件的最后添加一个新的元素时,这两个树的之间的转变,工作的很好。
1 | <ul> |
React将匹配两个
<li>first</li>
树,匹配<li>second</li>
树,然后去插入<li>third</li>
树。
如果你天真的实现,当插入在最开始的位置,那么会有很糟糕的性能问题。比如,以下的这两课树之间的转换很差:
1 | <ul> |
React将会改变每一个子节点而不是实现它,它可以保持
<li>Duke</li>
和<li>Villanova</li>
的子节点的完整性,
这种低效性是一个问题.
为了解决这一个问题,React支持了一个key的属性,当子组件有key属性时,React使用这个key,来匹配原始树的子节点与
随后的树中的子节点,通过添加key使树的转换更加高效。
1 | <ul> |
现在React知道持有’2014’的key是新的节点,而持有
2015
和2016
的节点仅仅移动就可以了,在实践中,发现一个key并不是
很难,你将要陈列的元素也许已经有一个唯一的id了,所以这个key值来源于你的data:
1 | <li key={item.id}>{item.name}</li> |
当不是这种场景时,你可以添加一个key或生成一个key,这个key只需要和他相邻的节点不同就可以了,不需要全局唯一。
作为最后的手段,你可以传递数组的下标作为key,如果列表不需要排序这工作的很好,但是重新排序会很慢。
记住reconciliation算法的实现细节很重要,React可以在每一个动作下重新render整个App,最终结果相同,我们会定期的改善
使常见的场景更加快速。
在最近的实现中,你可以表达一个事实,一个子组件已经从它的兄弟节点中移除了,但是你不能告诉它被移到哪里了。算法会重新
render整个子组件们。
因为React依赖于启发式算法,如果不能满足它背后的假设,性能会受影响。
- 该算法不会视图去匹配不同类型节点的子树,如果你发现两个类型的组件的输出很相似,你可以让他们变为相同的类型,在实践中
我们还没发现这会成为一个问题。- keys应该是稳定的,可预测的,以及唯一的,不稳定的key(比如:Math.random())将会引起,组件实例和DOM节点不必要的重新创建,
从而导致性能退化和状态丢失.
这一节主要是给我们一个React代码库的概述,他的一些规范以及实现,如果你想对React有所贡献的话
这个指南将会使你更加舒服的更改。我们不一定推荐在React应用程序的任何这些惯例,有些惯例由于一些
历时原因而存在但是后期也许会更改.
在Facebook,内部的我们使用的是一个名叫’Haste’的自定义模块,他和commonjs和类似并且也使用
require()
但是有一些重要的不同而正是这些使外部的贡献者很困惑。在Commonjs中,当你导入一个模块时,你需要明确指定
它的相对路径:
1 | // Importing from the same folder: |
然而,在Haste中所有的文件名都是全局唯一的,在React代码库中,你可以按照文件名导入其他的任何模块。
1 var setInnerHTML = require('setInnerHTML');Haste最初是用来开发大型app的比如像Facebook,很容易将文件移动到不同的目录并且无需担心他们的相对路径
在任何编辑器的模糊查询会让你得到正确的位置,而这归功于全局唯一的文件名.
React是从Facebook的代码库中提取出来的,所以使用Haste是历时原因,在将来,我们有可能会移植Commonjs或ES6
的模块化方式同社区更好的对齐,但是这需要Facebook内部基础架构做出很大改变,所以也不是一朝一夕就能完成的.
如果你记得一下这些规则,Haste会对你更有意义:
- 在React原生代码库中的所有文件名都是全局唯一的,这也是为什么有些文件名繁长啰嗦的原因。
- 当你添加了一个新文件时,确认你包含了许可证的标头,你可以从别的地方复制一份,一个许可证标头的格式一般是:
1 | /** |
记得更改@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仓库之后,你会发现有几个顶级文件夹在里面:
src
是React的源码,如果你要修改相关的代码,src
文件夹将是你花费时间最长的.docs
是React文档网站,当你修改了API时,请确认修改了相应的markdown文件examples
包含了一些生成不同配置的React例子packages
包含了元数据用来对应React仓库中的所有包(比如package.json),尽管如此,他们的源码还是位于src
文件夹中。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/client
和src/renders/dom/server
共享一段代码,那么这段代码应该位于src/renders/shared
.
按照同样的逻辑,如果src/renders/dom/client
和src/renders/native
共享一个工具类,那么此工具类应该位于src/renders/shared
.
这个规范虽然不是强制的,但是当我们遇到pull request
时会检查.
React代码库用
warning
模块来展示警告:
1 | var warning = require('warning'); |
当
warning
条件为false时,警告将会显示
这种思考方式是条件响应的是正常情况而不是异常情况。
这是一种避免垃圾邮件以及控制台重复输出的好主意。
1 | var warning = require('warning'); |
警告仅会在开发环境下可用,在正是环境下,他们会被完全削去,如果你需要禁止一些代码路径的执行
使用invariant
模块代替.
1 | var invariant = require('invariant'); |
当
invariant
为false的时候,将引发
“invariant”是这种情况一直为true的一种说明方式,你可以把它当做断言。
保持开发和正是环境行为一直是很重要的,所以invariant
可以在正式及开发环境引发,警告信息会自动
的被替换为错误码以避免字节数大小的影响。
你可以在代码库使用
__DEV__
伪全局变量,守卫只针对开发环境的代码块。
在编译阶段中,在CommonJS构建时,它将会被转变为process.env.NODE_ENV !== 'production'
。
在独立构建时,在未压缩的构建时它变为true,在压缩构建的时候它将会被剔除。
1 | if (__DEV__) { |
一些内部的以及公共的方法以JSDoc形式的注释注释:
1 | /** |
我们试图让存在的注释更新但我们不是强制的,我们没有在新写的代码中使用JSDoc,替代方案是
我们使用Flow强制类型.
我们最近引入Flow来检查代码库,在许可证标头用
@flow
标记的文件
将会执行类型检查.
我们接受给已存在代码添加@flow
的pull请求,Flow注释长这样:
1 | ReactRef.detachRefs = function( |
只要有可能,新提交的代码应该使用Flow注解,你可以在本地运行
npm run flow
来检查你的代码.
React原来是用ES5编写的,我们已经用babel支持了ES6的特性。包含classes,
但是,许多React代码还是用ES5写的。
尤其是,你可能经常看到以下的形式:
1 | // Constructor |
这段代码的
Mixin
和React的mixins
特性并无关系,这只是一种分组在一个对象的方法的方式,这些方法有可能
在后期获取并附加到别的class上,我们使用了这种方式尽管我们尝试在新代码中避免使用。同等的ES6代码长得是
这个样子的:
1 | class ReactDOMComponent { |
有时候我们会转换ES5代码为ES6代码,然而这对我们来说不是非常重要,因为有一个正在进行的努力,React协调器的实现带有
少量的面向对象的方法,我们完全不需要使用class。
React在有些模块中使用了动态注入,虽然它易于理解,但是不幸的是,它阻碍了我对代码的理解,它存在的主要原因是React起初
只支持DOM作为target,React Native是已React的分支开始的,我们不得不添加动态注入让React Native覆盖一些行为.你有可能
会看见一些模块声明动态依赖是以下面的这种方式:
1 | // Dynamically injected |
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的核心包含了所有顶级的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。
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仓库中
甚至完全不同的render比如React DOM和React Native需要共享相同的逻辑,尤其是reconciliation算法应尽可能相似,声明的渲染,
自定义组建,状态,生命周期方法,以及refs始终跨平台工作。
为了解决这一问题,不同的renders共享一些代码。我们称这些代码为reconciler
(协调器),当一个更新(setUpdate())被安排时,
reconciler(协调器)调用了组建中的render(),mounts,updates,或者unmount.
reconciler(协调器)没有被单独的分离出来,因为它们目前没有公共的API,它们会被renders使用,比如React DOM,React Native。
‘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’是否不同,如果type
和key
有改变,他会委派
去更新已存在的内部实例,另外,它会卸载老的子组件,并镶嵌新的,这个会在Reconciliation algorithm(协调器算法)中描述。
Recursion(递归)
在一次更新中,stack reconciler会一直深入复杂的组件,运行他们的render()方法,并决定是否更新以及替换他们的唯一的组件。通过平台组件
(div View)执行平台代码,Host组件可能会有很多子组件,通常会递归处理。stack reconciler经常处理同步的单个处理组件树,当个别的树处理完成了
stack reconciler不会停止,所以当更新很深以及CPU有限时它不是最优的。
‘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
React实现了一个综合型的事件系统,在ReactDOM和React Native都可工作,它的代码位于
src/renders/shared/shared/event
.
每个React的附件组件在npm都以react-addons-的前缀的包单独的分离出来。它们的代码位于
src/addons
.
另外,我们提供了一个单独的叫react-with-addons.js
的输出文件,它包含了React代码以及所有的addons暴露出的对象。
在单页应用中,服务器的相应,UI状态,缓存数据,被选中的标签,是否加载动画效果等等这些都可以理解为state,当应用变得庞
大复杂时传统的javascript代码处理这些状态 ,只会让维护变得更加困难,而用redux的原因就是将应用程序中的state的变化变得
可预测
state是只读的,唯一改变state的方法就是触发action,action就是一个描述你要干什么的js对象
1 | var actionName = { |
使用reducer修改state,reducer是一个纯函数,它接收的参数为先前的state和将要执行的action,并返回新的state
Action是把数据从应用中传到store的有效载荷,它是store数据的唯一来源,一般通过store.dispatch( {type: 'xx', desc: 'xx'} )
将action传到store
ActionCreator就是生成action对象的函数,返回一个action对象1
2
3
4
5
6function 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()待定
Action只是描述了事件发生了而已,但是并没有指明应用如何更新state,而更新state正是reducer做的事情。
在redux应用中,所有的state都被保存在一个单一对象中。reducer就是一个函数,接收旧的state和action,返回新的state.
不要在reducer里做以下操作:
action用来描述发生了什么,使用reducer根据action更新state,Store就是将它们联系到一起的对象,Store的职责:
严格的单项数据流是redux架构的设计核心,Redux应用中数据的生命周期遵循下面4个步骤:
1 | function todos(state=[], action) { |
当你触发action后,combineReducers返回的reducers会负责调用两个reducer,然后把两个结果集合并成一个state树:1
2
3
4return{
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
store.getState()
获取当前的statethis.setState()
来更新 Redux和React之间没有关系。Redux支持React、Angular、jQuery甚至纯javascript
**任何一个从connect()包装好的组件都可以得到一个dispatch方法作为组件的props,以及得到全局state中所需的内容**
connect()的唯一参数是selector。此方法可以从Redux store中接收到全局state,然后返回组件中需要的props
1 | class App extends React.Component { |
前面所述的是同步action,每当dispatch action时,state会理解被更新。那么Redux如何操作异步数据流?
当调用异步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和网络请求结合起来?使用Redux Thunk这个中间件,通过使用中间件,action creator除了返回action对象
外还乐意返回函数,当action creator返回函数时,这个函数会被Redux Thunk middleware执行。这个函数并不需要保持纯净;它可以带有
副作用,包括异步执行、API请求。这个函数还可以dispatch action
1 | //thunk action creator |
我们如何在dispatch机制中引入Redux Thunk middleware?使用appluMiddleware(),thunk的一个优点就是它的结果可以再次被dispatch
1 | //**index.js代码** |
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是指可以被嵌入在框架接收请求道产生相应过程之中的代码,它提供的是位于action被发起之后,到达reducer之前的扩展点
可以利用Redux middleware来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
使用Redux的一个益处就是它让state的变化过程变得可预知和透明。每当一个action发起后,新的state就会被计算保
存下来。state不能自身修改,只能由特定的action引起变化
Action的type用常量,可以将所有type放在一个文件中,然后引入
Action Creators创建生成action的函数
生产Action Creators写简单的action creator函数,尤其是数量巨大的时候,代码不易于维护,可以写一个用于生成action creator的函数:
1 | function makeActionCreator(type, ...argNames) { |
redux-actions可以帮助生成action creator,这个待研究
actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22export 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
51import { 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
29export 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
31import { 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非常大,会带来性能问题
creatSelector
函数创建可记忆的selector,createSelector
接收一个input-selectorsselectors/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
26import { 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 的 input-selector。下面
的 visibleTodosSelector 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。
1 | const keywordSelector = (state) => state.keyword |
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
47import 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)
使用场景
需要把action creator往下传到一个组件上,却不想让这个组建觉察到Redux的存在,而且不希望把Redux store
或dispatch传给它
参数
actionCreators
(Fuction Or Object):一个action creator,或者键值是action creators的对象
dispatch
(Function):一个dispatch函数,由store
实例提供
返回值
(Function or Object):一个与原对象类似的对象,只不过这个对象中的每个函数值都可以直接dispatch
action。如果传入的是一个函数,返回的也是一个函数。
示例
TodoActionCreationCreator.js
1 | export function addTodo(text) { |
SomeComponent.js
1 | import React from 'react'; |