这是我参与更文挑战的第3天,活动详情查看: 更文挑战
前言
上一篇已经将Vue diff的整个过程走了一遍,这一篇主要看一下Vue 在patch过程中怎么创建新节点。注意,这里的节点是真实的DOM节点,不是虚拟DOM,不要搞混了。在这之前先来简单复习下即将用到的虚拟DOM定义的VNode对象的几个属性:
| option | value |
|---|---|
| tag | DOM标签 |
| 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中。