为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件来按需加载
Vue也原生支持了异步组件的能力
Vue.component('async-example', function (resolve, reject) {// 这个特殊的 require 语法告诉 webpack// 自动将编译后的代码分割成不同的块,// 这些块将通过 Ajax 请求自动下载。require(['./my-async-component'], resolve)})
Vue注册的组件不再是一个对象而是一个工厂函数,函数参数resolve和reject,函数内部用setTimeout模拟了异步,实际使用可能是通过动态请求异步组件的js地址,最终通过执行resolve方法,它的参数就是异步组件对象
异步组件实现
createComponent函数,定义在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 {if (isUndef(Ctor)) {return}const baseCtor = context.$options._base// plain options object: turn it into a constructorif (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 componentlet asyncFactory// Ctor是一个函数且不会执行Vue.extend逻辑,所以Ctor.id是undefinedif (isUndef(Ctor.cid)) {asyncFactory = CtorCtor = 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)}}// ...}
resolveAsyncComponent方法,定义在src/core/vdom/helpers/resolve-async-component.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 = currentRenderingInstanceif (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {// already pendingfactory.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 = truelet timerLoading = nulllet 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()}if (renderCompleted) {owners.length = 0if (timerLoading !== null) {clearTimeout(timerLoading)timerLoading = null}if (timerTimeout !== null) {clearTimeout(timerTimeout)timerTimeout = null}}}const resolve = once((res: Object | Class<Component>) => {// cache resolvedfactory.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 = trueforceRender(true)}})const res = factory(resolve, reject)if (isObject(res)) {if (isPromise(res)) {// () => Promiseif (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 {timerLoading = setTimeout(() => {timerLoading = nullif (isUndef(factory.resolved) && isUndef(factory.error)) {factory.loading = trueforceRender(false)}}, res.delay || 200)}}if (isDef(res.timeout)) {timerTimeout = setTimeout(() => {timerTimeout = nullif (isUndef(factory.resolved)) {reject(process.env.NODE_ENV !== 'production'? `timeout (${res.timeout}ms)`: null)}}, res.timeout)}}}sync = false// return in case resolved synchronouslyreturn factory.loading? factory.loadingComp: factory.resolved}}
处理了3种异步组件的创建方式
普通函数异步组件
Vue.component('async-example', function (resolve, reject) {// 这个特殊的 require 语法告诉 webpack// 自动将编译后的代码分割成不同的块,// 这些块将通过 Ajax 请求自动下载。require(['./my-async-component'], resolve)})
Promise异步组件
Vue.component('async-webpack-example',// 该 `import` 函数返回一个 `Promise` 对象。() => import('./my-async-component'))
高级异步组件
const AsyncComp = () => ({// 需要加载的组件。应当是一个 Promisecomponent: import('./MyComp.vue'),// 加载中应当渲染的组件loading: LoadingComp,// 出错时渲染的组件error: ErrorComp,// 渲染加载中组件前的等待时间。默认:200ms。delay: 200,// 最长等待时间。超出此时间则渲染错误组件。默认:Infinitytimeout: 3000})Vue.component('async-example', AsyncComp)
普通函数异步组件
resolve和reject函数用once函数做了一层包装,定义在src/shared/util.js中
/*** Ensure a function is called only once.* 利用闭包和一个标志位保证了它包装的函数只会执行一次*/export function once (fn: Function): Function {let called = falsereturn function () {if (!called) {called = truefn.apply(this, arguments)}}}
确保resolve和reject函数只执行一次
执行constres = factory(resolve, reject)就是执行组件的工厂函数,同时把resolve和reject函数作为参数传入,组件的工厂函数通常会发送请求去加载异步组件的js文件,拿到组件定义的对象res后执行resolve(res),会先执行factory.resolved = ensureCtor(res, baseCtor)// 为了保证能找到异步组件js定义的组件对象// 如果它是一个普通对象则调用Vue.extend把它转换成一个组件的构造函数function ensureCtor (comp: any, base) {if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) {comp = comp.default}return isObject(comp)? base.extend(comp): comp}
之后resolve中判断了sync,该场景下sync为false,那么就会执行forceRender函数
const forceRender = (renderCompleted: boolean) => {// 遍历factory.contexts,拿到每个调用异步组件的实例vm - owners[i]for (let i = 0, l = owners.length; i < l; i++) {(owners[i]: any).$forceUpdate()}if (renderCompleted) {owners.length = 0if (timerLoading !== null) {clearTimeout(timerLoading)timerLoading = null}if (timerTimeout !== null) {clearTimeout(timerTimeout)timerTimeout = null}}}
$forceUpdate()定义在src/core/instance/lifecycle.js
Vue.prototype.$forceUpdate = function () {const vm: Component = this// 调用渲染watcher的update方法,让渲染wacher对应的回调函数执行,也就是触发了组件的重新渲染if (vm._watcher) {vm._watcher.update()}}
Vue通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行$forceUpdate可以强制组件重新渲染一次
Promise异步组件
Vue.component('async-webpack-example',// 该 `import` 函数返回一个 `Promise` 对象。() => import('./my-async-component'))
webpack 2+ 支持了异步加载的语法糖:() => import(‘./my-async-component’)
当执行完 res = factory(resolve, reject),返回的值就是 import(‘./my-async-component’) 的返回值,它是一个 Promise 对象if (isObject(res)) {if (isPromise(res)) {// () => Promiseif (isUndef(factory.resolved)) {res.then(resolve, reject)}// ...}}
当组件异步加载成功后执行resolve,加载失败则执行reject,巧妙实现了配合webpack2+ 的异步加载组件的方式(Promise)加载异步组件
高级异步组件
Vue.js 2.3+支持了一种高级异步组件的方式,通过一个简单的对象配置搞定loading组件和error组件的渲染时机
const AsyncComp = () => ({// 需要加载的组件。应当是一个 Promisecomponent: import('./MyComp.vue'),// 加载中应当渲染的组件loading: LoadingComp,// 出错时渲染的组件error: ErrorComp,// 渲染加载中组件前的等待时间。默认:200ms。delay: 200,// 最长等待时间。超出此时间则渲染错误组件。默认:Infinitytimeout: 3000})Vue.component('async-example', AsyncComp)
高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent,当执行完 res = factory(resolve, reject),返回值就是定义的组件对象
满足isPromise(res.component)为trueif (isObject(res)) {if (isPromise(res)) {// ...} else if (isPromise(res.component)) {// 异步组件加载成功后执行resolve,失败执行rejectres.component.then(resolve, reject)}}
异步组件加载是一个异步的过程,接着又同步执行
// 判断是否定义了error组件if (isDef(res.error)) {factory.errorComp = ensureCtor(res.error, baseCtor)}// 判断是否定义了loading组件if (isDef(res.loading)) {factory.loadingComp = ensureCtor(res.loading, baseCtor)// 如果delay配置为0,则这次直接渲染loading组件,否则延时delay执行forceRender,那么又会再一次执行到resolveAsyncComponentif (res.delay === 0) { // 是否设置看res.delay且为0factory.loading = true} else { // 否则延时delay的时间执行timerLoading = setTimeout(() => {timerLoading = nullif (isUndef(factory.resolved) && isUndef(factory.error)) {factory.loading = trueforceRender(false)}}, res.delay || 200)}}// 是否配置了timeoutif (isDef(res.timeout)) {timerTimeout = setTimeout(() => {timerTimeout = null// 如果组件没有成功加载执行rejectif (isUndef(factory.resolved)) {reject(process.env.NODE_ENV !== 'production'? `timeout (${res.timeout}ms)`: null)}}, res.timeout)}
最后一部分
sync = false// return in case resolved synchronouslyreturn factory.loading? factory.loadingComp: factory.resolved
异步组件加载失败
当异步组件加载失败会执行reject函数
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 = trueforceRender(true)}})
把factory.error设置为true,同时执行forceRender()再次执行到resolveAsyncComponent
if (isTrue(factory.error) && isDef(factory.errorComp)) {return factory.errorComp}
返回factory.errorComp,直接渲染error组件
异步组件加载成功
当异步组件加载成功会执行resolve函数
const resolve = once((res: Object | Class<Component>) => {// cache resolvedfactory.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}})
把加载结果缓存到factory.resolved中,这时sync为false,则执行forceRender()再次执行到resolveAsyncComponent
if (isDef(factory.resolved)) {return factory.resolved}
直接返回factory.resolved,渲染成功加载的组件
异步组件加载中
如果异步组件加载中并未返回
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {return factory.loadingComp}
会返回factory.loadingComp,渲染loading组件
异步组件加载超时
如果超时则走到reject,和加载失败一样,渲染error组件
异步组件patch
如果第一次执行resolveAsyncComponent,除非使用高级异步组件 0 delay去创建了一个loading组件,否则返回的是undefined
// async componentlet asyncFactoryif (isUndef(Ctor.cid)) {asyncFactory = CtorCtor = 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.// 通过createAsyncPlaceholder创建一个注释节点作为占位符return createAsyncPlaceholder(asyncFactory,data,context,children,tag)}}
createAsyncPlaceholder方法定义在src/core/vdom/helpers/resolve-async-components.js中
export function createAsyncPlaceholder (factory: Function,data: ?VNodeData,context: Component,children: ?Array<VNode>,tag: ?string): VNode {const node = createEmptyVNode()node.asyncFactory = factorynode.asyncMeta = { data, context, children, tag }return node}
实际上就是创建一个占位的注释VNode,同时把asyncFactory和asyncMeta赋值给当前vnode
当执行 forceRender 的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 render、patch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的总结
3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态
异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了
