残缺代码 (https://github.com/mfaying/si...

虚构DOM

虚构DOM将虚构节点vnode和旧虚构节点oldVnode进行比照,得出真正须要更新的节点进行DOM操作。对两个虚构节点进行比照是虚构DOM中最外围的算法(即patch)。

因为Vue.js的变动侦测粒度很细,肯定水平上能够晓得哪些状态产生了变动,所以能够通过细粒度的绑定来更新视图,Vue.js1.0就是这样实现的。然而因为粒度太细,会有很多watcher同时察看某状态,会有一些内存开销以及一些依赖追踪的开销,所以Vue.js采纳了一个中等粒度的解决方案。状态侦测不再细化到某个具体节点,而是某个组件,组件外部通过虚构DOM来渲染视图,这能够大大缩减依赖数量和watcher数量。

VNode

在Vue.js中存在一个VNode类,应用它能够实例化不同类型的vnode实例,而不同类型的vnode实例各自示意不同类型的DOM元素。vnode能够了解为JavaScript对象版本的DOM元素。

VNode的作用

Vue.js能够将上一次渲染视图时创立的vnode缓存起来,将新创建的vnode和缓存的vnode进行比照,找出不一样的办法并基于此去批改实在的DOM。

VNode的类型

  1. 正文节点
  2. 文本节点
  3. 元素节点
  4. 组件节点
  5. 函数式组件
  6. 克隆节点

正文节点

export const createEmptyVNode = text => {  const node = new VNode();  node.text = text;  node.isComment = true;  return node;}

文本节点

export function createTextVNode (val) {  return new VNode(undefined, undefined, undefined, String(val))}

克隆节点

克隆节点是将现有节点的属性复制到新节点中,让新创建的节点和被克隆接节点的属性保持一致,从而实现克隆成果。它的作用是优化动态节点和插槽节点(slot node)。

以动态节点为例,动态节点因为它的内容不会扭转,除了首次渲染须要执行渲染函数获取VNode之外,后续更新不须要执行渲染函数从新生成vnode。因而就会应用创立克隆节点的办法将vnode克隆一份,应用克隆节点进行渲染。

export function cloneVNode (vnode, deep) {  const cloned = new VNode(    vnode.tag,    vnode.data,    vnode.children,    vnode.text,    vnode.elm,    vnode.context,    vnode.componentOptions,    vnode.asyncFactory  )  cloned.ns = vnode.ns  cloned.isStatic = vnode.isStatic  cloned.key = vnode.key  cloned.isComment = vnode.isCommet  cloned.isCloned = true  if (deep && vnode.children) {    cloned.children = cloneVNodes(vnode.children)  }  return cloned;}

克隆节点与被克隆节点之间的惟一区别是isCloned属性。

元素节点

元素节点通常会存在以下4种无效属性:

  1. tag: 节点名称,例如p、ul、li等
  2. data: 节点上的数据,比方attrs、class和style等。
  3. children: 以后节点的子节点列表
  4. context: 以后组件的Vue.js实例

组件节点

和元素节点相似,此外它还有以下两个独有的属性

  1. componentOptions: 组件节点的选项参数,蕴含propsData、tag和children等信息。
  2. componentInstance: 组件的实例,也是Vue.js的实例。

函数式组件

和元素节点相似,此外它还有以下两个独有的属性

  1. functionalOptions
  2. functionalContext

patch

虚构DOM最外围的局部是patch,它能够将vnode渲染成实在的DOM。patch也能够叫作patching算法。

patch不是暴力替换节点,而是在现有DOM上进行批改来达到渲染视图的目标。对现有DOM进行批改须要做三件事:

  1. 创立新增的节点
  2. 删除曾经废除的节点
  3. 批改须要更新的节点

新增节点

新增节点的一个很显著的场景就是,当oldVnode不存在而vnode存在时,就须要应用vnode生成实在的DOM元素并将其插入到视图中。

当vnode和oldVnode齐全不是同一个节点时(雷同的地位节点都存在),须要应用vnode生成实在的DOM元素并将其插入到视图中。

删除节点

当一个节点只在oldVnode中存在时,咱们须要把它从DOM中删除。

当vnode和oldVnode齐全不是同一个节点时,在DOM中须要应用vnode创立的新节点替换oldVnode所对应的旧节点,而代替过程中是将新创建的DOM节点插入旧节点的旁边,而后再将旧节点删除。

更新节点

当新旧两个节点是雷同的节点,咱们须要对这两个节点进行比拟粗疏的比对,而后对oldVnode在视图中所对应的实在节点进行更新。

创立节点

只有三种类型的节点会被创立并插入到DOM中:元素节点、正文节点和文本节点。

判断vnode是否是元素节点,只须要判断它是否具备tag属性即可。咱们能够调用以后环境下的createElement办法(在浏览器环境下是document.createElement)来创立元素节点。

将元素渲染到视图,只须要调用以后环境下的appendChild办法(在浏览器环境下是parendNode.appendChild)

元素通常都会有子节点(children),所以当一个元素节点被创立后,咱们须要将它的子节点也创立进去并插入到这个刚创立出的节点上面。这是一个递归的过程,只须要将vnode中的children属性循环一遍,将每个子虚构节点都执行一遍创立元素的逻辑。

如果vnode不存在tag属性,那么它可能是另外两个节点:正文节点和文本节点。当isComment属性为true时,vnode是正文节点,否则为文本节点。如果是文本节点,调用以后环境下的createTextNode办法(在浏览器环境下是document.createTextNode)来创立实在的文本节点并将其插入到指定的父节点中。如果是正文节点,则调用以后环境下的createComment办法(在浏览器环境下是document.createComment)来创立实在的正文节点并将其插入到指定的父节点。

删除节点

function removeVnodes (vnodes, startIdx, endIdx) {  for (; startIdx <= endIdx; ++startIdx) {    const ch = vnodes[startIdx]    if (isDef(ch)) {      removeNode(ch.elm)    }  }}const nodeOps = {  removeChild (node, child) {    node.removeChild(child)  }}function removeNode(el) {  const parent = nodeOps.parentNode(el)  if (isDef(parent)) {    nodeOps.removeChild(parent, el)  }}

将节点操作封装成函数放在nodeOps里是为了预留跨平台渲染接口。

更新节点

1.动态节点
如果新旧两个虚构节点都是动态节点,就不须要进行更新操作,能够间接跳过更新节点的过程。
2.新虚构节点有文本属性
依据新节点(vnode)是否有text属性,更新节点能够分为两种不同的状况。如果有text属性,不管之前旧节点的子节点是什么,间接调用setTextContent办法(浏览器为node.textContent)将视图中DOM节点的内容改为虚构节点的text属性所保留的文字。若新旧节点都是文本,且文本雷同,则不执行。
3.新虚构节点无文本属性
如果新虚构节点没有text属性,那么它是一个元素节点。
3.1有children的状况
若旧虚构节点也有children属性,那么须要对新旧两个虚构节点的children进行一个更具体的比照并更新。更新children可能会挪动某个子节点的地位,也可能会删除或新增某个节点,具体更新children前面会介绍。

若无children属性,阐明旧虚构节点要么是一个空标签,要么是一个文本节点。如果是文本节点,那么先把文本清空让它变成空标签,而后将新虚构节点中的children挨个创立成实在的DOM元素节点并将其插入到视图中的DOM节点上面。
3.1无children的状况
阐明新创建的节点是一个空节点,旧虚构节点有子节点、有文本都须要删除,达到视图是空标签的目标。

更新子节点

后面探讨了当新节点的子节点和旧节点的子节点都存在并且不雷同,会进行子节点的更新操作。上面咱们具体探讨这种状况。

更新子节点大略能够分为4种操作:更新节点、新增节点、删除节点、挪动节点。

比照两个子节点列表(children),首先须要做的事件是循环。循环newChildren,每循环到一个新子节点,就去oldChildren找到和以后节点雷同的那个旧子节点。如果找不到,就新增,创立节点并插入视图;如果找到了,就做更新操作;如果地位不同,就挪动节点。

更新策略

1.创立子节点
如果在oldChildren中没有找到与本次循环所指向的新子节点雷同的节点,咱们须要执行创立节点的操作,将新创建的节点插入到oldChildren中所有未解决节点(未解决就是没有任何更新操作的节点)的后面。不能插入已解决节点前面,这是因为旧虚构节点已解决节点不包含咱们新插入的节点,如果间断插入多个新节点,新节点的程序就会是反的。
2.更新子节点
节点在newChildren和oldChildren中都存在且地位雷同,须要进行更新节点的操作。更新节点操作之前曾经介绍过。
3.挪动子节点
节点在newChildren和oldChildren中都存在但地位不同,须要将这个节点的地位以新虚构节点的地位为基准进行挪动。

通过Node.insertBefore()办法,咱们能够将一个已有节点挪动到一个指定的地位。

那么如何得悉新虚构节点的地位在哪里呢?其实并不难。

比照两个子节点列表是通过从左到右循环newChildren这个列表,而后每循环一个节点,就去oldChildren中寻找与这个节点雷同的节点进行解决。也就是说,newChildren中以后被循环到的这个节点的右边是被解决过的。那就不难发现,这个节点的地位是所有未解决节点的第一个节点。所以,只有把须要挪动的节点挪动到所有未解决节点的最后面就能够了。

对于怎么分辨哪些节点是解决过的,哪些节点是未解决的,前面会介绍。

4.删除子节点
当newChildren中所有节点都被循环了一遍后,如果oldChildren中还有没有被解决的节点,那么这些节点就是须要被删除的。

优化策略

通过状况下,并不是所有子节点的地位都会产生挪动,一个列表中总有几个节点的地位是不变的。针对这些地位不变的或者说地位能够预测的节点,咱们不须要循环来查找,咱们有4种快捷查找形式:

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前
  4. 新前与旧后

新前:newChildren中所有未解决的第一个节点
新后:newChildren中所有未解决的最初一个节点
旧前:oldChildren中所有未解决的第一个节点
旧后:oldChildren中所有未解决的最初一个节点

新前与旧前

如果新前与旧前是同一个节点,因为地位雷同,所以更新节点即可。如果不是,换下一个快捷查找形式。

新后与旧后

如果新后与旧后是同一个节点,因为地位雷同,所以更新节点即可。

新后与旧前

如果新后与旧前是同一个节点,因为地位不同,除了更新节点以外,还须要执行挪动节点的操作,将节点挪动到oldChildren中所有未解决节点的最初面。

新前与旧后

如果新前与旧后是同一个节点,因为地位不同,除了更新节点以外,还须要执行挪动节点的操作,将节点挪动到oldChildren中所有未解决节点的最后面。

如果后面这4种形式比照之后都没有找到雷同的节点,这时再通过循环的形式去oldChildren中具体找一圈。

哪些节点是未解决过的

因为咱们的逻辑都是在循环体内解决的,所以只有让循环条件保障只有未解决过的节点能力进入循环体内即可。

一个失常的循环都能实现这个成果,然而因为咱们的优化策略,节点是有可能从前面比照的,比照胜利就会进行更新解决。也就是说,循环不再是只解决所以未解决过的节点的第一个,有可能会解决最初一个,这种状况下就不能从前往后循环,而应该是从两边向两头循环。

那么,怎么实现从两边向两头循环呢?

首先,咱们先筹备4个变量:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。在循环体内,每解决一个节点,就将下标向指定的方向挪动一个地位。通常状况下是对新旧两个节点进行更新操作,就相当于一次性解决两个节点,将新旧两个节点的下标都向指定方向挪动一个地位。

开始地位所示意的节点被解决后,就向后挪动一个地位;完结地位的节点被解决后,则向前挪动一个地位。也就是说,oldStartIdx和newStartIdx只能向后挪动,而oldEndIdx和newEndIdx只能向前挪动。

当开始地位大于等于完结地位,阐明所以节点都遍历过了,则完结循环。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {  // 做点什么}

你可能会发现,无论newChildren或者oldChildren,只有有一个循环结束,就会退出循环。那么,当新子节点和旧子节点的节点数量不统一时,会导致循环完结后依然有未解决的节点,也就是说这个循环不须要笼罩所有节点。

因为如果oldChildren先循环结束,这个时候如果newChildren中还有残余节点,那么阐明这些节点都是新增的节点,间接把这些节点插入DOM中就行了。

如果newChildren先循环结束,如果oldChildren还有残余的节点,阐明这些节点都是被废除的节点,将这些节点从DOM中移除即可。

在patch中,还有一部分逻辑是建设key和index索引的对应关系。在Vue.js的模板中,渲染列表时能够为节点设置一个属性key,这个属性能够标示一个节点的惟一ID。在更新子节点时,须要在oldChildren中循环去找一个节点。如果咱们为子节点设置了属性key,建设了key和index索引的对应关系,就生成了一个key对应着一个节点下标这样一个对象。那么在oldChildren中找雷同节点时,能够间接通过key拿到下标,从而获取节点,基本不须要通过循环来查找节点。

残缺代码 (https://github.com/mfaying/si...

参考

《深入浅出Vue.js》