Jeffery

人无远虑、必有近忧

  • 主页
  • 随笔
  • 技术
  • 相册
  • 关于
所有文章 友链 关于我

Jeffery

人无远虑、必有近忧

  • 主页
  • 随笔
  • 技术
  • 相册
  • 关于

聊聊vue2.5的patch过程(diff算法)

阅读数:0次 2019-04-09

文章导航

× 文章目录
  1. 1. 简介
  2. 2. 什么是VNode?
  3. 3. mounted过程都发生了什么?
    1. 3.1.  1)组件实例初始化创建生成DOM
    2. 3.2.  1)组件数据更新时更新DOM
  4. 4. patch原理分析
    1. 4.1. 1、patch逻辑
    2. 4.2. 2、patchVnode逻辑
    3. 4.3. 3、updateChildren逻辑
  5. 5. 小结

简介

Vue2.0开始,引入了Virtual Dom,了解diff过程可以让我们更高效的使用框架,必要时可以进行手工优化,本文针对的是Vue2.5.7版本中的Virtual Dom进行分析,力求以图文并茂的方式来分析diff的过程。

其中patch过程中所用到的diff算法来源于snabbdom

PS: 如有不对之处,还望指正。

什么是VNode?

我们知道,浏览器中真实的DOM节点对象上的属性和方法比较多,如果每次都生成新的DOM对象,对性能是一种浪费,在这种情况下,Virtual Dom出现了,而VNode是用来模拟真实DOM节点,即把真实DOM树抽象成用JavaScript对象构成的抽象树,从而可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象,当数据发生改变时,在改变真实DOM节点之前,会先比较相应的VNode的的数据,如果需要改变,才更新真实DOM,大大提升了性能。同时VNode不依赖平台。

具体可以通过以下代码查看标准DOM对象上的方法和属性

1
2
3
4
const dom = document.createElement('div');
for (let key in dom) {
console.log(key)
}

VNode构造函数具体结构如下(具体见源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node

// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
fnScopeId: ?string; // functional scope id support
}

mounted过程都发生了什么?

在了解patch过程之前,先来大概了解下mounted过程,我们知道,Vue最终会调用$mounted方法来进行挂载。
一般来说,Vue有两条渲染路径,分别对应生命周期中mounted和updated两个钩子函数,分别如下:

 1)组件实例初始化创建生成DOM

在该过程时,初始的Vnode为一个真实的DOM节点或者undefined(创建组件)

$mounted => mountComponent => updateComponent => _render => _update => patch => createElm => nodeOps.insert => removeVnodes

 1)组件数据更新时更新DOM

在该过程,初始化Vnode为之前的prevVnode,不是真实DOM节点

flushSchedulerQueue => watcher.run => watcher.get => updateComponent => _render => _update => patch => patchVnode => updateChildren

其中,_render函数内部则是调用createElement方法将渲染函数转为VNode,而_update函数则是在内部调用patch方法将VNode转化为真实的DOM节点。

createElement和patch过程是一个深度遍历过程,也就是”先子后父”,即先调用子类的mounted或updated钩子方法,在调用父类的该钩子。

附上一张\$mounted流程图:

\$mounted过程

patch原理分析

patch过程也是一个深度遍历过程,比较只会在同层级进行,不会跨层级比较,借用一篇相当经典的文章
React’s diff algorithm中的图,图能很好的解释该过程,如下:

React’s diff algorith
patch接收6个参数,其中两个主要参数是vnode和oldVnode,也就是新旧两个虚拟节点,下面详细介绍下patch过程

1、patch逻辑

1、如果vnode不存在,而oldVnode存在,则调用invodeDestoryHook进行销毁旧的节点

2、如果oldVnode不存在,而vnode存在,则调用createElm创建新的节点

3、如果oldVnode和vnode都存在

 1)如果oldVnode不是真实节点且和vnode是相同节点(调用sameVnode比较),则调用patchVnode进行patch

 2)如果oldVnode是真实DOM节点,则先把真实DOM节点转为Vnode,再调用createElm创建新的DOM节点,并插入到真实的父节点中,同时调用removeVnodes将旧的节点从父节点中移除。

2、patchVnode逻辑

1、如果vnode和oldVnode完全一致,则什么都不做处理,直接返回

2、如果oldVnode和vnode都是静态节点,且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode的elm和oldVnode.children都复制到vnode上即可

3、如果vnode不是文本节点或注释节点

 1)如果vnode的children和oldVnode的children都存在,且不完全相等,则调用updateChildren更新子节点

 2)如果只有vnode存在子节点,则调用addVnodes添加这些子节点

 3)如果只有oldVnode存在子节点,则调用removeVnodes移除这些子节点

 4)如果oldVnode和vnode都不存在子节点,但是oldVnode为文本节点或注释节点,则把oldVnode.elm的文本内容置为空

