-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
reactjs源码分析-下篇(更新机制实现原理) #3
Comments
改造的diff算法有一点问题:
应该修改为
|
对于flattenChildren方法中,如果两个子节点其中一个定义了key值,另一个没有定义key值。那么在扁平化处理的时候是否后一个子节点覆盖掉前一个子节点的情况呢? |
对于generateComponentChildren方法中,将有序的nextChildrenElements 数组转换成无序的nextChildren对象,下面进行diff的时候子节点的前后次序是不是已经乱掉了呢? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
reactjs源码分析-下篇(更新机制实现原理)
reactjs是目前比较火的前端框架,但是目前并没有很好的解释原理的项目。reactjs源码比较复杂不适合初学者去学习。所以本文通过实现一套简易版的reactjs,使得理解原理更加容易。包括:
声明:
所有实例源码都托管在github。点这里里面有分步骤的例子,可以一边看一边运行例子。
前言
紧接上文,虚拟dom差异化算法(diff algorithm)是reactjs最核心的东西,按照官方的说法。他非常快,非常高效。目前已经有一些分析此算法的文章,但是仅仅停留在表面。大部分小白看完并不能了解(博主就是 = =)。所以我们下面自己动手实现一遍,等你完全实现了,再去看那些文字图片流的介绍文章,就会发现容易理解多了。
实现更新机制
下面我们探讨下更新的机制。
一般在reactjs中我们需要更新时都是调用的setState。看下面的例子:
点击文字,调用setState就会更新,所以我们扩展下ReactClass,看下setState的实现:
可以看到setState主要调用了对应的component的receiveComponent来实现更新。所有的挂载,更新都应该交给对应的component来管理。
就像所有的component都实现了mountComponent来处理第一次渲染,所有的componet类都应该实现receiveComponent用来处理自己的更新。
自定义元素的receiveComponent
所以我们照葫芦画瓢来给自定义元素的对应component类(ReactCompositeComponent)实现一个receiveComponent方法:
不要被这么多代码吓到,其实流程很简单。
它主要做了什么事呢?首先会合并改动,生成最新的state,props然后拿以前的render返回的element跟现在最新调用render生成的element进行对比(_shouldUpdateReactComponent),看看需不需要更新,如果要更新就继续调用对应的component类对应的receiveComponent就好啦,其实就是直接当甩手掌柜,事情直接丢给手下去办了。当然还有种情况是,两次生成的element差别太大,就不是一个类型的,那好办直接重新生成一份新的代码重新渲染一次就o了。
本质上还是递归调用receiveComponent的过程。
这里注意两个函数:
另外可以看到这里还处理了一套更新的生命周期调用机制。
文本节点的receiveComponent
我们再看看文本节点的,比较简单:
没什么好说的,如果不同的话,直接找到对应的节点,更新就好了。
基本元素element的receiveComponent
最后我们开始看比较复杂的浏览器基本元素的更新机制。
比如我们看看下面的html:
想一下我们怎么以最小代价去更新这段html呢。不难发现其实主要包括两个部分:
所以更新代码结构如下:
整体上也不复杂,先是处理当前节点属性的变动,后面再去处理子节点的变动
我们一步步来,先看看,更新属性怎么变更:
属性的变更并不是特别复杂,主要就是找到以前老的不用的属性直接去掉,新的属性赋值,并且注意其中特殊的事件属性做出特殊处理就行了。
下面我们看子节点的更新,也是最复杂的部分。
就像我们之前说的一样,更新子节点包含两个部分,一个是递归的分析差异,把差异添加到队列中。然后在合适的时机调用
_patch
把差异应用到dom上。那么什么是合适的时机,updateDepth又是干嘛的?
这里需要注意的是,
_diff
内部也会递归调用子节点的receiveComponent于是当某个子节点也是浏览器普通节点,就也会走_updateDOMChildren这一步。所以这里使用了updateDepth来记录递归的过程,只有等递归回来updateDepth为0时,代表整个差异已经分析完毕,可以开始使用patch来处理差异队列了。所以我们关键是实现
_diff
与_patch
两个方法。我们先看_diff的实现:
我们分析下上面的代码,咋一看好多,好复杂,不急我们从入口开始看。
首先我们拿到之前的component的集合,如果是第一次更新的话,这个值是我们在渲染时赋值的。然后我们调用generateComponentChildren生成最新的component集合。我们知道component是用来放element的,一个萝卜一个坑。
注意flattenChildren我们这里把数组集合转成了对象map,以element的key作为标识,当然对于text文本或者没有传入key的element,直接用index作为标识。通过这些标识,我们可以从类型的角度来判断两个component是否是一样的。
generateComponentChildren会尽量的复用以前的component,也就是那些坑,当发现可以复用component(也就是key一致)时,就还用以前的,只需要调用他对应的更新方法receiveComponent就行了,这样就会递归的去获取子节点的差异对象然后放到队列了。如果发现不能复用那就是新的节点,我们就需要instantiateReactComponent重新生成一个新的component。
当我们生成好新的component集合以后,我们需要做出对比。组装差异对象。
对比老的集合和新的集合。我们需要找出涵盖四种情况,包括三种类型(UPATE_TYPES)的变动:
所以我们找出了这三种类型的差异,组装成具体的差异对象,然后加到了差异队列里面。
比如我们看下面这个例子,假设下面这些是某个父元素的子元素集合,上面到下面代表了变动流程:
数字我们可以理解为给element的key。
正方形代表element。圆形代表了component。当然也是实际上的dom节点的位置。
从上到下,我们的4 2 1里 2 ,1可以复用之前的component,让他们通知自己的子节点更新后,再告诉2和1,他们在新的集合里需要移动的位置(在我们这里就是组装差异对象加到队列)。3需要删除,4需要新增。
好了,整个的diff就完成了,这个时候当递归完成,我们就需要开始做patch的动作了,把这些差异对象实打实的反映到具体的dom节点上。
我们看下_patch的实现:
_patch
主要就是挨个遍历差异队列,遍历两次,第一次删除掉所有需要变动的节点,然后第二次插入新的节点还有修改的节点。这里为什么可以直接挨个的插入呢?原因就是我们在diff阶段添加差异节点到差异队列时,本身就是有序的,也就是说对于新增节点(包括move和insert的)在队列里的顺序就是最终dom的顺序,所以我们才可以挨个的直接根据index去塞入节点。但是其实你会发现这里有个问题,就是所有的节点都会被删除,包括复用以前的component类型为
UPATE_TYPES.MOVE_EXISTING
的,所以闪烁会很严重。其实我们再看看上面的例子,其实2是不需要记录到差异队列的。这样后面patch也是ok的。想想是为什么呢?我们来改造下代码:
可以看到我们多加了个lastIndex,这个代表最后一次访问的老集合节点的最大的位置。
而我们加了个判断,只有_mountIndex小于这个lastIndex的才会需要加入差异队列。有了这个判断上面的例子2就不需要move。而程序也可以好好的运行,实际上大部分都是2这种情况。
这是一种顺序优化,lastIndex一直在更新,代表了当前访问的最右的老的集合的元素。
我们假设上一个元素是A,添加后更新了lastIndex。
如果我们这时候来个新元素B,比lastIndex还大说明当前元素在老的集合里面就比上一个A靠后。所以这个元素就算不加入差异队列,也不会影响到其他人,不会影响到后面的path插入节点。因为我们从patch里面知道,新的集合都是按顺序从头开始插入元素的,只有当新元素比lastIndex小时才需要变更。其实只要仔细推敲下上面那个例子,就可以理解这种优化手段了。
这样整个的更新机制就完成了。我们再来简单回顾下reactjs的差异算法:
首先是所有的component都实现了receiveComponent来负责自己的更新,而浏览器默认元素的更新最为复杂,也就是经常说的 diff algorithm。
react有一个全局_shouldUpdateReactComponent用来根据element的key来判断是更新还是重新渲染,这是第一个差异判断。比如自定义元素里,就使用这个判断,通过这种标识判断,会变得特别高效。
每个类型的元素都要处理好自己的更新:
自定义元素的更新,主要是更新render出的节点,做甩手掌柜交给render出的节点的对应component去管理更新。
text节点的更新很简单,直接更新文案。
浏览器基本元素的更新,分为两块:
整个reactjs的差异算法就是这个样子。最核心的两个_shouldUpdateReactComponent以及diff,patch算法。
小试牛刀
有了上面简易版的reaactjs,我们来实现一个简单的todolist吧。
效果如下:
整个的流程是这样:
ReactCompositeComponent
渲染自定义元素TodoList,调用getInitialState拿到初始值,然后使用ReactDOMComponent
渲染render返回的div基本元素节点。div基本元素再一层层的使用ReactDOMComponent
去渲染各个子节点,包括input,还有p。基本上,整个流程都梳理清楚了
结语
这只是个玩具,但实现了reactjs最核心的功能,虚拟节点,差异算法,单向数据更新都在这里了。还有很多reactjs优秀的东西没有实现,比如对象生成时内存的线程池管理,批量更新机制,事件的优化,服务端的渲染,immutable data等等。这些东西受限于篇幅就不具体展开了。
reactjs作为一种解决方案,虚拟节点的想法比较新奇,不过个人还是不能接受这种别扭的写法。使用reactjs,就要使用他那一整套的开发方式,而他核心的功能其实只是一个差异算法,而这种其实已经有相关的库实现了。
最后再吐槽下前端真是苦命,各种新技术,各种新知识脑细胞不够用了。也难怪前端永远都缺人。
相关资料:
The text was updated successfully, but these errors were encountered: