vlambda博客
学习文章列表

vue中Virtual DOM源码学习(简书原文转移)

vue升级到2.0之后就加入了Virtual DOM,对于Virtual DOM的概念这里就不做过多的说明了。本文主要分析一下vue中Virtual DOM渲染到真实DOM是如何实现的。

最近我在研究关于markdown解析相关的东西,在Github上找到相关的解析器基本都是会把markdown解析为字符串的方式。在参考了多个开源库之后选择了marked[1]作为解析库去解析markdown。选择marked的原因是因为marked相对与其他库来说代码量相对较少,一个人比较容易搞定,并且解析能力与效率都是很不错的,并且star数量也比较高,所以代码质量也是非常高的。

在编写markdown解析的时候,第一目的就是把markdown解析为vnode,然后再渲染到真实DOM。所以自己实现解析markdown部分就是基于marked实现的,在解析完成之后就得到了vnode的树形结构。

目前解析为vnode基本已经实现,但很多地方还需要进行优化。目前最需要解决的就是把vnode渲染到真实DOM中去,所以就选择vue中的Virtual DOM作为基础去研究。其实写这篇文章也是为自己写vnode的render方法整理思路,所以这也是为什么标题叫做学习而不是分析了。对于文章中错误的还望给我指正。

vue中的vnode的数据结构

vue的vnode代码部分位于项目的src/core/vdom文件夹下,vue的Virtual DOM是基于snabbdom[2]修改的,vnode类的数据结构如下

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 functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node 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;
constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.functionalContext = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false }
// DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance }}

vnode是如何渲染到Real DOM的

vue中负责把vnode渲染到Real DOM主要工作的是位于src/core/vdom/patch.js文件中的代码完成的 下面我们就来看看patch.js中都做了哪些工作呢,这里可以结合源码来看https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

在阅读源码的时候一般的顺序都是看看这个文件都依赖了哪些文件,其次也是最重要的,文件导出了什么东西,导出的就是文件的核心部分了,也就是阅读源码的入口。

在文件顶部就可以看到文件的依赖了,这里引入的文件如下(只写了重要模块的作用)

import VNode from './vnode' // vnode类import config from '../config' // vue的全局配置,包括运行环境的检测import { SSR_ATTR } from 'shared/constants'import { registerRef } from './modules/ref'import { activeInstance } from '../instance/lifecycle' 
import { warn, // 打印警告信息 isDef, // 判断值不为undefined和null isUndef, // 判断值为undefined或null isTrue, makeMap, // 把字符串按``,``分割为数组 isPrimitive // 判断值是否为string、number或boolean} from '../util/index' // 一些工具函数

导出模块一共有两个地方

1.位于[30行](https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js#L30)的导出了一个空的vnode对象2.位于70行[3]的生成主patch函数的函数

可以看到最重要的函数是createPatchFunction,createPatchFunction接受一个参数,并返回了函数patch。patch函数接受6个参数:

oldVnode: 旧的虚拟节点或旧的真实dom节点vnode: 新的虚拟节点hydrating: 是否要跟真实dom混合removeOnly: 特殊flag,用于 组件 parentElm:父节点refElm: 新节点将插入到refElm之前

patch函数的主要思想:

1.如果vnode不存在但oldVnode存在,则表示要移除旧的node,那么就调用invokeDestroyHook(oldVnode)来进行销毁2.如果oldVnode不存在但是vnode存在,说明是要创建新节点,那么就调用createElm来创建新节点3.当vnode和oldVnode都存在时:

1.如果oldVnode与Vnode是同一节点是就调用patchVnode处理去比较两个节点的差异2.当vnode和oldVnode不是同一个节点时,如果oldVnode是真实DOM节点或hydrating设置为true,需要用hydrate函数将虚拟DOM和真实DOM进行映射,然后将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点,根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置


patchVnode函数主要思路:

1.如果vnode===oldVnode则直接返回,不执行任何操作2.如果oldVnode跟vnode都是静态节点,且具有相同的key,当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作3.如果vnode不是text节点

1.如果oldVnode与vnode都有子节点,并且子节点不相等,就调用updateChildren执行更新子节点操作2.oldVnode没有子节点,vnode有子节点,则创建节点3.oldVnode有子节点,vnode没有子节点,就移除旧的节点4.如果oldVnode为text节点,就移除文本节点

4.vnode为text节点就设置节点文本内容


updateChildren函数实现功能:

1.分别获取oldVnode和vnode的第一个和最后一个节点,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx分别为oldVnode与vnode的子节点的下标,和最后一个子节点的下标,然后在while循环中执行比较,并且移动oldStartIdx和newStartIdx,直到oldStartIdx大于oldEndIdx或者newStartIdx大于newEndIdx2.假如没有oldStartVnode就将oldStartIdx加1,并重新求得oldStartVnode,进入下一次循环3.假如没有oldEndVnode就将oldEndIdx减1,并重新求得oldEndVnode,进入下一次循环4.假如oldStartVnode和newStartVnode是相同类型的节点,就调用patchVnode去比较两个节点,并使oldStartIdx和newStartIdx都加1,同时开始节点也更新对应下标的节点5.假如oldEndVnode和newEndVnode是同类型节点,就调用patchVnode去比较两个节点,并使oldEndIdx和newEndIdx都减去1,同时开始节点也更新对应下标的节点6.假如oldStartVnode和newEndVnode是同类型节点,就调用patchVnode去比较两个节点,如果removeOnly是false,那么可以把oldStartVnode.elm移动到oldEndVnode.elm之后,并使oldStartIdx加1,newEndIdx减去1,同时更新对应节点为最新的节点7.假如oldEndVnode和newStartVnode是同类型的节点,就调用patchVnode去比较两个节点,如果removeOnly是false,那么可以把oldEndVnode.elm移动到oldStartVnode.elm之前,并使oldEndIdx减1,newStartIdx加1,同时更新对应节点为最新的节点8.如果以上条件都不匹配,则查找oldVnode中与vnode具有相同key的节点,并将查找的结果赋值给elmToMove。

如果找不到相同key的节点,则表示是新创建的节点如果找到了,就判断这两个节点是否为同一类型的节点

1.若为同一类型就调用patchVnode,就将对应下标处的oldVnode设置为undefined,如果removeOnly是false,就把elmToMove.elm插入到oldStartVnode.elm之前,newStartIdx加1,且把newStartVnode设置为下一个节点2.如果没有找到就直接创建新的节点,并执行newStartVnode = newCh[++newStartIdx]-


9.循环结束后,如果oldStartIdx > oldEndIdx,就把vnode中间没有循环到的节点添加到新DOM中10.如果newStartIdx > newEndIdx,就把oldVnode中没有遍历到的节点从DOM中移除


至此,vue中实现VDOM至真实DOM的就基本讲解完成了,至于生命周期,在这里没有提及,只要在对应的地方加入生命周期回调就ok了

最后成果

由于水平有限,文章中有不正确的地方还望原谅与指正,谢谢!

References

[1] marked: https://github.com/chjj/marked
[2] snabbdom: https://github.com/snabbdom/snabbdom
[3] 70行: https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js#L71
[4] Vue原理解析之Virtual Dom: https://segmentfault.com/a/1190000008291645