1、异步组件三种用法
//方式1
Vue.component('async-component', function (resolve, reject) {
require(['./MyAsyncComponent'], resolve);
});
//方式2
Vue.component(
'async-webpack-example',
() => import('./my-async-component')
)
//方式3
Vue.component('async-example', () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
}))
2、使用场景
如果我们需要展示某个产品的信息,产品关联的tab页面有20个,使用异步组件就使得代码很简单,减少了组件的注册
<template>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="基本信息" name="first"></el-tab-pane>
<el-tab-pane label="配置信息" name="second"></el-tab-pane>
<el-tab-pane label="角色信息" name="third"></el-tab-pane>
<el-tab-pane label="部门信息" name="fourth"></el-tab-pane>
...
<el-tab-pane label="其他信息" name="twenty"></el-tab-pane>
</el-tabs>
<async-webpack-example />
</template>
<script>
export default {
data() { return { activeName: 'second' }; },
methods: {
handleClick(path) {
Vue.component(
'async-webpack-example',
() => import('./my-async-component/'+ path)
)
}
}
};
</script>
3、源码
src\core\vdom\create-component.ts
3.1 createComponent
- Ctor.cid为空,则为异步组件,使用 resolveAsyncComponent 加载。
- 创建一个占位符组件,在异步组件加载完成前用于渲染。
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
context: Component,
children?: Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// 由于我们这个时候传入的 `Ctor` 是一个函数,那么它也并不会执行 `Vue.extend` 逻辑,因此它的 `cid` 是 `undefiend`,进入了异步组件创建的逻辑
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor as typeof Component)
}
if (typeof Ctor !== 'function') {
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
// 1、异步组件加载
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 2、创建一个占位符
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}
data = data || {}
resolveConstructorOptions(Ctor as typeof Component)
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 函数组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
propsData,
data,
context,
children
)
}
const listeners = data.on
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
3.2 resolveAsyncComponent
src\core\vdom\helpers\resolve-async-component.ts
- 执行 factory 方法
- 异步组件使用方式2、3返回一个promise对象,import后执行 forceRender->forceUpdate
function ensureCtor(comp: any, base) {
if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
comp = comp.default
}
return isObject(comp) ? base.extend(comp) : comp
}
export function createAsyncPlaceholder(
factory: Function,
data: VNodeData | undefined,
context: Component,
children: Array<VNode> | undefined,
tag?: string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
export function resolveAsyncComponent(
factory: { (...args: any[]): any; [keye: string]: any },
baseCtor: typeof Component
): typeof 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) {
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: number | null = null
let timerTimeout: number | null = null
owner.$on('hook:destroyed', () => remove(owners, owner))
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
owners[i].$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 | Component) => {
factory.resolved = ensureCtor(res, baseCtor) //----factory.resolved---
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
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 {
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(null)
}
}, res.timeout)
}
}
}
sync = false
return factory.loading ? factory.loadingComp : factory.resolved
}
}
3.3 forceRender
当执行 forceRender forceUpdate
的时候, 会触发组件的重新渲染,见 vm._update(vm.render())那么会再一次执行 resolveAsyncComponent
,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined
,因此就走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode
的
src\core\instance\lifecycle.ts
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
4、疑问
疑问:webpack编译是按需加载,也就是我们用到了哪些代码,才会进行编译打包。当我们使用异步组件的时候,path是一个变量,如何做到把这些路径的文件提前编译呢?
带着这样的疑问我们来先看看 如下的demo效果吧
// 静态导入
import './my-async-component/a'
// 动态导入
import(path)
import('./my-async-component/'+ path)
我们写个demo,可以看出
- import('./com/'+ path)
是将'./com/'下所有的文件提前编译了,文件src/dd.js就没有被编译
-
import(path) webpack entry:'./src/index.js', 是将src 下所有的文件提前编译了,文件src/dd.js就被编译了
-
import(path) webpack entry:'./index.js', 是将整个项目所有的文件提前编译了,包括 node_modules
通过上面我们可以看出,webpack import 动态导入的时候,会根据 路径 进行提前编译,当我们路径是变量的时候,会根据不同的情况编译不同内容,为了防止非必要的文件被提前编译,我们的路径尽可能要写的明确一点
5、webpack 打包 import动态编译源码
想要了解去另外一篇 webpack编译require、import 吧
欢迎关注我的前端自检清单,我和你一起成长