Vue源码学习(五) diff算法解析
上篇关于Vue Watcher原理分析的文章中,在解释了Vue watcher的源码之后,将watcher分为了三类,分别是userWatcher,computeWatcher,以及renderWatcher。
这三者的主要不同之一就是求Watcher.value时的不同,userWatcher的value就是我们的观察对象,computeWatcher的value求值是通过我们在computed属性中定义的handler,而renderWatcher的value就是更新视图的结果。
上篇博客中主要讲的是前两者,对于renderWatcher的具体的更新视图的流程没有详细解释,也就是update,这其中最主要的逻辑就是vue的diff算法。
这篇博客就来讲一下vue的diff算法,并通过这个算法来讲一下啊我们平时写vue时的一点相关的注意事项。
源码分析
我们直接看renderWatcher中的求值函数是什么,对于这一段是怎么来的,可以看我的上篇关于vue数据驱动原理的博客
1 | new Watcher(vm, updateComponent, noop, { //创建一个watcher,并传入刚才定义好的updateComponent,这个watcher是一个渲染watcher |
这段代码就是组件在初始化过程中创建的渲染Wacher,其中第二个参数就是求值函数,也就是视图更新的函数。
1 | updateComponent = () => { //定义updateComponent函数,調用上面声明的_update,第一个参数是_render返回的组件,_render定义在core/instance/render.js中 |
也就是说视图更新函数就是重新调用render函数生成vnode,并把新的vnode更新到视图上。
我们现在主要的关注点就是这个update,也就是我们的diff算法
_update
1 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { |
这个函数主要就是一个回溯算法,首先将之前全局的activeInstance保存到函数闭包中,并将activeInstance置为当前的节点,这个时候去调用patch如果更新子节点就可以用全局的activeInstance作为父节点,更新完成之后再将闭包中的之前的instance回复回来。
__patch__
现在就是我们最主要的逻辑了,patch,也就是我们的diff算法是如何更新节点的。
1 | return function patch (oldVnode, vnode, hydrating, removeOnly) { |
这段代码看似很长,其实总结起来就是以下几步:
- 如果定义了oldVnode但是没有定义vnode,说明是要移除旧的节点。
- 如果定义了vnode但是没有定义oldVnode,直接创建新节点就好。
- 如果不是原生标签并且是相同的节点,就执行patchVnode算法,去进行进一步的patch,这里为什么明明是相同的却还要patch呢?主要是因为这个相同节点的判断是一个初步的判断,类似于布隆过滤器,我说相同不一定相同,但是我说不同就是一定不同,而且就算二者真的相同,子节点也可能不同,这个待会在讲,这个也是diff最核心的部分。
- 如果不是相同节点,那就做三件事:创建新节点,更新旧节点的父节点,销毁旧节点。
patchVnode
我们再来详细讲一下上面的第三点
1 | if (isUndef(vnode.text)) { |
这就是patchVnode的核心代码,也是分为以下几步:
- 如果没定义text属性,说明不是文本节点。
- 这个时候如果新旧节点都有children,那就执行updateChildren
- 如果只有新节点有,就把新节点的children直接挂载
- 如果只有旧节点有,那就把所有旧节点的children删除
- 如果旧节点是文本节点,那就把旧节点的文本置为空字符串
- 如果定义了,说明新节点是文本节点,直接把旧节点的text属性置为新节点的text
updateChildren
现在我们进入和diff算法最核心的部分
1 | function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
这段代码看起来很长,其实也很容易懂,大家自己看一看,随便举个例子试一下就明白了。
这里比较重要的一点是对 nodeOps.insertBefore
这个函数的理解,它的作用是将第一个参数中的第二个参数(第一个参数的子节点)移动到第三个参数前面,不是单纯的insert。
如果大家实在不明白,可以去看一下这篇博客。
注意事项
通过上述的分析,我们对diff算法应该有了个大致的了解,但其实有一点我们还没有将,那就是sameVnode这个是怎么定义的。
还是直接上代码
1 | function sameVnode (a, b) { |
这里可以看到首先必须二者的key要相同,这个key就是我们平时列表渲染时传入的key,很多人都会直接用数组的index,其实这样做会有一些问题。
第一个问题就是如果我有三个相同的节点,现在reverse,如果我的key是index,那么所有的sameVnode都会判断失败,明明可以重用的节点却被销毁然后新建了,这对性能会有影响。
第二个问题是如果我要删除第一个节点,那么oldCh第二个节点的key就会与ch的第一个节点相同,这就可能导致我本来想删除第一个节点,却删除了最后一个节点。
所以说我们还是尽量不要用数组的index作为key。