4、如果vnode是文本节点或注释节点,并且vnode.text和oldVnode.text不相等,则更新oldVnode的文本内容为vnode.text

3、updateChildren逻辑

updateChildren方法主要通过while循环去对比2棵树的子节点来更新dom,通过对比新的来改变旧的,以达到新旧统一的目的。

1、如果oldStartVnode不存在,则将oldStartVnode设置为下一个节点

2、如果oldEndVnode不存在,则将oldEndVnode设置为上一个节点

3、如果oldStartVnode和newStartVnode是同一个节点(sameVnode),则调用patchVnode进行patch重复流程,同时将oldStartVnode和newStartVnode设置为下一个节点

4、如果oldEndVnode和newEndVnode是同一个节点(sameVnode),则调用patchVnode进行patch重复流程,同时将oldEndVnode和newEndVnode设置为上一个节点

5、如果oldStartVnode和newEndVnode是同一个节点(sameVnode),则调用patchVnode进行patch重复流程,同时将oldStartVnode设置为下一个节点,newEndVnode设置为上一个节点,需要对DOM进行移动

6、如果oldEndVnode和newStartVnode是同一个节点(sameVnode),则调用patchVnode进行patch重复流程,同时将oldEndVnode设置为上一个节点,newStartVnode设置为下一个节点,需要对DOM进行移动

7、否则,尝试在oldChildren中查找与newStartVnode具有相同key的节点

 1)如果没有找到,则说明newStartVnode是一个新节点,则调用createElem创建一个新节点,同时将newStartVnode设置为下一个节点

 2)如果找到了具有相同key的节点

  (1)如果找到的节点与newStartVnode是同一个节点(sameVnode),则调用patchVnode进行patch重复流程,同时把newStartVnode.elm移动到oldStartVnode.elm之前,并把newStartVnode设置为下一个节点,需要对DOM进行移动

  (2)否则,调用createElm创建一个新的节点,同时把newStartVnode设置为下一个节点

上述过程中,如果oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,即oldChildren和newChildren节点在遍历过程中如果任意一个的开始索引和结束索引重合,则表明遍历结束。

遍历结束后,还需针对oldChildren和newChildren没有遍历的节点进行处理,分为以下两种情况:

1)如果oldStartIdx大于oldEndIdx,说明newChildren可能还未遍历完,则需要调用addVnodes添加newStartIdx到newEndIdx之间的节点

2)如果newStartIdx大于newEndIdx,说明oldChildren可能还未遍历完,则需要调用removeVnodes移除oldStartIdx到oldEndIdx之间的节点

附上一张流程图:

patch过程
针对以上过程,对其中的各个情况都分别简单举个例子,进行分析,可以自行debugger

情况一:oldStartVnode和newStartVnode是相同节点

情况一
情况二:oldEndVnode和newEndVnode是相同节点

情况二
情况三:oldStartVnode和newEndVnode是相同节点

情况三
情况四:oldEndVnode和newStartVnode是相同节点

情况四
情况五:oldStartVnode、oldEndVnode、newStartVnode和newEndVnode都不是相同节点

情况五
附上总图:

总图

小结

1、不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。

2、diff的遍历过程中,只要是对dom进行的操作都调用nodeOps.insertBefore,nodeOps.insertBefore只是原生insertBefore的简单封装。

比较分为两种,一种是有vnode.key的,一种是没有的。但这两种比较对真实dom的操作是一致的。

3、对于与sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的情况,不需要对dom进行移动。

赏

谢谢你请我吃糖果

  • vue
  • vnode
  • patch
  • diff

扫一扫,分享到微信

微信分享二维码
常见的Web安全及其攻防姿势
  1. 1. 简介
  2. 2. 什么是VNode?
  3. 3. mounted过程都发生了什么?
    1. 3.1.  1)组件实例初始化创建生成DOM
    2. 3.2.  1)组件数据更新时更新DOM
  4. 4. patch原理分析
    1. 4.1. 1、patch逻辑
    2. 4.2. 2、patchVnode逻辑
    3. 4.3. 3、updateChildren逻辑
  5. 5. 小结
© 2019 Jeffery
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链
  • 关于我

tag:

  • cli
  • javascript
  • nodejs
  • design
  • 设计模式
  • readline
  • vue
  • path
  • template
  • AST
  • 源码
  • 插件
  • 轮播
  • 滚动
  • welcome
  • intoduction
  • web
  • web安全
  • vnode
  • patch
  • diff
  • 正则表达式
  • 量词
  • 贪婪模式
  • 惰性模式
  • 苏州
  • 旅行
  • 风景
  • segmentfault
很惭愧<br><br>只做了一点微小的工作<br>谢谢大家