vue源码阅读四:虚拟DOM是如何渲染成真实的DOM的?(上)

1,136 阅读4分钟

上一篇:vue源码阅读三:虚拟 DOM 是如何生成的?(下)

前言

前面用了两篇文章,讲虚拟 DOM 是如何生成的。终于到了如何将虚拟 DOM 渲染成真实 DOM 的部分了。
回顾下之前的mountComponent 函数,中间有行代码如下:

vm._update(vm._render(), hydrating)

vm._render(),我们已经知道是如何生成虚拟 DOM 的了。接下来,我们看看vm._update是如何将虚拟 DOM 渲染成真实的 DOM的。

_update

_update 的主要代码如下:

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ...
    if (!prevVnode) {
      // 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    ...
  }

可以看到,_update 主要调用了__patch__ 方法。暂时还没有看过diff算法,就先分析首次的渲染吧。
_update主要是调用了__patch__方法,并将该方法所返回的结果,覆盖之前的vm.$el

__patch__

先找到__patch__方法在哪里定义的。找到之后,就是下面这样:

Vue.prototype.__patch__ = inBrowser ? patch : noop

这段很好理解,当前环境是在浏览器中时,将patch 赋值给 __patch__
这时,我们不仅又要问 patch 又是什么呢?

export const patch: Function = createPatchFunction({ nodeOps, modules })

可以看到,patch 就是 createPatchFunction 方法。
所以,_update 实际上相当于是调用了createPatchFunction 方法来生成真实的DOM。需要注意的地方是{ nodeOps, modules }分别是什么。

  • nodeOps:封装了一些原生DOM操作方法。像DOM的创建、插入、移除等都是封装在这里面的。

  • modules:封装了对原生DOM的特性进行操作的方法。如class/styles 的添加、更新等等。

  • createPatchFunction

  export function createPatchFunction(backend) {
    ...
    return function patch(oldVnode, vnode, hydrating, removeOnly) {
      const isRealElement = isDef(oldVnode.nodeType)
      // 是真实的 DOM 时,则将真实的 DOM 转为虚拟 DOM
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode)
      }
      const oldElm = oldVnode.elm
      // 获取旧节点的父元素
      const parentElm = nodeOps.parentNode(oldElm)
      // 创建真实 DOM
      createElm(
        vnode, // 新的虚拟节点
        insertedVnodeQueue, 
        oldElm._leaveCb ? null : parentElm, // 要插入的父节点
        nodeOps.nextSibling(oldElm) // 下一个节点
      )
      // 返回真实的 DOM,代替之前的 vm.$el
      return vnode.elm
    }
  }

在第一次的渲染时,旧的虚拟节点oldVnode就是vm.$el,是真实的DOM,会被转为对应的虚拟DOM。然后在获取到oldVnode的父元素之后,将新的虚拟节点vnode转为真实的DOM

createElm

  function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
   ...
    // 创建组件节点
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    // 是元素节点
    if (isDef(tag)) {
      // 先创建最外层的元素
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)
      // 创建最外层元素内部的节点
      createChildren(vnode, children, insertedVnodeQueue)
      // 将创建好的元素插入到父节点
      insert(parentElm, vnode.elm, refElm)
    } else if (isTrue(vnode.isComment)) {
      // 注释节点
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      // 文本节点
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
    ...
  }

生成真实的DOM主要是这个函数,分为创建组件节点和创建普通节点。

  • 创建普通节点

创建普通节点又分为创建元素节点、注释节点和文本节点。

  • 元素节点
  // 创建元素节点
  if (isDef(tag)) {
    // 先创建最外层的元素
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
     : nodeOps.createElement(tag, vnode)
    setScope(vnode)
    // 创建最外层元素内部的节点
    createChildren(vnode, children, insertedVnodeQueue)
    // 将创建好的元素插入到父节点
    insert(parentElm, vnode.elm, refElm)
  }
  
  ----------------------------------------------------
  // createChildren 方法
  function createChildren(vnode, children, insertedVnodeQueue) {
    // 数组的话,遍历 children,然后递归调用 createElm 方法
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children)
      }
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      // 文本节点类型, 如 string/number/symbol/boolean,创建并插入到父节点
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }
  -----------------------------------------------------
  // insert 方法
  function insert(parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

在创建时,先创建最外层的元素,而后创建外层元素内部的元素,如果内部元素是数组组成的虚拟DOM,则递归遍历数组。如果内部元素是文本元素类型(如:string/number/boolean/symbol),则创建文本节点,然后插入到外层元素下面。最后在将外层元素插入到父节点下。

  • 注释节点
else if (isTrue(vnode.isComment)) {
  // 注释节点
  vnode.elm = nodeOps.createComment(vnode.text)
  insert(parentElm, vnode.elm, refElm)
}

创建注释节点,比较简单。直接创建注释节点,然后插入到父元素下。

  • 文本节点
else {
  // 文本节点
  vnode.elm = nodeOps.createTextNode(vnode.text)
  insert(parentElm, vnode.elm, refElm)
}

其他情况下,均是创建文本节点,然后插入到父元素下。
整个流程如下:

还有一部分是创建组件节点。这个我们放到下一节将。

总结

  • 在首次渲染时,将虚拟DOM转为真实的DOMvm._update 会调用 __patch__方法,而 patch 方法实际上则是封装了 createPatchFunction方法。
  • createPatchFunction 方法中,先获取旧节点的父元素,然后将虚拟DOM转为真实DOM,插入到旧节点的父元素下。
  • 在将虚拟DOM转为真实DOM时,又将虚拟DOM分为组件类型的虚拟DOM、和普通的虚拟DOM。不同类型的DOM,处理方式也不一样。组件类型的,我们放在下节单独讲。
  • 而普通的虚拟DOM,处理方式如上面的流程图所示。