组件实现原理
render 阶段
对于子组件的编译过程,其实和普通标签一样,最终都会编译成_c
函数(对应_createElement
函数),但是在render
阶段生成Vnode
的时候就有所不同了:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (typeof tag === 'string') {
let Ctor
// 1. 如果是普通标签,那么直接创建 vnode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 2. 否则的话,可能是组件,这个时候通过 resolveAsset 去 $options 中查找对应的 组件配置
// 如果是全局组件注册,会返回一个构造函数,如果是局部组件注册,会返回 组件配置,也就是options
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
}
// ...
}
_c
方法会判断标签的类型,如果不是普通标签,那么此时可能是组件标签,需要查找组件的定义。如果找到了组件的定义,那么调用createComponent
为组件生成Vnode
:
export function createComponent (
Ctor: Class<Component> | Function | Object | void, // 组件的配置 or 组件的构造函数
data: ?VNodeData,
context: Component, // 当前的实例
children: ?Array<VNode>,
tag?: string // 组件名称
): VNode | Array<VNode> | void {
// * 这里的 Ctor 有几种形式
// * 1. 全局形式定义的 component,那么 Ctor 是构造函数形式
// * 2. 局部定义的 component,那么是 对象形式。会对对象形式进行 extend 处理
const baseCtor = context.$options._base
// 局部注册的组件为配置形式
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
installComponentHooks(data)
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
)
return vnode
}
其中调用了installComponentHooks
会为data
添加几个方法,如下:
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// ...
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert (vnode: MountedComponentVNode) {
// ...
},
destroy (vnode: MountedComponentVNode) {
// ...
}
}
patch 阶段
在patch
阶段创建真实节点时:
function createElm () {
...
// 如果是组件,则创建相应节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
}
// 创建Component
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
return true
}
}
}
如果当前是组件的Vnode
,此时会调用init
方法:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// 创建子组件
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
此时会调用createComponentInstanceForVnode
方法,生成子组件实例(其中activeInstance
指的是当前正在更新的组件,子组件是在父组件渲染时去渲染的,因此表示父组件实例):
export function createComponentInstanceForVnode (
vnode: any,
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, // 指向组件对应的 vnode
parent
}
// render 阶段 createComponent 生成vnode的时候,
// 数据都存在componentOptions中的,包括组件构造函数 Ctor
return new vnode.componentOptions.Ctor(options)
}
子组件实例化阶段
找到组件的构造函数后,进行实例化,这个过程中在options
合并时有所不同:
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
// 组件的父亲组件
opts.parent = options.parent
opts._parentVnode = parentVnode
// 组件的配置选项
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
// * _renderChildren 是插槽内部的节点 (在没有scope的情况下)
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
}
实例化后,得到子组件实例,开始进行mount
,编译更新完成后,将其添加到parent
上:
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
组件小结
最后总结一下Compnent
的流程:
- 编译完成后,开始进行
render
生成Vnode
。此时在_c
函数中,如果不是普通标签的标签,说明可能是定义的组件。 - 根据可能是组件的标签,根据名称到
options
中查找对应的组件定义。如果找到了,它可能是构造函数(比如使用Vue.component
全局定义的),它也可能是对象形式(比如局部定义的)。 - 找到定义后,创建一个
Vnode
,将构造函数等信息全部存放到componentOptions
属性中,并且添加init,insert
等hook
。 - 在
patch
阶段,创建真实节点时,先判断有没有init
方法。如果存在,说明可能是组件。此时需要取到之前保存的组件构造函数,并进行实例化,并且挂载到当前真实节点上。
异步组件实现原理
异步组件的基本使用如下:
// 直接定义一个promise处理回调
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
// 直接定义一个promise
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 或者返回一个对象
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
异步组件的处理和子组件一样,都是在render
阶段处理的:
let asyncFactory
// 异步组件的 Ctor 为函数形式,但是没有 cid
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
// 解析 Ctor,如果能拿到构造函数,说明加载完成了或存在占位Vnode
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 否则,创建一个替代 Vnode
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
看一下resolveAsyncComponent
的处理:
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
// ...
}
参数中factory
对应了组件定义时的内容。后面会开始加载组件,如果正在加载,那么loading
为true
,此时显示loadingComp
,如果resolved
存在,那么会显示加载完成的组件resolved
。
其次是owners
的处理,owners
表示哪些组件使用了该异步组件。这样在异步组件加载完成时,就能通知哪些组件去更新了。
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null
;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
// ...
}
最后,开始正在的加载过程:
// 加载完成时,更新所有使用该异步组件的组件
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
}
// 仅加载一次,然后缓存起来,下一次直接使用
const resolve = once((res: Object | Class<Component>) => {
// 加载完成的构造函数
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
// 错误处理
const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
// 如果是函数形成,那么直接调用即可
const res = factory(resolve, reject)
if (isObject(res)) {
// 如果是 promise
if (isPromise(res)) {
// 如果是 () => Promise 会使用 then 处理加载回调
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
// 如果是对象形式 { component: () => Promise }
res.component.then(resolve, reject)
// 此时还会添加其他的占位 component
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {}
if (isDef(res.timeout)) {}
}
}
异步组件小结
最后小结一下异步组件的实现:异步组件与普通的组件最大的不同是在render
阶段构造函数的处理。如果异步组件还未开始加载,那么会返回占位组件的构造函数。并且开始进行加载组件,并且在组件加载完成时,遍历用到了该异步组件的组件,进行$forceUpdate()
。