在《render 渲染原理》一文中,分析了 render 函数如何将 Vue 实例渲染成 VNode。在这渲染过程中,有两种方式:一是普通节点渲染;二是组件渲染。而当时只分析了普通节点的渲染过程,那么本文将分析组件的渲染过程。
组件渲染成 VNode
按照惯例,沿着主线将组件渲染成 VNode 的过程整理成一张图,如下:
通过例子来进行分析:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
还记得在函数 _createElement 有这么一段逻辑:
if (typeof tag === 'string') {
...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
通过例子可以知道,用户手写 render 函数渲染 App 组件;因此,tag 其数据类型不是 string,而是一个对象(组件对象),从而进入 else 分支,调用函数 createComponent 生成组件 VNode。
createComponent 内部实现
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
函数接收 5 个参数,分别如下:
Ctor:表示一个组件对象data:表示 VNode 数据context:表示 Vue 实例children:表示 VNode 子节点tag:表示标签
作用:将组件渲染成组件 VNode。
从函数的代码实现来看下,其功能点主要有:
- 构造子类构造函数
- 异步组件逻辑
- 解析&合并 options
v-model逻辑- 提取
props - 函数式组件逻辑
- 安装组件钩子函数
- 实例化组件 VNode
而在本文中,着重分析加粗要点。
构造子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
对于 context.$options._base ,有没有好奇它是怎么来的?下面一步一步地来揭开其面纱。
在初始化 Vue 时,位于 src/core/index.js 文件里,有这么一行代码:
initGlobalAPI(Vue)
而在该函数的实现中,也有这么一行代码
export function initGlobalAPI (Vue: GlobalAPI){
...
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
}
可是你会发现 _base 是定义在 Vue.options ,而使用的是 Vue.$options._base,那么这其中又发生了什么呢?
其实在实例化 Vue 的过程中,有一段逻辑是对 options 进行合并处理的:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
将用户传入的 options 与 Vue 构造函数的 options 进行合并,最后将合并后的 options 赋值给 vm.$options ,因此可以通过 vm.$options 访问到 _base ,从而可知 _base 实际是指向 Vue。
在这里,Ctor 是指 App 组件对象,isObject(Ctor) 为 true ,则进入 if 分支逻辑,调用 Vue 方法 extend 构建子类构造涵,即
baseCtor.extend(Ctor) ,那么来看下其是如何实现的?(src/core/global-api/extend.js)
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this // 指向 Vue 构造函数,而不是 Vue 实例
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
}
extend 接收一个参数:
extendOptions:其数据类型是一个Object,指向export default {},即平时编写 Vue 组件时导出的对象
作用:构造一个 Vue 的子类,采用一种非常经典的原型继承方式将一个纯对象转换成一个继承于 Vue 构造器的子类 Sub ;同时对其进行扩展,并且将 Sub 构造函数进行缓存,避免对同一个组件进行重复构造,多次执行 Vue.extend。
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
变量 cachedCtors 用来缓存组件(Sub)构造函数,cid 作为 key ,而 Sub 构造函数作为 value。如果同一个组件被多次加载,那么除了第一次会执行 Vue.extend 外,其它都是直接从缓存中获取组件构造函数的。
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
获取组件名称,并对其名称进行合法性校验。如果子组件没有定义 name 属性,则直接从其父组件获取 name 属性。
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
这里是采用经典的原型继承方式来构造子类构造函数,即将一个纯对象转换成一个继承于 Vue 构造函数的子类 Sub。也就是说,先声明构造函数 Sub ,并且将 Sub 原型 prototype 指向 Vue 原型,通过 Object.create 实现;同时将 Sub.prototype.constructor 指向其自身 Sub ,从而实现了继承。
Sub.options = mergeOptions(
Super.options,
extendOptions
)
将构造函数 Super 自带的 options 与 用户传入的 options 进行合并,并将其合并结果赋值给 Sub.options 。
// 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)
}
如果 Sub.options 包含 props 属性,则对其进行初始化;如果 Sub.options 包含 computed 属性,则对其进行初始化。
// 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]
})
将 Super 属性赋值给 Sub 构造函数,对其进行扩展。
// cache constructor
cachedCtors[SuperId] = Sub
通过对象 cachedCtors 来缓存 Sub 构造函数,返回 Sub。
安装组件钩子函数
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
函数 installComponentHooks 接收一个参数:
data:其数据类型是VNodeData,表示 VNode data
作用:将 componentVNodeHooks 钩子函数 init、prepatch 、insert、destroy 合并到 hooks 中;在合并过程中,如果 hooks 存在相应的钩子函数,则调用 mergeHook 函数对其进行合并,采取的合并策略是依次执行钩子函数;否则直接保存到 hooks 中。最终在 VNode pathc 过程中执行对应的钩子函数。
那么来看下四个钩子函数具体是如何实现的?
// inline hooks to be invoked on component VNodes during patch
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)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
对于 hooks 已经存在的钩子函数,采用的合并策略是调用 mergeHook ,具体实现如下:
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
实例化组件 VNode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
最终是通过 new VNode 来实例化组件 VNode,并将其最终结果返回。
至此,组件渲染成 VNode 的核心逻辑已经分析完了。