1. 异步组件实现原理
defineAsyncComponent
函数是一个高阶组件 ,他的返回值是一个包装组件。此包装组件会根据状态来决定渲染的内容,加载成功后渲染组件 ,在未渲染成功时渲染一个占位符节点
1.1 基本用法
使用:
js
let asyncComponent = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
render: () => h("div", "hi james"),
});
}, 1000);
});
});
实现:
js
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (isFunction(source)) {
source = { loader: source }
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = true,
onError: userOnError
} = source
const load = (): Promise<ConcreteComponent> => {
let thisRequest: Promise<ConcreteComponent>;
// ... 省略部分代码,下面再说明
}
return defineComponent({
name: 'AsyncComponentWrapper',
// ... 省略部分代码
setup() {
const loaded = ref(false);
// 执行这个Promise,等待promise的执行结果,等待之前渲染片段,等待成功后,将成功态的结果赋给要渲染的组件
load()
.then(() => {
loaded.value = true;
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// 判断当前组件的父组件是否为一个 keep-alive 组件,并且该 keep-alive 组件的 vnode 属性存在。如果是的话,我们会强制更新父组件,以便让 loaded 组件的名称也被考虑在内。
queueJob(instance.parent.update);
}
})
.catch((err) => {
// 处理异步加载组件时可能出现的错误。如果出现错误,我们会调用 onError() 方法来进行错误处理,并将 error.value 属性设置为错误对象
onError(err);
error.value = err;
});
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
}
})
}
1.2 异步组件超时处理
js
let asyncComponent = defineAsyncComponent({
loader: () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
render() {
return h("div", "hi james");
},
});
}, 1000);
});
},
timeout: 2000, // 异步组件超时时间
errorComponent: {
render: () => h("Text", "超时错误"),
},
});
js
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (isFunction(source)) {
source = { loader: source }
}
return defineComponent({
setup() {
const loaded = ref(false)
const error = ref(); // 是否超时
if (timeout != null) {
// 根据超时时间,开个定时器,时间一到就标记已超时
setTimeout(() => {
if (!loaded.value && !error.value) {
const err = new Error(
`Async component timed out after ${timeout}ms.`
)
onError(err)
error.value = err // 显示错误组件
}
}, timeout)
}
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent)
}
}
},
});
}
组件卸载的时候需要稍作处理
js
const unmount = (vnode) => {
const { shapeFlag } = vnode;
if (vnode.type === Fragment) {
return unmountChildren(vnode.children);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 如果是组件的话就移除
return unmount(vnode.component.subTree); // 移除组件
}
hostRemove(vnode.el);
};
1.3 异步组件 loading 处理
js
let asyncComponent = defineAsyncComponent({
loader: () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
render() {
return h("div", "hi james");
},
});
}, 3000);
});
},
timeout: 2000,
errorComponent: {
render: () => h("Text", "超时才显示错误"),
},
delay: 1000,
loadingComponent: {
render: () => h("h2", "loading...."),
},
});
js
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
T extends Component = { new(): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (isFunction(source)) {
source = { loader: source }
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = true,
onError: userOnError
} = source
const loaded = ref(false)
const error = ref()
const delayed = ref(!!delay)
// 延时显示loading
if (delay) {
setTimeout(() => {
delayed.value = false
}, delay)
}
return () => {
if (loaded.value && resolvedComp) {
// 组件加载完毕显示
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
// 显示错误组件
return createVNode(errorComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
// 显示loading组件
return createVNode(loadingComponent)
}
}
}
1.4 异步组件加载重试处理
js
let asyncComponent = defineAsyncComponent({
loader: () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({
render() {
return h("div", "hi james");
},
});
}, 3000);
});
},
timeout: 2000,
errorComponent: {
render: () => h("Text", "超时错误"),
},
delay: 1000,
loadingComponent: {
render: () => h("h2", "loading...."),
},
onError(retry) {
console.log("错了");
retry();
},
});
js
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
T extends Component = { new(): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (isFunction(source)) {
source = { loader: source }
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = true,
onError: userOnError
} = source
let pendingRequest: Promise<ConcreteComponent> | null = null
let resolvedComp: ConcreteComponent | undefined
let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}
const load = (): Promise<ConcreteComponent> => {
let thisRequest: Promise<ConcreteComponent>
return (
pendingRequest ||
(thisRequest = pendingRequest =
loader()
.catch(err => {
err = err instanceof Error ? err : new Error(String(err))
if (userOnError) {
return new Promise((resolve, reject) => {
// 重新加载包装为成功状态的promise作为函数继续执行
const userRetry = () => resolve(retry())
const userFail = () => reject(err)
userOnError(err, userRetry, userFail, retries + 1)
})
} else {
throw err
}
})
.then((comp: any) => {
// ... 省略部分代码
}))
)
}
}
2. 函数式组件实现原理
函数式组件(functional-components)本质就是一个函数 ,函数的返回值就是虚拟 DOM 。 在 Vue 3
中,所有的函数式组件都是用普通函数创建 的。换句话说,不需要定义 { functional: true }
组件选项。
js
export const createVNode = (type, props, children = null) => {
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 带状态的对象形式组件(业务组件用的多)
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT // 纯函数组件
: 0;
// 创建虚拟节点是
};
js
function initProps(instance, propsOptions, propsData) {
// ... 属性初始化的时候如果是函数式组件,则 attrs 就是函数式组件的props
if (instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
instance.props = attrs;
}
}
产生
subTree
时, 要根据类型做不同的处理
js
export function renderComponentRoot(instance) {
let { render, proxy, vnode, props } = instance;
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
return render.call(proxy, proxy);
} else {
return vnode.type(props); // 函数式组件直接调用即可
}
}
const subTree = renderComponentRoot(instance);
3. Provide/Inject 原理
在 vue3 中可以使用
provide
和inject
来替代 vue2 的全局属性挂载app.config.globalProperties
Tips: ⚠️ 依赖注入时,注入的数据来源不明确(需要指定唯一的 key 值)。不适合在业务代码中使用,可能会带来重构复杂的问题。(一般用于编写插件使用)
3.1 Vue3 中依赖注入原理
在创建实例时会采用父组件的
provides
属性
js
const instance: ComponentInternalInstance = {
uid: uid++,
provides: parent ? parent.provides : Object.create(appContext.provides),
};
3.2 Provide 源码分析
provides
属性会向上查找父组件provides
属性
js
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T
) {
if (!currentInstance) {
if (__DEV__) {
// Provide 只能在 setup 方法里面使用
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides // 默认的情况下,当前实例会继承父类的provides对象
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) { // 如果组件提供自己的provide的时候,就会用父组件的provides作为原型链创建新的对象
// 然后在 inject 中,我们可以从实例中查找注入父函数,让原型链做这些工作。
provides = currentInstance.provides = Object.create(parentProvides)
}
// 在TS中不允许符号来作为索引类型
provides[key as string] = value
}
}
3.3 Inject 源码分析
会查找
provides
中是否包含需要注入的属性
如何获取 provide 的值呢?
重点:如果当前实例是根节点,那就会回退到当前实例的上下文中拿;否则就是从父级实例的 provides 中拿
js
export function inject(
key: InjectionKey<any> | string, // 需要注入的属性
defaultValue?: unknown, // 提供属性的默认值
treatDefaultAsFactory = false // 属性默认值是否为工厂函数形式
) {
// 这里回退到currentRenderingInstance,是为了便于 functional components 来调用
const instance = currentInstance || currentRenderingInstance
// 支持从 app-level 的 provides 中查找,使用 app.runWithContext() 方法
if (instance || currentApp) {
// 也支持 app.use 插件
// 重点:获取provides值:如果当前实例是根节点,那就会回退到当前实例的上下文中拿;否则就是从父级实例的provides中拿
const provides = instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: currentApp!._context.provides
if (provides && (key as string | symbol) in provides) {
// 在TS中不允许符号来作为索引类型
return provides[key as string] // 则将属性返回
} else if (arguments.length > 1) { // 注入式可以采用默认参数注入
// provides中找不到注入的属性,就读取默认值(可以支持传入一个函数)
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
// inject 只能在 setup 方法里面或者函数组件中使用
warn(`inject() can only be used inside setup() or functional components.`)
}
}
总结
本文主要介绍了 Vue3
中的异步组件、函数式组件和依赖注入的实现原理。
1、异步组件实现原理:Vue3
使用 defineAsyncComponent
函数定义异步组件,该函数返回一个包装组件,根据状态决定渲染内容。异步组件支持超时处理、加载状态处理、重试处理等功能。
2、函数式组件实现原理:Vue3
中的函数式组件是一个返回虚拟 DOM 的普通函数,不需要定义 { functional: true }
组件选项。在渲染组件时,根据 vnode.shapeFlag
的类型进行处理。
3、Provide/Inject
原理:Vue3
使用 provide
和 inject
方法实现依赖注入。在创建实例时,会采用父组件的 provides
属性。provide
方法用于在当前实例中提供一个值,而 inject
方法用于在当前实例中注入一个值。注入时会向上查找父组件的 provides
属性,如果找不到,则使用默认值。