初始化组件patch

11/12/2021 vue

update (opens new window)小节分析了普通vnode渲染到页面的过程,这小节重点分析组件vnode(占位符vnode)的渲染过程

借助如下例子进行后续的逻辑分析

<body>
	<div id="app"></div>
</body>
<script>
new Vue({
  el: '#app',
  render (h) {
    return h({
      template: `
        <div class="form-wrapper">
          <label class="form-title" for="name">姓名</label>
          <input class="form-input" :value="name">
        </div>
      `,
      data () {
        return {
          name: '张三'
        }
      }
    })
  }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

渲染函数render方法会将生成的占位符vnode返回,并作为参数调用vm._update方法,在该方法内部会紧接着调用vm.__patch__方法,关于vm._update方法和vm.__patch__方法可以参考update (opens new window)小节中对应的部分,调用vm.__update__方法实际上是调用src/core/vdom/patch.js文件中返回的patch方法

return function patch (oldVnode, vnode, hydrating, removeOnly) {
	if (isUndef(vnode)) {
		if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
		return
	}

	let isInitialPatch = false
	const insertedVnodeQueue = []

	if (isUndef(oldVnode)) {
		// empty mount (likely as component), create new root element
		isInitialPatch = true
		createElm(vnode, insertedVnodeQueue)
	} else {
		const isRealElement = isDef(oldVnode.nodeType)
		if (!isRealElement && sameVnode(oldVnode, vnode)) {
			// patch existing root node
			patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
		} else {
			if (isRealElement) {
				// mounting to a real element
				// check if this is server-rendered content and if we can perform
				// a successful hydration.
				if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
					oldVnode.removeAttribute(SSR_ATTR)
					hydrating = true
				}
				if (isTrue(hydrating)) {
					if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
						invokeInsertHook(vnode, insertedVnodeQueue, true)
						return oldVnode
					} else if (process.env.NODE_ENV !== 'production') {
						warn(
							'The client-side rendered virtual DOM tree is not matching ' +
							'server-rendered content. This is likely caused by incorrect ' +
							'HTML markup, for example nesting block-level elements inside ' +
							'<p>, or missing <tbody>. Bailing hydration and performing ' +
							'full client-side render.'
						)
					}
				}
				// either not server-rendered, or hydration failed.
				// create an empty node and replace it
				oldVnode = emptyNodeAt(oldVnode)
			}

			// replacing existing element
			const oldElm = oldVnode.elm
			const parentElm = nodeOps.parentNode(oldElm)

			// create new node
			createElm(
				vnode,
				insertedVnodeQueue,
				// extremely rare edge case: do not insert if old element is in a
				// leaving transition. Only happens when combining transition +
				// keep-alive + HOCs. (#4590)
				oldElm._leaveCb ? null : parentElm,
				nodeOps.nextSibling(oldElm)
			)

			// update parent placeholder node element, recursively

			// ......

			// destroy old node
			if (isDef(parentElm)) {
				removeVnodes([oldVnode], 0, 0)
			} else if (isDef(oldVnode.tag)) {
				invokeDestroyHook(oldVnode)
			}
		}
	}

	invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
	return vnode.elm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

借助上面所写的例子分析,在调用该方法时,由于vm为根实例对象,所以oldVnode则为vm.$el真实DOM对象,并在该方法内会将其转化为虚拟dom,紧接着会调用createElm方法,该方法和patch方法在同一文件中

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
    }

    // ......

  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

createElm内由于参数vnode为占位符vnode,紧接着会调用createComponent方法,该方法和上一小节分析的createComponent方法不是同一个方法,该方法和createElm定义在同一文件中

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
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在该方法内会首先判断是否存在vnode.data,如果存在则会将占位符vnode作为参数调用vnode.data.hook.init钩子函数,在createComponent (opens new window)小节分析过,在生成占位符vnode之前会先进行一些组件钩子函数的安装,所以实际调用的是在src/core/vdom/create-component.js中定义的组件钩子函数init方法

const componentVNodeHooks = {
  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)
    }
  },
  
  // ......
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

由于为初始渲染,占位符vnode还没有组件实例,所以会调用createComponentInstanceForVnode方法生成一个组件实例,需要注意的时,调用createComponentInstanceForVnode方法时传递的第二个参数为activeInstance,该变量定义在src/core/instance/lifecycle.js中,该变量主要用于存储当前vm实例,createComponentInstanceForVnode方法也定义在src/core/vdom/create-component.js

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

该方法主要功能是new一个组件实例,并返回,在上一小节分析过,在生成占位符vnode之前会根据组件参数对象构建一个该组件对应的构造函数,该构造函数会作为实例化占位符vnode时的参数选项,所以这里的vnode.componentOptions.Ctor实际上就是实例化该占位符vnode时的构造函数,该构造函数在Vue.extend方法内生成并返回,该方法定义在src/core/global-api/extend.js

Vue.extend = function (extendOptions: Object): Function {
	extendOptions = extendOptions || {}
	const Super = this
	const SuperId = Super.cid
	const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
	if (cachedCtors[SuperId]) {
		return cachedCtors[SuperId]
	}

	const name = extendOptions.name || Super.options.name
	if (process.env.NODE_ENV !== 'production' && name) {
		validateComponentName(name)
	}

	const Sub = function VueComponent (options) {
		this._init(options)
	}
	Sub.prototype = Object.create(Super.prototype)
	Sub.prototype.constructor = Sub
	Sub.cid = cid++  
	Sub.options = mergeOptions(
		Super.options,
		extendOptions
	)
	Sub['super'] = Super

	// For props and computed properties, we define the proxy getters on
	// the Vue instances at extension time, on the extended prototype. This
	// avoids Object.defineProperty calls for each instance created.
	if (Sub.options.props) {
		initProps(Sub)
	}
	if (Sub.options.computed) {
		initComputed(Sub)
	}

	// allow further extension/mixin/plugin usage
	Sub.extend = Super.extend
	Sub.mixin = Super.mixin
	Sub.use = Super.use

	// create asset registers, so extended classes
	// can have their private assets too.
	ASSET_TYPES.forEach(function (type) {
		Sub[type] = Super[type]
	})
	// enable recursive self-lookup
	if (name) {
		Sub.options.components[name] = Sub
	}

	// keep a reference to the super options at extension time.
	// later at instantiation we can check if Super's options have
	// been updated.
	Sub.superOptions = Super.options
	Sub.extendOptions = extendOptions
	Sub.sealedOptions = extend({}, Sub.options)

	// cache constructor
	cachedCtors[SuperId] = Sub
	return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

Vue.extend生成的构造函数都原型继承与Vue构造函数,所以在调用this._init方法时,实际上调用的Vue.prototype._init方法,又回到了最开始分析的地方,又是熟悉的感觉,该方法定义在src/core/instance/init.js

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

需要注意的是,在实例化Vue根实例时调用该方法传递的参数和在实例化组件实例时调用该方法传递的参数不同

// 实例化Vue根实例时传递的参数为
{
	data: {
		// .......
	},
	methods: {
		// .......
	},
	render () {
		// ......
	}
	// ......
}
// 实例化组件实例时传递的参数为
{
	_isComponent: true,
	_parentVnode: vnode, // 该组件对应的占位符vnode
	parent // 父实例对象 activeInstance 对应的vm对象
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

继续回到_init方法中,会先进行参数合并,由于实例化组件实例和实例化Vue根实例时传递的参数差异,所以会调用initInternalComponent,该方法和_init方法定义在同一文件中

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

该方法内部逻辑较简单,首先利用vm.constructor.options作为原型创建对象vm.$optionsvm.constructor.options其实就是指向Vue.options和该构造函数对应的组件对象合并后的对象,可以查看createComponent (opens new window)小节对Vue.extend部分的参数合并逻辑分析,这里则不过多赘述,接着将options中的一些属性(parent, parentVnode)和该构造函数对应的占位符vnode中的一些属性(propsData, listeners, children, tag)添加到vm.$options

回到_init方法中,接着就是做对组件实例的生命周期,事件,render,状态等的一些初始化工作 这里简单介绍下initLifecycle方法,该方法定义在src/core/instance/lifecycle.js

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

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

该方法主要是将当前实例和父实例做一些关联(parentchildren的关联),和在当前实例上定义一些初始化变量

接着回到src/core/vdom/create-component.js中定义的组件钩子init方法中

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
 
      // ...... 
 
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  
  // ......
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

生成的组件实例会赋给vnode.componentIntancechild,然后调用child.$mount(undefined, false)方法(由于在createComponent方法中调用init钩子时,传递的参数hydratingfalse,所以参数为undefined, false

接着后续会执行和实例化Vue根实例一样的方法步骤

  • 将组件template编译成render
  • 调用vm._render方法生成虚拟DOM
  • 将生成的虚拟DOM作为参数调用vm._update(内部调用vm.__patch__方法)

继续回到patch方法中

return function patch (oldVnode, vnode, hydrating, removeOnly) {
	if (isUndef(vnode)) {
		if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
		return
	}

	let isInitialPatch = false
	const insertedVnodeQueue = []

	if (isUndef(oldVnode)) {
		// empty mount (likely as component), create new root element
		isInitialPatch = true
		createElm(vnode, insertedVnodeQueue)
	} else {
    
		// ......
		
	}

	invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
	return vnode.elm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

由于调用$mount方法时el接收到的对应参数值为undefined,所以vm.$eloldVnode都为undefined

紧接着在patch方法内部调用createElm方法,并且返回vnode.elm真实DOM 可以回顾update (opens new window)小节对该方法和vm._update后续逻辑的解析

接着继续回到src/core/vdom/patch.js文件中的createComponent方法的后续逻辑中

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
	let i = vnode.data
	if (isDef(i)) {

		// ......

		// 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
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

占位符vnode的组件实例已经生成,接着会调用initComponent方法,该方法和createComponent方法定义在同一文件中

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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

该方法内部主要更新vnode.elm(将组件的根元素赋给vnode.elm)紧接着执行各个模块的create钩子,并且判断该占位符vnode是否有create钩子,如果有则执行,然后将该占位符vnode添加到队列insertedVnodeQueue中,setScope方法暂略过分析,继续回到createComponent方法,接着执行insert方法,该方法和initComponent方法定义在同一文件中

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)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11

将占位符vnode对应的组件的根元素,添加到父级元素parent中,至此,便实现了组件的初始化渲染,元素插入完毕,createComponent方法直接return true,回到patch方法后续逻辑

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {

          // ......

          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // ......

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

patch方法内接着移除oldVnode,并调用invokeInsertHook方法,循环调用insertedVnodeQueue队列中的每个组件的insert钩子

最后更新时间: 12/4/2022, 1:44:46 PM