# 虚拟 DOM 如何转化为真实 DOM

上节讲解了 render 函数的生成,调用 render 函数之后,可以得到模版对应的虚拟 DOM。但虚拟 DOM 只是一个抽象出来的 JavaScript 对象,我们要将其渲染到页面上,必须将其转化为真实的 DOM 结构,然后再挂载到指定的位置上。在源码中,调用 render 函数之后获得的 VNode 会继续传递给 update 函数,执行 update 函数,就可以实现虚拟 DOM 向真实 DOM 的转化。本篇文章就来简略分析一下,Vue2 中是如何将虚拟 DOM 转化为真实 DOM 的。源码项目地址:Nwu23787/mini-Vue2: 手写 Vue2 核心源码 (github.com) (opens new window)

# 1、patch

大名鼎鼎的 patch 方法,相信大家都并不陌生,update 函数内部其实就是调用了 patch 函数。patch 函数是 Vue 核心源码中非常重要的一个函数,他主要有两个功能:

  1. 初次渲染时,将虚拟 DOM 转换为真实 DOM,然后将真实 DOM 挂载到页面上对应的位置,替换该位置原有的 DOM 结构。
  2. 在后续 DOM 更新时,对比新旧 VNode,通过 diff 算法找出两者的差异,进行 DOM 的更新。

本文主要讨论第一种情况,有关 DOM 更新和 diff 算法的内容将在后续博客中介绍。

patch 函数如何区分上面的两种情况呢,主要是靠传入的参数来确定的,具体来说,就是由第一个参数的类型决定的。patch 函数接收两个参数:

  1. 如果第一个参数为真实的 DOM 对象(一般来说是我们传入的 el),第二个参数为 VNode,则说明要执行初渲染流程,那么就根据第二个参数,生成真实 DOM,替换第一个 DOM 对象。
  2. 如果第一个参数 和 第二个参数都为 VNode,则通过 diff 算法,比较两者 VNode差异,更新真实 DOM。

下面看一下 patch 函数在初渲染部分的实现:

/**
 * 接收两个参数
 * 1. 初渲染:第一个参数为真实的 DOM 对象,第二个参数为 VNode,则根据第二个参数,生成真实 DOM,替换第一个 DOM 对象
 * 2. 更新:第一个参数 和 第二个参数都为 VNode,则比较两者 vnode差异,更新真实 dom
 * @param {Object} oldVnode 一个 VNode 或者是一个真实的 DOM 对象
 * @param {Object} newVnode 新的 VNode
 */
function patch(oldVnode, newVnode) {
    const isRealElement = oldVnode.nodeType

    if (isRealElement) {
        // 对象上有 nodeType 属性,则为真实 DOM
        const elm = oldVnode

        const parentElm = elm.parentNode // 获取到老节点的父节点,便于后面删除和新增(即替换)操作

        // 创建真实 dom
        let newElm = createElm(newVnode)
        console.log(newElm);

        // 先把新 DOM 插入到老DOM的后面,然后再删除老DOM,这样可以保证新DOM替换了老DOM
        parentElm.insertBefore(newElm, elm.nextSibling)
        parentElm.removeChild(elm)

        // return newElm
    } else {
        // 进行 diff 算法,更新
    }
}

patch 函数在初渲染流程主要分为以下几步:

  1. 通过 parentNode 属性,获取到真实 DOM(也就是第一个参数)的父节点
  2. 通过 createElm 函数,将 VNode 转化为真实 DOM
  3. 将生成的真实 DOM 插入到旧的 DOM 之后
  4. 将旧的 DOM 从父节点中删除,完成新旧 DOM 的替换

后面两步主要是为了保证新的 DOM 一定会取代原有的旧的 DOM 在文档中的位置。

# 2、createElm

这个函数是 Vue2 中真正负责将 VNode 转化为真实 DOM 的函数。其只接收一个参数,就是虚拟 DOM。在内部对虚拟 DOM 进行递归操作,通过 VNode 节点的 tag 属性,来判断该节点是文本节点还是元素节点。如果是文本节点,则使用 JavaScript 原生的 createTextNode 方法来生成文本节点。如果是元素节点,就调用 JavaScript 原生的 createElement 方法,生成一个元素节点。然后再递归遍历该 VNode 的所有子节点,递归执行 createElm 方法,最后将传入的 VNode 转化为真实 DOM 返回。

createElm 函数源码:

// 创建真实DOM
function createElm(vnode) {
    // 将 VNode 解构
    let { tag, data, children, text } = vnode
    if (typeof tag === 'string') {
        // 传入的是标签,文本节点的tag为undefined
        // 创建元素
        // !!!把真实 DOM 挂载到 虚拟DOM 上!便于后续更新,比如修改了属性,就可以直接找到真实的dom进行更新
        vnode.el = document.createElement(tag)

        // 更新元素属性
        patchProps(vnode.el, data)

        // 创建子DOM
        children.forEach((item) => {
            // 挂载子DOM
            vnode.el.appendChild(createElm(item))
        })
    } else {
        vnode.el = document.createTextNode(text)
    }
    return vnode.el
}