通过一个例子来分析
<body>
<div id="app"></div>
<script src="./vue.js"></script>
<script>
const App = {
data() {
return {
message: 'app数据'
}
},
render(h) {
return h(
'div',
{
attrs: {
class: 'app1'
}
},
this.message
)
}
}
const vm = new Vue({
el: '#app',
components: {
App
},
render(h) {
return h(App)
}
})
</script>
</body>
接着上一篇,通过 vm._render
生成了组件vnode,就该通过vm._update
来插入到dom中,vm._update中调用了vm.__patch__
,然后调用 createPatchFunction
中返回的patch
,在patch中会调用createElm
,
在createElm
中组件的插入和普通元素就不一样了,
src/core/vdom/patch.ts
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
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
//createComponent 方法目的是尝试创建子组件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
}
进入 createComponent
尝试创建组件,
src/core/vdom/patch.ts
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的时候,有一个方法 installComponentHooks(data)
会把组件的钩子初始化放在vnode的data的hook里面,然后就会执行i(vnode, false)
,先进入init方法看一下
src/core/vdom/create-component.ts
init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
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)
}
},
if中是keep-alive的逻辑进不去,看else里面,通过createComponentInstanceForVnode
创建组件实例,参数是vnode(也就是例子App组件的vnode),activeInstance当前活跃的组件实例一会再说,在这里此时是最外层new Vue()生成的这个实例
src/core/vdom/create-component.ts
export function createComponentInstanceForVnode(
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent?: any
): 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)
}
参数vnode是组件APP的vnode,parent就是例子里的vue实例vm;给_parentVnode赋值组件App的vnode,
最后new vnode.componentOptions.Ctor(options)
,还记得上一篇中,src/core/vdom/create-component.ts的方法createComponent
里面,生成组件Appd的时候,new Vnode的第五个参数componentOptions
传入了 { Ctor, propsData, listeners, tag, children }
,Ctor
也是通过 Ctor = baseCtor.extend(Ctor as typeof Component)
继承了Vue构造函数而生成的App的构造函数;
最后是返回App的组件实例;
在执行new vnode.componentOptions.Ctor(options)
,会执行到Vue.extend
中的
const Sub = function VueComponent(this: any, options: any) {
this._init(options)
}
这个this就是组件App的实例,然后又回到了Vue.prototype._init
这个方法
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
initLifecycle(vm)
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这个参数options是
options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, //组件App的vnode
parent // Vue实例vm
}
此时options._isComponent
为true,不再是undefined,进入initInternalComponent
,
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create((vm.constructor as any).options))
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
...
}
options._parentVnode 是组件App的vnode,options.parent 是Vue实例vm ,所以在App组件实例的 $options上的 $options.parent是Vue实例vm
, $options._parentVnode是 App的vnode
,
在Vue.prototype._init
中,再看一下 initLifecycle(vm)
src/core/instance/lifecycle.ts
export let activeInstance: any = null
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
}
export function lifecycleMixin(Vue: typeof Component) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
}
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
options.parent 实际就是 Vue实例vm ,当前活跃的组件实例,activeInstance是全局变量,在_update
中会对activeInstance做操作,
在patch前,执行setActiveInstance(vm)
,activeInstance = vm
,prevActiveInstance = activeInstance
,
在patch后,activeInstance = prevActiveInstance
,
以上很大一部分都是建立父子组件之间的关系,在当前的组件实例的vnode在patch过程中,把当前组件实例作为父组件实例,传给子组件
然后回到child.$mount(hydrating ? vnode.elm : undefined, hydrating)
,手动调用了 $mount
来挂载,也就是执行之前的 Vue.prototype.$mount
和 mountComponent
方法,然后mountComponent
里面实例化Watcher来执行vm._update(vm._render(), hydrating)
,回到了之前的逻辑
总结:组件的patch过程
- 在patch中调用
createElm
,createElm通过createComponent
尝试创建组件, - 在
createComponent
中调用了组件的钩子init
,在init中,获得组件实例
child,并且手动调用组件实例的$mount
, - 然后又会走到
mountComponent
,vm._update(vm._render(), hydrating)
, - 回到
createElm
,当createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
不满足时,就往下走,就跟之前普通元素挂载一样,因为js是单线程,都是子组件先插入,而后才是父组件,最后是外面哪个div#app插到Dom里面