在前面的章节中,我们介绍了compile渲染template,完成了template –> AST –> render function 的过程。接下来让我们看看VNode的生成过程,以及将VNode转换为真实dom的patch过程。
在我们之前看到的内容中,对model进行操作的时候,会触发对应的Dep中的Watcher对象。Watcher对象会调用对应的update来修改视图。最终的结果是产生新的VNode节点,新的VNode和旧的VNode会进行一个patch过程,对比过程得出差异diff,最终将diff更新到视图上。这就是从虚拟dom到dom的更新过程。
VNode
VNode是什么
先看源码
包含了我们常用的一些标记节点的属性,为什么是VNode?其实网上已经有大量的资料可以了解到VNode的优势,比如大量副交互的性能优势,ssr的支持,跨平台Weex的支持…这里不再赘述。我们接下来看一下VNode的基本分类:
比如我们创建一个 emptyVNode:
比如一个 textVNode:
生成 VNode
有了之前章节的知识,现在我们需要编译下面这段template
|
|
得到这样的render function
|
|
得到这样的render function
|
|
要看懂上面的 render function,只需要了解 _c,_m,_v,_s这几个函数的定义,具体定义可以查看flow/component.js
,其中 _c 是 createElement,_m 是 renderStatic,_v 是 createTextVNode,_s 是 toString。 我们在编译的过程中调用了vm._render() 方法,其中_render函数中有这样一句:
|
|
也就是通过调用自身的render方法,传入$createElement方法来生成VNode节点。那么核心便在$createElement的方法上了。
经过上面的一系列过程,最终得到了我们的VNode,看起来是这样的:
小结
render函数的执行,调用了createElement方法,其内部通过传入的相关参数,根据不同类型,一步步解析出了VNode。那么 VNode如果进行渲染成不同平台所需的内容呢?下篇我们将会继续介绍patch的工作。
patch函数
通过前文的介绍,我们知道需要将VNode转换成真实的node节点,需要通过patch函数来实现:
而patch在下面代码中定义的:
|
|
createPatchFunction
这里主要是为了判断当前环境是否是在浏览器环境中,也就是是否存在Window对象。这里也是为了做跨平台的处理,如果是在server render环境,那么patch就是一个空操作。 我们接着去找render的实现:
|
|
这里通过createPatchFunction函数,来创建返回一个patch函数。path接收6个参数:
- oldVnode: 旧的虚拟节点或旧的真实dom节点
- vnode: 新的虚拟节点
- hydrating: 是否要跟真是dom混合
- removeOnly: 特殊flag,用于组件
- parentElm:父节点
- refElm: 新节点将插入到refElm之前 具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,我们发现代码中的关键在于sameVnode、 createElm 和 patchVnode 方法。
sameVnode
判断2个节点,是否是同一个节点
createElm
|
|
通过以上的注释,我们可以知道:createElm 方法的最终目的就是创建真实的 DOM 对象
patchVnode
|
|
让我们来画张图屡一下大致的流程:
图解diff算法
addVnodes和removeVnodes都比较好理解,一个是增加一个节点元素,一个是删除节点元素。主要来看一下updateChildren方法。
updateChildren源码
源码如下
图解updat过程
定义初始
如果oldStartVnode 和 newStartVnode 是sameVnode,则patchVnode,同时彼此向后移动一位
5555如果oldEndVnode 和 newEndVnode 是sameVnode,则patchVnode,同时彼此向前移动一位
如果oldStartVnode 和 newEndVnode 是 sameVnode,则先 patchVnode,然后把oldStartVnode移到oldCh最后的位置即可,然后oldStartIdx向后移动一位,newEndIdx向前移动一位
如果oldEndVnode 和 newStartVnode 是 sameVnode,则先 patchVnode,然后把oldEndVnode移到oldCh最前的位置即可,然后newStartIdx向后移动一位,oldEndIdx向前移动一位
若为同一类型就调用patchVnode,就将对应下标处的oldVnode设置为undefined,把vnodeToMove插入到oldCh之前,newStartIdx继续向后移动。如果两个 vnode 不相似,视为新元素,执行 createElm创建。
如果老 vnode 数组的开始索引大于结束索引,说明新 node 数组长度大于老 vnode 数组,执行 addVnodes 方法添加这些新 vnode 到 DOM 中
如果老 vnode 数组的开始索引小于结束索引,说明老 node 数组长度大于新 vnode 数组,执行 removeVnodes 方法从 DOM 中移除老 vnode 数组中多余的 vnode。
总结
到这里,patch的主要功能也基本讲完了,我们发现,在本篇中,大量出现了一个key字段。经过上面的调研,其实我们已经知道Vue的diff算法中其核心是基于两个简单的假设:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
- 同一层级的一组节点,他们可以通过唯一的id进行区分 基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n),当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。 比如一下这个情况:
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率? 所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。