# 第二篇 虚拟DOM
Vue.js 2.0 引入了虚拟 DOM,比 Vue.js 1.0 的初始渲染速度提升了 2-4 倍,并大大降低的内存消耗。
- 什么是虚拟 DOM,虚拟 DOM 的原理是什么?
- 为什么 Vue.js 2.0 开始引入了虚拟 DOM?为什么 Vue.js 2.0 开始引入了虚拟 DOM?
# 5. 虚拟 DOM 简介
# 5.1 什么是虚拟 DOM
在 Web 早期页面的交互效果简单,不太需要频繁操作 DOM,用 jQuery 开发就可以满足需求。
随着时代发展,页面上的功能越来越多,需要实现的需求也越来越复杂,程序中需要维护的状态也越来越多,DOM 操作也越来越频繁。
这样会导致代码中有相当多的代码是在操作 DOM,程序中的状态很难管理,代码逻辑混乱,一堆全局变量,不好维护。这是命令式操作 DOM 的问题,虽然简单易用,但不好维护。
现在主流的前端框架 Vue、React 都是声明式操作 DOM。通过描述状态(data)和 DOM 之间的映射关系是怎样的,就可以将状态渲染成视图(View)。状态到视图的过程,框架会帮我们做,不需要我们自己手动去操作 DOM。
在计算机术语中有 声明式编程 declarative 和 命令式编程 imperative 两种编程模式
- 声明式编程:告诉机器,我要做什么(what),具体怎么做(how)由机器自己决定。
- 命令式编程:告诉机器,具体怎么做(how),机器不会管你具体要做什么(what)。
状态可以是 JS 中的任意类型,Object、Array、String、Number、Boolean 等都可以作为状态。
渲染的过程:将状态作为输入,并生成 DOM 输出到页面上显示出来,这个过程叫渲染。
状态 => DOM
通常程序在运行过程中,状态会不断变化(比如 Ajax 异步请求),而每次状态变化都需要重新渲染。如何确定状态中发生了什么变化,以及需要在哪里更新 DOM?
- 最简单粗暴的解决方式是,把全部 DOM 删了,然后重新根据状态生成 DOM, 显示到页面。
- 这样会带来比较大的开销,照成相当多的性能浪费,因为通常状态只有部分节点需要重新渲染。
- 最好的方式是找到具体需要更新的 DOM,尽可能减少操作 DOM,当状态发生变化时,只更新与这个状态有关联的 DOM。
这个问题有很多种解决方案:
- Angular 中是脏检查
- React 中是使用虚拟 DOM
- Vue.js 1.0 中是通过细粒度的绑定。
虚拟 DOM 只是众多解决方案中的一种,可以用,但并不是必须要用。
虚拟 DOM(Virtual DOM)的解决方式是:
根据状态(data)生成一个虚拟节点树(Virtual Node树) => 使用虚拟节点树进行渲染
在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行比对,只渲染不同的部分
虚拟节点树一般简写为 vnode 树
# 5.2 Vue 2.0 为什么要引入虚拟 DOM
Angular 和 React 的变化侦测有一个共同点,就是不知道哪些状态(data) 变了,就需要进行比较暴力的比对。
- React 是通过虚拟 DOM 进行比对
- Angular 是使用脏检查的流程
Vue 的变化侦测和他们不一样,Vue 可以具体知道是哪些状态发生了变化,完全可以进行细粒度的更新。
这也是 Vue 1.0 的实现。但有个缺点,粒度太细,每一个绑定都会有一个对应的 Watcher 实例来观察状态的变化,如果项目很大,状态会非常多,导致内存开销很大。
Vue 2.0 选择了中等粒度的解决方案,就是引入虚拟 DOM。
- 组件级别是一个 Watcher 实例,即就算组件内有 10 个节点使用了某个状态,其实只有一个 watcher 在观察这个状态的变化。
- 这个状态发生变化后,只能通知到组件。组件内部在通过虚拟 DOM 进行比对与渲染,这是比较折中的方案。
Vue 之所以能随意调整绑定的粒度,本质上还要归功于前面章节中讲到的变化侦测。
# 5.3 Vue 中的虚拟 DOM
Vue 中通过模板(template)来描述状态与视图之间的映射关系。
流程如下:
- 将 template 编译为渲染函数(render 函数)
- 执行渲染函数生成虚拟节点(vnode,就是 js 对象)
- 使用虚拟节点更新视图(view)
模板转换为视图过程:
模板 ==编译==> 渲染函数 ==执行==> [vnode ==patch==> 视图](虚拟DOM)
虚拟 DOM 在 Vue 中所做的事 - 提供虚拟节点 vnode 和对新旧两个 vnode 进行比对,并根据比对结果进行 DOM 操作更新视图
虚拟 dom 执行流程
vnode ====> [vnode ==patch/diff==> oldVnode](虚拟DOM) ==> 视图
# 6. VNode
VNode 就是 Virtual node(虚拟节点),在 Vue 中,VNode 是一个类,可以用来创建不同类型的 vnode 实例。
vnode 本质上就是一个 js 对象,用来描述 dom 节点。不同类型的 vnode 实例,表示不能类型的 DOM 元素。
export default class VNode {
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 // rendered in this component's scope
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined // component instance
this.parent = undefined // component placeholder node
this.raw = false // contains raw HTML? (server only)
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false // is a v-once node?
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
}
}
渲染视图的过程是:先创建 vnode,再使用 vnode 生成真实 dom,最后插入到页面渲染视图
vnode ==create==> DOM ==insert==> 视图
VNode 的作用:
- 在每次渲染视图时都先创建 vnode 并缓存,当下次状态变化重新渲染时,生成新的 vnode,并与上次缓存的 vnode 比较,再找出差异,基于此再去修改真实的 DOM。
# VNode 类型
vnode 类型有以下 6 种:
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件
- 克隆节点
前面提到 vnode 是一个 js 对象,不同类型的 vnode 之间只是属性不同。无效属性默认会赋值为 undefined 或 false.
来看看不同类型的 vnode 都有哪些有效属性。
1、注释节点
export const createEmptyVNode = text => {
const node = new VNode)_
node.text = text
node.isComment = true
return node
}
// <!-- 注释节点 -->
// 实例值
{
text: "注释节点",
isComment: true
}
2、文本节点
export function createTextVNode(text) {
return new VNode(undefined, undefined, undefined, String(val))
}
// "文本节点"
// 实例值
{
text: "文本节点"
}
3、克隆节点:复制一个节点内容,唯一的区别在于 isCloned 属性为 true
主要用于优化静态节点和 slot node,比如对于静态节点,只需要首次显示时,使用渲染函数生成 vnode,后面再次渲染时,不需要再执行 render 函数生成 vnode,而是对原先的 vnode 进行 copy
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
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
}
4、元素节点
- tag:节点名称(标签名称),比如 p、ul、li、 div 等
- data:包含了一些节点上的数据,比如 attrs、class 和 style 等
- children:当前子节点列表。
- context: 当前组件的 Vue.js 实例
// <p><span>Hello</span><span>zuo</span></p>
{
children: [Vnode, Vnode],
context: {...},
data: {...},
tag: 'p',
...
}
5、组件节点:组件节点和元素节点类似,有以下两个独有属性
- componentOptions:组件节点的选项参数,包含 propsData、tag 和 children 信息。
- componentInstance:组件的实例,也是 Vue.js 实例,Vue 中每个组件都是一个 Vue.js 实例
// <child></child>
{
componentInstance: {...},
componentOptions: {...},
context: {...},
data: {...},
tag: 'vue-component-1-child',
// ...
}
6、函数式组件:函数式组件和组件节点类似,有两个独有的属性 functionalContext, functionalOptions
{
componentInstance: {...},
componentOptions: {...},
context: {...},
data: {...},
tag: 'div'
}
# 7. patch
/pætʃ/ 直译为 "补丁"
虚拟 DOM 最核心的部分是 patch
通过 patch 可以比对新旧两个虚拟 DOM,从而只针对发生了变化的节点进行更新。
下面来看具体的比对、更新逻辑:
← 第一篇 变化侦测 第三篇 模板编译原理 →