Vue2.0源码理解(6) - 组件注册

组件注册

前言

在 Vue.js 中,除了它内置的组件如 keep-alive、component、transition、transition-group 等,其它用户自定义组件在使用前必须注册。在开发过程中可能会遇到如下报错信息:

 Unknown custom element: <app> - did you register the component correctly? 
For recursive components, make sure to provide the "name" option.

一般报这个错的原因都是我们使用了未注册的组件。Vue.js 提供了 2 种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。

全局注册

在初始化加载阶段会调用initAssetRegisters函数把需要注册的组件挂载到Vue.options上。

 // src\core\global-api\index.js
initAssetRegisters(Vue)
 // src\core\global-api\assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
// 标注①
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
// 优先拿name,没有则取id
definition.name = definition.name || id
// 标注②
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}

标注①:
对ASSET_TYPES进行遍历,我们先看看遍历对象ASSET_TYPES是什么?

 // src\shared\constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]

其实就是存放着插件、指令、过滤器这三个分类名称的数组,这里我们只单独针对component进行分析。
标注②:
this.options._base其实是Vue,具体原因请查看之前的文章《组件的创建和patch过程》。
通过Vue.extend把对象转换成构造器。
最后把definition放到this.options即Vue.options上,然后return definition。
虽然挂载到Vue.options上,但是又是什么时候会被拿去注册成真正的组件呢?
我们回顾_createElement函数:

 // src\core\vdom\create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
if (typeof tag === 'string') {
//是否HTML原生标签
if (config.isReservedTag(tag)) {
...
// 标注①:resolveAsset
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
...
}
}
}

标注① :resolveAsset函数做了什么?

 // src\core\util\options.js
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
//判断配置中是否存在该组件
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
//id转换成驼峰型判断
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
//id转换成首字母大写判断
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
// 原型上面找
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
//返回构造器
return res
}

其实就是经过各种情况判断识别Vue.options是否有定义该组件,有的话则返回,然后最后经过createComponent函数进行了组件的注册。关于createComponent函数的不了解的请查阅之前文章《组件的创建和patch过程》
局部注册其实和全局注册的几乎一样,只是它需要在此前做一个option合并:

 // src\core\global-api\extend.js
Sub.options = mergeOptions(
Super.options,
extendOptions
)

关于合并的详细分析请查阅之前文章《合并配置》
由于合并配置是挂载于Sub上的,也就是说它只是一个在当前Sub作用域下的,一次这种创建方式的组件只能局部使用。

异步组件注册

工程模式

平时开发,特别涉及到首屏加载的时候,一次性加载完成会对首屏体验变成,所以往往加了各种异步加载,组件注册当然也支持异步注册,下面我们看看源码是怎么实现。
我们先在main.js写一个注册一个异步组件

 Vue.component('HelloWorld',function(resolve,reject){
require(['./components/HelloWorld'],function(res){
resolve(res);
})
})

还是回到最初的方法initAssetRegisters(Vue)

 // src\core\global-api\assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
// 优先拿name,没有则取id
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}

其实在这个函数里只是把方法挂载到options上,那么在哪里处理异步呢?我们回看创建组件方法

 // src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// async component
let asyncFactory
if (isUndef(Ctor.cid)) { //因为是方法,所以没有id
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
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
)
}
}

因为我们异步注册传进的是一个方法,所以是没有cid的,所以就到了判断里面逻辑。这里面我们分两步分析,分别是resolveAsyncComponent方法和createAsyncPlaceholder方法。

①、resolveAsyncComponent方法
 // src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
...
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
}
})

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)
// 异步,因此res为undefined
if (isObject(res)) {
...
}
...
sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}

第一次进来主要处理了两件事:
1、定义了resolve和reject方法用于回调。
2、执行了组件注册时参入的方法。
最后return时候factory.loading是undefined的,因此回到createComponent时进入createAsyncPlaceholder方法

②、createAsyncPlaceholder方法
 // src\core\vdom\helpers\resolve-async-component.js
export function createAsyncPlaceholder (
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}

createAsyncPlaceholder方法其实就是创建一个空的注释节点。
至此同步线程结束,回到了组件注册时,我们定义的方法。

 Vue.component('HelloWorld',function(resolve,reject){
require(['./components/HelloWorld'],function(res){
//同步线程结束,调用resolve方法。
resolve(res);
})
})

res参数指向的就是组件的本身。
然后我们回看resolve方法:

 // src\core\vdom\helpers\resolve-async-component.js
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
}
})

其中ensureCtor方法主要是利用Vue.extend做构造器转换,执行后挂载到我们定义的方法下,factory.resolved在后面重新强制渲染会用来做判断,然后执行下一步,进入forceRender方法。

 // src\core\vdom\helpers\resolve-async-component.js
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate()
}

if (renderCompleted) {
contexts.length = 0
}
}

forceRender方法主要是调用$forceUpdate()触发一次强制渲染。
这次我们又回到最初的起点,呆呆的。。。。
来,我们继续看resolveAsyncComponent老友:

 // src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
...
if (isDef(factory.resolved)) {
return factory.resolved
}
})

这次factory.resolved是有的了,然后就返回factory.resolved构造器。

 // src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// async component
let asyncFactory
if (isUndef(Ctor.cid)) { //因为是方法,所以没有id
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
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
)
}
}

返回构造器后createAsyncPlaceholder方法就不再去跑了,然后会正常进入patch阶段把组件注册。

promise模式

 // import会返回一个promise对象
Vue.component('HelloWorld',
() => import('./components/HelloWorld')
)

和工厂模式殊途同归,还是去到了resolveAsyncComponent方法

 // src\core\vdom\helpers\resolve-async-component.js
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
...
//这时候返回的是promise对象
const res = factory(resolve, reject)

if (isObject(res)) {
// 此处调用promise对象的then方法
if (typeof res.then === 'function') {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
...
}
})

和工厂模式不一样的是factory执行后会返回一个promise对象,从而进入到判断逻辑,然后使用promise的then方法从而进行到异步调用注册组件。

高级模式

 const LoadingComp = {
template:"<div>LoadingComp</div>"
}

const ErrorComp = {
template:"<div>ErrorComp</div>"
}

const AsyncComp = {
// 需要加载的组件,应当是一个promise对象
component: import('./components/HelloWorld.vue'),
//加载中应当渲染的组件
loading:LoadingComp,
// 出错时渲染的组件
error:ErrorComp,
// 渲染加载中组件前的等待时长
delay:200,
// 最长等待时长,超时渲染报错组件
timeout:3000,
}

Vue.component('HelloWorld',AsyncComp);

高级模式就不再分析了,走的还是resolveAsyncComponent方法,有兴趣的可以自行研究一下,总体流程差不多.

组件注册就说到这了,后面我们会探讨响应式的原理。

标签: Javascript

添加新评论