组件
组件注册
组件注册方式有两种:全局注册和局部注册。
全局注册
使用Vue.component(tagName, options)
js
// 定义全局 api:Vue.component、Vue.filter、Vue.directive
// 主要逻辑就是:往 Vue.options 上存放对应的配置
// 最后,在 new Vue 的时候,通过 mergeOptions 将全局注册的组件合并到每个组件的配置对象中
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,使用组件配置中的 name,没有,使用 id
definition.name = definition.name || id
// 通过Vue.extend() 创建子组件,返回子类构造器
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// this.options.compoments[id] = definition
// this.options.directives[id] = definition
// this.options.filters[id] = definition
// 在 new Vue 时通过 mergeOptions 将全局注册的组件合并到每个组件的配置对象中
this.options[type + 's'][id] = definition
return definition
}
}
})
}
在创建vnode
时,使用isDef(Ctor = resolveAsset(context.$options, 'components', tag))
判断是否调用createComponent。
js
// 返回组件的构造函数,作为createComponent钩子的参数。
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
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
}
判断顺序:id->驼峰形式->驼峰加首字母大写,因此,在使用Vue.component(id, definition)全局注册组件时,id可以市连字符、驼峰或首字母大写的形式。
局部注册
在组件内部使用conponents选项进行组件局部注册,把components合并到vm.$options.components上。
异步组件
异步组件的三种使用方式:
js
Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
js
// Promise异步组件
Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
js
// 高级异步组件,可以设置loading和error组件
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
对异步组件的处理:
js
// 对异步组件的处理
// async component
let asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
// 处理高阶组件0delay创建loading组件,否则返回undefined,创建一个注释节点作为占位符,把asyncFactory和asyncMeta赋值给vnode,执行forceRender,触发组件重新渲染,则返回值不为undefined。
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.
// 是创建一个注释节点vnode
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
}
}
Ctor
是函数,不会执行Vue.extend
,cid
是undefined,执行resolveAsyncComponent
方法。
js
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
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
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++) {
// $forceUpdate 的逻辑非常简单,就是调用渲染 watcher 的 update 方法,让渲染 watcher 对应的回调函数执行,也就是触发了组件的重新渲染。
// 之所以这么做是因为 Vue 通常是数据驱动视图重 新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次。
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
// once 确保包装的函数只执行一次,使用单例模式实现,多个地方初始化一个异步组件,实际加载只有一次
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)
} 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 形式异步组件 isPromise: 判断是否 promise 对象的方法
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
// 高级异步组件
res.component.then(resolve, reject)
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
// 延时展示loading
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
// 超时失败
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production' ?
`timeout (${res.timeout}ms)` :
null
)
}
}, res.timeout)
}
}
}
sync = false
// return in case resolved synchronously
return factory.loading ?
factory.loadingComp :
factory.resolved
}
}
总结
- 组件注册有全局和局部注册两种方式,本质是mergeOptions,执行components中的构造函数。
- 全局组件的id有三种写法,异步组件有三种类型。
- 异步组件除了0delay的高级组件本质都是两次渲染,第一次生成注释节点占位。
- 编程思想:工厂函数以及单例模式