上两篇中,React具有了基本的渲染能力。但是,一旦渲染发生,就不能再改变了。这一篇中,我们将在render中,添加更新功能,并且,将简单的展示虚拟dom的diff过程。
简单更新
让React应用实现更新,最普通的办法就是调用组件的setState()方法。但是,React也支持通过React.render()来实现更新。就像如下所示:
1 | React.render(<h1>hello</h1>, root); |
本篇中,我们暂时忽略setState(),先通过Feact.render()来实现更新。说实话,这就是最屌丝的『props改变,所以更新』的模型,即如果你又render了,并且传入了不同的props给子组件,那么就更新呗。
开始
理念非常简单,Feact.render()只需检查,之前是否渲染过,如果渲染过,就执行update。结构如下所示
1 |
|
看起来很美好,如果之前渲染过,则将之前的组件和更新后的组件,传递给一个函数,这个函数将计算出dom所需要做出的更新动作。否则,就像上一篇所讲的那样,直接将组件渲染进来即可。那么,问题已经被降级为搞定我们缺失的2个函数。
记住我们所做的
对于每一次渲染,我们需要记录那些我们渲染过的组件,以获取他们的引用,方便后续的渲染。咋整呢?最好的方法就是在创建dom节点的时候,做上一个标记。
1 |
|
那么,对于已经挂在的组件的情况,一样的返回container.__feactComponentInstance。
1 | function getTopLevelComponentInContainer(container) { |
更新
首先先看一个简单的示例。
1 |
|
2秒后,我们又一次调用了Feact.render(),但是,这次调用所传入的元素大概长这样
1 | { |
当Feact确定了这是一个更新动作,则会进入到updateRootComponent()函数中,
1 |
|
这里注意,我们没有创建一个新的组件,prevComponent是我们第一次渲染的时候就创建的组件,现在只是更新了它自己而已。所以,组件一旦被创建,它将一直存在,直到被卸载(unmount)。
再来考虑FeactDOMComponent
1 |
|
receiveComponent()只是调用了updateComponent(),而updateComponent()则最终调用了_updateDOMProperties()和_updateDOMChildren(),这2个函数最终,完成了真实dom的更新。需要注意的是,_updateDOMProperties()更多的关注了CSS相关的内容。简便期间,我们暂时不考虑它,仅仅指出,在React中,这个函数是用来解决样式的更新的。
_updateDOMChildren()在React中,那可是相当的复杂,主要是解决了各种不同的场景下的执行情况。但是,在Feact中,为了方便理解,我们只考虑子节点是文本的情况,也就是上文中所写的,我们从hello,更新到了hello again。
1 | class FeactDOMComponent { |
从上面可以看出,Feact的_updateDOMChildren非常屌丝,但是大概原理就是这样。
更新自定义组件
上面这些内容,我们实现了FeactDOMComponent的更新,但是下面这种情况就无能为力了。
1 |
|
更新自定义组件就有趣多了,这也是React的牛逼之处。有一个好消息,自定义组件的更新,归根结底会降级到原生组件的更新,所以上面我们做的工作,都是有效的,没有浪费。
还有个更好的消息,updateRootComponent在执行的时候,并不关心组件是自定义的组件,还是原生的组件。他只是调用receiveComponent,所以,我们需要做的,只是给FeactCompositeComponentWrapper也增加一个receiveComponent就好啦。
1 |
|
这里有点点复杂,但是也解决了很多问题,而且,React中,基本跟我们写的一样,一样的在ReactCompositeComponentWrapper有上面我们写的4个函数。
最终,这些一系列复杂的更新操作,都会降级到去render一系列的props,然后,把得到的结果传递给_renderedComponent进行更新。_renderedComponent会变成下一个FeactCompositeComponentWrapper或者FeactDOMComponent。
使用协调器
挂载组件当然要通过我们之前所写的FeactReconciler,虽然这个操作对于Feact没什么意义,但是我们还是保持和React一致。
1 |
|
生命周期shouldComponentUpdate和componentWillReceiveProps
1 |
|
还有个大坑
到目前为止,还有个很大的问题不知你们发现了没,那就是现在所有的更新,都是假设更新的时候,都是使用了相同的组件,也就是说,下面这种情况我们可以更新
1 | Feact.render({ |
但是,下面这种情况更新不了
1 | Feact.render( |
这个例子中,我们传入了一个全新的组件,Feact非常弱智的继续渲染原来的MyCoolComponent,然后把他的props更新为{someOtherProp: 'hmmm' }。
正确的做法是告诉它,组件的type已经改变了,不应该再去更新,应该卸载掉MyCoolComponent,然后挂载SomeOtherComponent。
想实现这些,Feact必须做到以下2点:
- 具有卸载组件的能力(
unmount) - 通知组件的
type已经改变,然后让FeactReconciler执行FeactReconciler.mountComponent,而不是去去执行FeactComponent.receiveComponent
在
React中,如果你又一次渲染了相同的组件,那么它会更新。这时候,你不需要定义一个key给你的组件。key仅仅在需要渲染成吨的children的时候,是必要的。如果你忘记了给渲染的子组件增加key,React会给你一堆警告,你最好留意这些警告,因为如果没这些key的话,React在需要更新的时候执行的不是更新,而是卸载掉原来的组件,然后挂载新的。
现在知道什么是虚拟DOM了么
在React刚出来的时候,各种吹所谓的虚拟DOM,但是我觉得虚拟DOM并不是真的需要关心的。他仅仅是一些概念而已。真正需要关注的,是prevElement和nextElement,他们一起捕获了每次渲染不同的地方,然后FeactDOMComponent将这些不同的地方挂载到了真实的DOM上。