# 第二篇 虚拟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)来描述状态与视图之间的映射关系。

流程如下:

    1. 将 template 编译为渲染函数(render 函数)
    1. 执行渲染函数生成虚拟节点(vnode,就是 js 对象)
    1. 使用虚拟节点更新视图(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,从而只针对发生了变化的节点进行更新。

下面来看具体的比对、更新逻辑:

上次更新: 2023/3/14 00:20:52