异步组件
为什么需要异步组件
Vue
作为单页面应用在加载首页时常会遇到加载缓慢的问题,导致在使用体验较差,这是因为在打包单页面应用时,页面会吧脚本打包成一个文件,这个文件包含了所有业务和非业务的代码, 脚本文件过大导致渲染页面时缓慢。
在进行首屏性能优化时,最常用的方法就是对于文件的拆分和代理的分离,按需加载的概念也是在这个前提下引入的。因此在Vue
开发过程中,我们会把一些非首屏的组件设计成异部组件,部分不影响初次视觉体验的组件也可以设计成异步组件。
定义异步组件
在 Vue
中,在注册组件时使用一个工厂函数定义组件,这个工厂函数会异步解析组件定义,只有当这个组件需要被渲染时才会触发该工厂函数,并且会把结果缓存起来以便后续使用。
// 注册全局组件时,定义为异步组件
Vue.component("async-component", (resolve, reject)=>{
setTimeout(()=>{
// 使用一个定时器来模拟异步加载过程
// resolve 需要返回一个组件的定义对象,该对象的属性与定义普通组件的属性一致
resolve({
template: "async-component"
})
}, 2000)
})
// 注册局部异步组件
const vm = new Vue({
components:{
asyncComponent: ()=> import('./test.vue')
}
})
异步组件流程分析
在组件基础的分析过程中,我们分析了实例的挂载流程分为根据渲染函数创建 Vnode
和根据 Vnode
创建真实 DOM
的过程。在创建 Vnode
的过程中,如果遇到组件占位符,会调用 createComponent
,在该方法中,会为子组件做选项合并和安装钩子函数。异步组件的处理也是在该函数中进行的。
function createComponent (
Ctor: Class | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array,
tag?: string
): VNode | Array | void {
if (isUndef(Ctor)) {
return
}
/**
* 这里的 baseCtor 为 Vue 构造函数
*/
const baseCtor = context.$options._base
// 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 || {}
// 合并构造器配置
resolveConstructorOptions(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
}
在注册异步组件时, Vue.component(name, options)
第二个参数工厂函数不是一个普通对象,因此无论是全局注册还是局部注册, 都不会执行 Vue.extend
方法生成子类构造器,所以在上面的 createComponent
方法中, Ctor.cid
不会存在,代码进入异步组件的分支。
异步组件的核心是 resolveAsyncComponent
函数,我们主要关心工厂函数的处理部分,来看下精简后的代码
function resolveAsyncComponent (
factory: Function,
baseCtor: Class
): Class | void {
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 resolve = once((res: Object | Class) => {})
const reject = once(reason => {})
const res = factory(resolve, reject)
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
根据上面的代码,针对异步组件工厂函数的写法,我们可以总结成三个步骤:
- 定义异步请求成功和请求失败的处理函数
- 执行组件定义的工厂函数
- 同步返回请求成功的函数处理
resolve
, reject
函数都是 once
方法执行的结果, once
方法的作用是防止多次调用异步组件, 使得 resolve
, reject
只会执行一次。
function once (fn: Function): Function {
// 利用闭包的特性将 called 作为标志位
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
来看下 resolve
, reject
的处理逻辑
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
const resolve = once((res: Object | Class) => {
// 专程组件构造器,并缓存到 resolved 属性中
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)
}
})
组件构造器创建完成之后,会进行一次视图的重新渲染。 由于 Vue 是数据驱动试图进行渲染的,则组件在加载完毕之后,并没有发生数据的变化,因此需要手动强制更新试图。 forceRender
函数内部会拿到每个调用异步组件的实例,然后执行 Vue
原型上的 $forceUpdate
方法刷新视图。 在异步组件加载过程中,因为 Ctor
为 undefine
会同步创建一个注释节点,在异步组件加载完成之后,触发 $forceUpdate
再次执行 createEmptyVNode
, 这是 Ctor
不为 undefined
,因此会走正常的组件渲染流程
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
)
}
Promise 异步组件
异步组件的另一种写法是在工厂函数中返回一个 Promise
对象,在 ES6
中引入了 import
来加载模块, import
是一个运行时加载模块的方法,,可以和 require
进行对比, import
是异步加载的, require
是同步加载的,并且 import
会返回一个 Promise 对象
具体用法
Vue.component("asyncComponent", ()=> import('./test.vue'))
清楚 Promise
异步组件的注册方式之后,继续来分析异步的流程。
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
}
在工厂函数内部使用 import
引入一个异步组件时, 工厂函数回返回一个 Promise
对象,成功加载则执行 resolve
, 失败则执行 reject
。 其中判断一个对象是否为 Promise
对象最简单的方法就是是否存在 then
和 catch
方法
function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
高级异步组件
为了在使用时能有更好的体验,可以在加载异步组件的过程中使用 loading
组件来显示一个等待状态,使用 error
组件处理组件加载失败的错误提示等。在定义异步组件时,工厂函数可以返回一个对象,对象中可以定义需要加载的组件 component
, 加载过程中显示的 loading
组件,加载错误显示的 error
组件。在组件渲染过程中,同样进入异步组件的分支
Vue.component("asyncComponent", ()=> {
// 需要加载的组件
component: import('./test.vue'),
// 加载过程中显示的 loading 组件
loading: LoadingComponent,
// 组件加载错误时显示的组件
error: ErrorComponent,
// loading 组件显示的延迟时间,默认为 200, 如果 delay 时间后,组件还没有加载成功,则显示 loading 组件
delay: 200,
// 组件加载超时时间,超过该时间组件为加载成功,认为组件加载失败,使用 error 组件进行提示
timeout: 3000
})
对于高级异步组件,工厂函数返回的是一个对象,来看下对于高级异步组件 Vue
的处理过程
if (isObject(res)) {
if (isPromise(res)) {
// 工厂函数返回一个 Promise 的处理逻辑
} else if (isPromise(res.component)) {
// 高级异步组件的处理流程
// 组件加载过程了 Promise 异步组件处理方式相同
res.component.then(resolve, reject)
if (isDef(res.error)) {
// 定义了错误组件时,创建错误组件的子类构造器,并保存到 errorComp 中
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
// 创建加载时组件子类构造器,并保存到 loadingComp zhong
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
// 立即展现加载时组件
factory.loading = true
} else {
timerLoading = setTimeout(() => {
timerLoading = null
// 延时指定时间后,组件还没加载完成并没有加载失败,显示加载时组件
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
// 默认显示加载时组件延时时间为 200 ms
}, 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)
}
}
}
通过分析上方代码可以看出,高级组件的加载过程 Promise
组件的加载过程相同,额外添加了加载失败和加载过程中的处理逻辑。通过 delay
属性来延迟显示加载中组件的显示,通过 timeout
属性来规定超时时间。
webpack 异步组件用法
在使用 webpack
打包 Vue
应用时,我们可以将异步组件的代码进行分离。 webpack
为异步组件的加载提供了两种写法
require.ensure():
这是webpack
提供给异步组件的写法,webpack
在打包时, 会静态地解析代码中的require.ensure()
, 同时将模块添加到一个分开的chunk
,中,其中函数的第三个参数为分离代码块的名称
Vue.component("asyncComponent", (resolve, reject)=>{
require.ensure([], ()=>{
resolve(require('./test.vue'))
}, "asyncComponent") // 这里的 asyncComponent 为 chunkName
})
import(/* webpackChunkName: "chunkName" */, component):
在ES6
,import
方法是推荐的写法, 通过注释webpackChunkName
来指定分离后组件模块的命名
Vue.component('asyncComponent', () => import(/* webpackChunkName: "asyncComponent" */, './test.vue'))