Vue.js 源码剖析 - 虚拟 DOM

1,163 阅读4分钟

什么是虚拟 DOM

  • 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述真实 DOM
  • Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性
    • 例如:指令 和 组件机制 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对 象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射 到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组 件机制。

Vue 1.x 中细粒度监测数据的变化,每一个属性对应一个 watcher,开销太大Vue 2.x 中每个组件对应一 个 watcher,状态变化通知到组件,再引入虚拟 DOM 进行比对和渲染。

为什么要使用虚拟 DOM

  • 使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如何操作 DOM以及DOM的浏览器兼容问题,从而提高开发效率
  • 作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR(服务端渲染)、Weex(跨移动端平台)
  • 虚拟 DOM不一定可以提供性能
    • 首次渲染的时候回增加开销,因为要维护一层额外的虚拟 DOM
    • 复杂视图下可提升渲染性能 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM, 如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM 之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM

Vue.js 中的虚拟 DOM

  • render 中的 h 函数用法
const vm = new Vue({
  el: '#app',
  render (h) {
    // h(tag, data, children)
    // return h('h1', this.msg)
    // return h('h1', { domProps: { innerHTML: this.msg } }) 
    // return h('h1', { attrs: { id: 'title' } }, this.msg) 
    const vnode = h(
      'h1', 
      {
        attrs: { id: 'title' }
      },
      this.msg 
    )
    console.log(vnode)
    return vnode
  },
  data: {
    msg: 'Hello Vue'
  } 
})
  • h 函数 vm.$createElement(tag, data, children, normalizeChildren)

    • tag 标签名称或组件对象
    • data 描述tag,可以设置 DOM 的属性或者标签的属性
    • children tag中的文本内容或者子节点
  • h函数返回结果 - VNode对象 核心特性

    • tag
    • data
    • children
    • text
    • elm(真实DOM)
    • key

VNode 创建过程

createElement

功能

createElement() 函数,用来创建虚拟节点 (VNode),我们的 render 函数中的参数 h,就是 createElement()

render(h) {
  // 此处的 h 就是 vm.$createElement return   h('h1', this.msg)
}

定义

在 vm._render() 中调用了用户传递的或者编译生成的 render 函数,这个时候传递了 createElement

  • src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

vm.cvm.$createElement 内部都调用了createElement,不同的是 vm.c 在编译生成的 render 函数内部会调用,vm.$createElement 在用户传入的 render 函数内部调用。

当用户传入 render 函数的时候,要对用户传入的参数做处理

  • src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 判断第三个参数
  // 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制 
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

在_createElement函数中创建VNode对象

执行完 createElement 之后创建好了 VNode,把创建好的 VNode 传递给 vm._update() 继续处理

update

功能

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

_update方法作用是把VNode渲染成真实DOM,首次渲染会调用,数据更新会调用

内部调用 vm.patch() 把虚拟 DOM 转换成真实 DOM

定义

  • src/core/instance/lifecycle.js

patch 函数初始化

功能

对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成 VNode

Snabbdom 中 patch 函数的初始化

  • src/snabbdom.ts
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
  }
}

Vue.js 中 patch 函数的初始化

  • src/platforms/web/runtime/index.js
import { patch } from './patch' 
Vue.prototype.__patch__ = inBrowser ? patch : noop
  • src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch' 
import baseModules from 'core/vdom/modules/index' 
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all 
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

// nodeOps 操作dom的API
// modules 操作属性样式事件、指令、ref等
export const patch: Function = createPatchFunction({ nodeOps, modules })
  • src/core/vdom/patch.js 函数柯里化
export function createPatchFunction (backend) {
  ......
  ......
  ......
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
  } 
}

patch 函数执行过程

createElm

把 VNode 转换成真实 DOM,插入到 DOM 树上

patchVnode

updateChildren

updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致。它处理过程中 key 的作用,在 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判 断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。