在这一节中,我们将给Feact增加setState方法,这个方法非常有趣,端起饮料好好享受吧。
给Feact添加state
state和props非常相似,他们都是组件在渲染的时候,流动在内部的数据。不同的是props来自于外部,state是内部的。到目前为止,Feact只支持props,所以,在我们搞出setState之前,我们得先给这个小框架增加个state这个概念。
getInitialState
当我们挂载一个新组件的时候,我们需要给这个组件增加一个初始state,这个时候,我们需要调用getInitialState这个生命周期函数。该生命周期函数需要在实例化的时候被调用,所以,我们需要在Feact.createClass构造函数里增加钩子。
1 | const Feact = { |
就像props一样,我们给组件实例增加state、
注意,当我们组件没有
getInitialState定义的时候,state的初始状态是null,React不会给state添加默认值为空对象。所以,当想使用state的时候,必须利用这个方法,返回一个对象。否则,如果在使用this.state.foo这样的操作的时候,第一次render会爆炸的哦。
现在,有了getInitialState,Feact组件可以随时使用this.state这个方法啦。
增加简单的setState()
现在,我们准备给setState在Feact.createClass中,找一个合适的位置。为了实现它,我们将给所有的通过Feact.createClass创建的组件一个prototype,这个prototype将拥有一个setState方法。
1 | function FeactComponent() { |
misSpecIntoComponent在React中,那可是相当的复杂,当然,也更健壮,它的角色更像是mixins,同时,保证用户在使用的时候,不会因为这个函数翻车。
让setState代入到updateComponent方法中
回顾上一节,我们通过FeactCompositeComponentWrapper.receiveComponent来实现一个组件的更新,而这个函数接下来调用了updateComponent方法,所以,看起来我们只要通过updateComponent来处理state就能实现更新。那么,我们只需要将FeactComponent.prototype.setState和FeactCompositeComponentWrapper.receiveComponent打通即可。
在React中,有『公共实例』和『内部实例』的概念。公共实例是通过createClass创建的组件的实例,内部实例是React内部对象的实例。那么,在这些概念下,内部实例就是FeactCompositeComponentWrapper,不难发现,内部实例能够感知到公共实例的一切,但是反过来却不行。现在,我们准备改变这个。setState是公共实例给内部实例通信的方法,带着这个想法,看如下实现
1 | function FeactComponent() { |
React解决getMyInternalInstancePlease这个问题的方法是通过一个实例映射,这个映射保存了某个公共实例内的内部实例。
1 | const FeactInstanceMap = { |
而这个映射关系的建立,是在组件挂载的时候。
1 | const FeactCompositeComponentWrapper { |
现在,还有一个没有用到的方法,FeactReconciler.performUpdateIfNecessary,这个方法就像其他的协调器方法一样
1 | const FeactReconciler { |
最终,我们终于调用了updateComponent,但是请注意,这里我们做了一点HACK,虽然我们调用了更新,但是,我们传递了相同的两个参数。任何时候,当updateComponet传递了相同的元素,React就知道,只有state更新了,否则就是props更新了。React会通过prevElement !== nextElement来判断是否调用componentWillReceiveProps,所以,这里先改造下Feact,让它也做相同的处理。
1 | class FeactCompositeComponentWrapper { |
这个只是updateComponent的片段,只是为了解决setState()并不会导致componentWillReceiveProps在渲染前的调用。也就是说,setState无需影响到props。
通过新的state更新
现在处理updateCompoent,内部实例已经通过internalInstance._pendingPartialState获取到了新的state,所以现在我们需要做的,仅仅是让这个组件再渲染一次。
1 | class FeactCompositeComponentWrapper { |
组件的更新跟之前很像,不同的是,我们增加了state的赋值操作。因为state仅仅挂载在公共实例上,_performComponentUpdate只改变了一行,_updateRenderedComponent一行没变。真正改变的重点就是在updateComponent中,我们合并state的操作。
至此,setState的功能,已经基本完成啦!
但是,上面的setState的实现,比较屌丝,性能也比较糟糕。主要的问题是,每次调用setState都会导致组件的渲染。这将迫使用户,要么好好想想怎么组装数据然后只使用一次setState,要么就接受这种每次调用就渲染的问题。接下来,我们要做的就是改造它,使它最好能自适应的具有批量工作的能力,从而减少渲染的次数。
批量调用setState
仔细观察生命周期函数的调用,不难发现,每次的渲染,都调用了componentWillReceiveProps。如果用户在componentWillReceiveProps中调用setState会发生什么?在当前的代码中,这将会导致在第一次渲染过程中又一次新的渲染,而对state改版而造成的props的响应,画面太美不敢看。所以,我们最好将一系列的state和props的改变,都塞到同一次渲染中。
首先我们需要给需要批量的操作保存起来
首先想到的就是改造_pendingPartialState,让它成为一个数组。
1 | function FeactComponent() { |
而在updateComponent中,调用我们将要设计的合并state方法。
1 | class FeactCompositeComponentWrapper { |
其次将批量合并之后的state塞到一次渲染过程中
注意这里的批量操作原理是非常简单的,并不是
React中的全部功能。我们主要指出批量操作的原理。
在Feact中,我们只在页面还在渲染的时候,批量合并state,其他时候,我们并不做这样的处理。所以,在updateComponent过程中,我们会做一个标记,告诉外面,我们正在渲染,在渲染结束之后,讲其设置为false。如果setState看到了这个标记为true,他会挂起这个state,但不渲染它。因为它知道,当当前的渲染结束的时候,渲染引擎会重拾这个state进行下一次渲染。
1 | class FeactCompositeComponent { |
基本上完成啦。
setState陷阱
现在,我们已经明白了setState的工作原理以及批量工作的概念,但是这里有几个关于setState的陷阱需要注意。我们知道,当我们利用state去更新组件的时候,有好几个步骤,每个步骤中,被挂起的state需要一个一个的处理,也就是说,当我们在setState中使用this.state是非常危险的
1 | componentWillReceiveProps(nextProps) { |
这个例子中,我们期待执行2次加法运算。但是,state会被批量处理,所以第二次setState和第一次的setState有相同的输入,所以,加法运算只会执行一次。
React中,解决这个问题的方法是传入一个回调函数
1 | componentWillReceiveProps(nextProps) { |
当传入回调函数的时候,我们将会得到正确的结果,我们将这个特性运用到Feact中去
1 | _processPendingState() { |
至此,大功告成啦!