vlambda博客
学习文章列表

Vue 进阶系列丨虚拟DOM和VNode

Vue 进阶系列教程将在本号持续发布, 一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!



2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。 Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!




什么是虚拟DOM

作为一个前端,想必或多或少都听说过虚拟 DOM 这个名词,那么虚拟 DOM到底是什么呢?

在 web 早期,页面的交互效果还是比较简单的,不需要频繁的去操作DOM,使用 Jquery 完全可以满足我们的需求。但是随着时代的发展,页面上的东西越来越多,需求也越来越复杂,相应的程序中需要维护的状态也越来越多,我们就要不断的去操作 DOM。此时命令式编程的 Jquery 就显得很臃肿,后期维护困难。

三大框架的诞生就是为了解决这一问题,声明式操作 DOM,我们只需要描述状态和DOM之间的映射关系,就可以将状态渲染成视图。从状态到视图的过程,框架会帮我们做,不需要我们手动去操作DOM。

当状态发生变化时,需要重新渲染页面,也就是框架要去帮助我们操作DOM,那么框架怎么去确定状态发生了什么变化以及 框架要去哪里更新DOM 呢?最简单的做法就是只要有一个状态发生了变化,页面里全部的DOM 元素都重新渲染一遍,但是访问 DOM 的操作是比较费时的,如果按照这么做的话,会造成相当大的浪费。那么 Vue.js 是怎么做的呢?

Vue.js 在渲染页面之前会先根据状态生成一份虚拟 DOM 树,然后使用虚拟DOM 树进行渲染,在渲染之前,会使用新生成的虚拟 DOM 树和上一次生成的虚拟 DOM 树进行对比,只渲染不同的部分。



Vue的虚拟DOM

在 Vue.js1.0 的时候,Vue 并没有引用虚拟 DOM,当时的做法是当状态发生变化是,Vue 已经知道了是哪个节点使用了这个状态,然后将这个节点进行更新操作,根本不需要对比操作。

但是这样的缺点是,对于每一个状态都有一个依赖跟踪器,粒度太细,这样就会有一些内存开销和依赖追踪的开销,当状态被越来越多的节点使用的时候,开销就会非常大。

Vue2.0 的时候选择了一个中等粒度,当状态发生变化的时候,只在组件级别有一个依赖跟踪器,当组件内的状态改变的时候,只能通知到组件层次,组件内部通过虚拟 DOM 进行比对与渲染。Vue 之所以能随意的切换绑定的粒度,本质上还要归功于变化侦测,关于变化侦测的相关知识请移步。

Vue 使用模板来描述状态与 DOM 之间的映射关系,通过编译将模板转换成渲染函数,执行渲染函数生成虚拟节点树(vnode),使用虚拟节点数就可以渲染页面了。当状态发生变化的时候,直接进行 DOM 操作会比较慢,所以首先会先生成虚拟节点树,将新的节点树和旧的节点树进行对比,然后再操作 DOM,更新页面。

所以,虚拟 DOM 主要做了两件事:
  • 提供与真实 DOM 节点所对应的虚拟节点 vnode

  • 将新的虚拟节点和旧的虚拟节点进行对比,然后更新页面




什么是 VNode

VNode 是 Vue.js 的一个类,使用它可以实例化不同的 vnode 实例,我们只需要控制传入的参数的不同就可以了。
class VNode { constructor ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag; this.data = data; this.children = children; this.text = text; this.elm = elm; this.ns = undefined; this.context = context; this.fnContext = undefined; this.fnOptions = undefined; this.fnScopeId = 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; } get child () { return this.componentInstance }}

我们可以看到 vnode 仅仅只是 VNODE 类的一个实例而已,通过传入的参数不同,就可以生成不同类型的 vnode 实例对象了。vnode 可以理解成节点描述对象,它描述了应该怎样去创建真实的 DOM 节点。



VNode 的种类

  • 注释节点

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


我们可以看到注释节点只有两个有效属性,text 和 isComment,其余属性都是默认的 undefined 或者 false。


例如,一个真实的注释节点:

<!-- 注释节点 -->


所对应的 vnode 是这样的:

{ text:'注释节点',  isComment:true}



  • 文本节点

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


文本节点只有一个 text 属性,它的 vnode 是这样的:

{  text:'我是文本内容',}



  • 克隆节点

function cloneVNode (vnode) { const cloned = new VNode( vnode.tag,    vnode.data, vnode.children && vnode.children.slice(), 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.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.asyncMeta = vnode.asyncMeta; cloned.isCloned = true; return cloned}


克隆节点就是将一个节点的内容全部复制到新的节点中。主要用来优化静态节点和插槽节点。当静态节点需要重新渲染的时候,并不需要重新再次走一遍渲染函数,然后重新生成 vnode,只需要使用克隆节点的方法将之前的 vnode 克隆一份即可,一定程度上优化了程序的性能。


克隆节点和被克隆节点的唯一区别是 isCloned 属性,克隆节点的 isCloned为 true,被克隆的原始节点的 isCloned 为 false。



  • 元素节点


元素节点一般包含 4 个有效属性:

  1. tag:就是节点的名称,例如 div、p、img

  2. data:该属性包含一些节点上的数据,例如 class、style

  3. children:该节点的子节点列表

  4. context:当前组件的 vue.js 实例


通常,函数式组件的 vnode 是这样子的:

{  children:[VNode,VNode],  context:{...},  data:{...},  tag:{...}}



  • 组件节点


和元素节点类似,有两个独有的属性 componentInstance 和 componentOptions。


通常,函数式组件的 vnode 是这样子的:

{  componentInstance:{...},  componentOptions:{...}, context:{...}, data:{...}, tag:{...}}


  • 函数式组件


和组件节点类似,有两个独有的属性 functionalContext 和 functionalOptions。


通常,函数式组件的 vnode 是这样子的:

{ functionalContext :{...}, functionalOptions:{...}, context:{...}, data:{...}, tag:{...}}




Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉
HFun 前端攻城狮

往期精彩: