异步组件
为什么须要异步组件
Vue
作为单页面利用在加载首页时常会遇到加载迟缓的问题,导致在应用体验较差,这是因为在打包单页面利用时,页面会吧脚本打包成一个文件,这个文件蕴含了所有业务和非业务的代码, 脚本文件过大导致渲染页面时迟缓。
在进行首屏性能优化时,最罕用的办法就是对于文件的拆分和代理的拆散,按需加载的概念也是在这个前提下引入的。因而在Vue
开发过程中,咱们会把一些非首屏的组件设计成异部组件,局部不影响首次视觉体验的组件也能够设计成异步组件。
定义异步组件
在 Vue
中,在注册组件时应用一个工厂函数定义组件,这个工厂函数会异步解析组件定义,只有当这个组件须要被渲染时才会触发该工厂函数,并且会把后果缓存起来以便后续应用。
// 注册全局组件时,定义为异步组件Vue.component("async-component", (resolve, reject)=>{ setTimeout(()=>{ // 应用一个定时器来模仿异步加载过程 // resolve 须要返回一个组件的定义对象,该对象的属性与定义一般组件的属性统一 resolve({ template: "<div>async-component</div>" }) }, 2000)})// 注册部分异步组件const vm = new Vue({ components:{ asyncComponent: ()=> import('./test.vue') }})
异步组件流程剖析
在组件根底的剖析过程中,咱们剖析了实例的挂载流程分为依据渲染函数创立 Vnode
和依据 Vnode
创立实在 DOM
的过程。在创立 Vnode
的过程中,如果遇到组件占位符,会调用 createComponent
,在该办法中,会为子组件做选项合并和装置钩子函数。异步组件的解决也是在该函数中进行的。
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 } /** * 这里的 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<Component>): Class<Component> | 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<Component>) => {}) 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<Component>) => { // 专程组件结构器,并缓存到 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'))