Vue patch过程(二)—— 创建节点

1,204 阅读3分钟

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

前言

上一篇已经将Vue diff的整个过程走了一遍,这一篇主要看一下Vue 在patch过程中怎么创建新节点。注意,这里的节点是真实的DOM节点,不是虚拟DOM,不要搞混了。在这之前先来简单复习下即将用到的虚拟DOM定义的VNode对象的几个属性:

optionvalue
tagDOM标签
elm对应的真实DOM节点
data虚拟节点的一些数据信息,如style,class等,是VnodeData类型
children当前节点包含的子节点
ns当前节点的命名空间
isComment是否是注释节点
fnScopeId节点的scopeId
context当前节点的父虚拟节点上下文作用域

了解了这几个属性的含义,接下来具体看下怎么创建新的真实DOM元素节点。上一篇已经看到了创建新节点是调用createElm函数,来看下它的源码:

createElm

function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        ...
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } 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)
    }
  }
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)
        }
    }
}

首先调用createComponent方法(后边介绍)来创建子组件,如果true则直接返回。

接下来判断是否具有tag,如果不具有则判断是不是注释节点,如果是则创建注释节点,否则说明是一个文本节点,将创建好的节点调用insert方法插入到父节点。

如果具有tag,创建一个elment占位元素给到节点的elm属性,为节点添加scopeId,设置样式作用域。接下来调用createChildren创建子元素。如果data不为空则调用invokeCreateHooks方法。

setScope

function setScope (vnode) {
    let i
    if (isDef(i = vnode.fnScopeId)) {
        nodeOps.setStyleScope(vnode.elm, i)
    } else {
        let ancestor = vnode
        while (ancestor) {
            if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
                nodeOps.setStyleScope(vnode.elm, i)
            }
            ancestor = ancestor.parent
        }
    }
    // for slot content they should also get the scopeId from the host instance.
    if (isDef(i = activeInstance) &&
        i !== vnode.context &&
        i !== vnode.fnContext &&
        isDef(i = i.$options._scopeId)
    ) {
        nodeOps.setStyleScope(vnode.elm, i)
    }
}

setScope主要是用来处理我们用的scoped CSS,先判断fnScopeId是否设置,如果设置了直接使用;否则就从父级上下文环境一层层查找。

createChildren

接下来看下createChildren做了什么:

function createChildren (vnode, children, insertedVnodeQueue) {
    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)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
}

首先判断子节点是不是数组,如果是则遍历所有的虚拟子节点,依次递归调用createElm方法;否则判断是不是文本节点,直接调用appenChild添加到父节点后边。可以看出由于递归调用的缘故,子节点会先执行insert方法,所以整个Vnode树是先子后父插入的。

invokeCreateHooks

function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
        cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
        if (isDef(i.create)) i.create(emptyNode, vnode)
        if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
}

invokeCreateHooks函数首先for循环调用了所有的create钩子函数,然后把vnode 加入到insertedVnodeQueue中。