系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
在上一章节中,我们知道对于新的vnode,需要调用createElm方法渲染成真实的DOM,那么接下来,我们就看看其内部是如何实现的。
createElm
createElm的代码如下所示:
/* core/vdom/patch.js */
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
vnode.isRootInsert = !nested // for transition enter check
// 1. 组件VNode
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 2. 普通元素节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
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)) {
// 3. 注释
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 4. 文本
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
因为不同类型节点之间的差异很大,所以在渲染的时候,Vue将它们分成了四种类型,分别执行各自的逻辑:
-
通过
createComponent方法尝试从组件占位符VNode创建组件实例; -
通过
vnode.tag判断是否是普通元素节点; -
通过
vnode.isComment判断是否是注释节点; -
如果不是上述三种类型节点,一律当成文本节点;
我们首先来看看注释节点和文本节点的创建过程,Vue会调用平台相关的createComment和createTextNode方法,生成真实的DOM:
/* platforms/web/runtime/node-ops.js */
export function createTextNode(text: string): Text {
return document.createTextNode(text)
}
export function createComment(text: string): Comment {
return document.createComment(text)
}
然后调用insert方法,将该节点插入到父节点中:
/* core/vdom/patch.js */
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)
}
}
}
由于这两种类型的节点没有data和children,所以到此也就处理完成了。那么接下来,我们就来看看Vue是如何生成元素节点的:
// 1. 创建DOM
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
// 2. 深度递归,创建子节点
createChildren(vnode, children, insertedVnodeQueue)
// 3. 执行module对应的create hook
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 4. 将节点插入到父节点中
insert(parentElm, vnode.elm, refElm)
可以看到,创建元素节点的逻辑还是很清晰的,首先调用createElement方法,创建对应的元素节点:
/* platforms/web/runtime/node-ops.js */
export function createElement(tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
然后调用createChildren方法,深度递归的创建子节点:
/* core/vdom/patch.js */
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)))
}
}
在所有的子节点都创建完成后,接着会判断vnode.data是否存在,如果存在,就会调用invokeCreateHooks方法,用来处理modules对应的create钩子函数:
/* core/vdom/patch.js */
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方法中,如果检测到VNode中存在insert钩子函数,还会将该VNode节点插入insertedVnodeQueue中,这是因为此时还在VNode的create阶段,所以需要等到patch结束后,在invokeInsertHook中再统一进行处理。
最后,将生成的DOM节点插入到父节点中,完成普通元素VNode的渲染过程。
createComponent
除了上面三种情况外,其实一开始就会尝试通过createComponent方法,创建组件节点的实例,其代码如下所示:
/* core/vdom/patch.js */
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
可以看到,对于组件VNode,首先会从data.hook中取出init钩子函数,它是在创建组件VNode节点时,添加到hook上的,其代码如下所示:
/* core/vdom/create-component.js */
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
在init方法中,首先会调用createComponentInstanceForVnode方法,创建子组件实例:
/* core/vdom/create-component.js */
export function createComponentInstanceForVnode(
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
可以看到,在createComponentInstanceForVnode方法中,Vue首先在内部构建了一个配置对象,然后调用子组件的构造器,从而完成子组件的初始化过程,其中,配置对象中的_parentVnode表示子组件的父占位符VNode,它的componentOptions属性中包含了需要传给子实例的详细信息,parent表示子组件的父实例,也就是之后子实例的$parent,最后,通过vnode.componentOptions.Ctor子构造器,创建子组件的实例,在其内部,同样是调用_init方法,所以子组件的创建过程和根组件的创建过程是类似的。
创建好组件实例后,会就将其赋值给父占位符的componentInstance,然后调用$mount方法,挂载子组件实例,这部分逻辑也与之前类似,首先创建子组件的渲染Watcher,然后调用_render方法生成子组件的渲染VNode,接着调用_update方法根据VNode渲染成真实的DOM。
在调用完init钩子函数后,此时调用栈已经回到了父实例中,此时的vnode.componentInstance就指向了刚刚创建的子组件实例,所以接着调用initComponent方法:
/* core/vdom/patch.js */
function initComponent(vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
可以看到,在initComponent方法中,首先尝试从vnode中提取pendingInsert,它里面包含了需要执行insert钩子函数的节点,然后将其添加到当前的insertedVnodeQueue中,初次渲染时,通过这样层层的提取,所以最后在根节点patch时,就可以在调用这些节点的insert钩子函数;接着,将子组件的根DOM赋值给父占位符的elm;然后判断子该组件是否是可patchable的,如果可挂载,那么就需要调用invokeCreateHooks方法,一方面将父占位符中的data作用在DOM上,另一方面,也将当前组件vnode插入到insertedVnodeQueue中。
最终,通过createComponent方法,同样也可以将组件节点渲染成真实的DOM。
总结
createElm会根据vnode的类型,使用不同的方法创建真实的节点,对于元素节点来说,它会递归创建子节点,所以在给定一个根vnode的情况下,Vue也能正确的生成整棵DOM树,而对于组节点来说,它会通过createComponent方法,执行init钩子函数,从而创建子组件的实例,并进行挂载,最终,同样将组件节点渲染成真实的DOM。