Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yangrenmu opened this issue Dec 27, 2019 · 0 comments
Labels

Comments

@yangrenmu
Copy link
Owner

上一篇: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,处理方式如上面的流程图所示。
@yangrenmu yangrenmu added the vue label Dec 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant