异步组件与函数式组件
我们已经详细讨论了组件的基本含义与实现。本章,我们将继续讨论组件的两个重要概念,即异步组件和函数式组件。在异步组件中,“异步” 二字指的是,以异步的方式加载并渲染一个组件。这在代码分割、服务端下发组件等场景中尤为重要。而函数式组件允许使用一个普通函数定义组件,并使用该函数的返回值作为组件要渲染的内容。函数式组件的特点是:无状态、编写简单且直观。在 Vue.js 2 中,相比有状态组件来说,函数式组件具有明显的性能优势。但在 Vue.js 3 中,函数式组件与有状态组件的性能差距不大,都很好。正如 Vue.js RFC 的原文所述:“在 Vue.js 3 中使用函数式组件,主要是因为它的简单性,而不是因为它的性能好”。
异步组件要解决的问题
从根本上来说,异步组件的实现不需要任何框架层面的支持,用户完全可以自行实现。渲染 App
组件到页面的示例如下:
import App from 'App.vue'
createApp(App).mount('#app')
js
上面这段代码所展示的就是同步渲染。我们可以轻易地将其修改为异步渲染,如下面的代码所示:
const loader = () => import('App.vue')
loader().then(App => {
createApp(App).mount('#app')
})
js
这里我们使用动态导入语句 import()
来加载组件,它会返回一个 Promise 实例。组件加载成功后,会调用 createApp
函数完成挂载,这样就实现了以异步的方式来渲染页面。
上面的例子实现了整个页面的异步渲染。通常一个页面会有多个组件构成,每个组件负责渲染页面的一部分。那么,如果只想异步渲染部分页面,要怎么办呢?这时,只需要有能力异步加载某一个组件就可以了。假设下面的代码是 App.vue
组件的代码。
<template>
<CompA />
<component :is="asyncComp" />
</template>
<script>
import { shallowRef } from 'vue'
import CompA from 'CompA.vue'
export default {
components: { CompA },
setup() {
const asyncComp = shallowRef(null)
// 异步加载 Comp 组件
import('CompB.vue').then(CompB => asyncComp.value = CompB)
return {
asyncComp
}
}
}
</script>
从这段代码的模板中可以看出,页面是由 <CompA />
组件和动态组件 <component>
构成。其中,CompA
组件是同步渲染的,而动态组件绑定了 asyncComp
变量。继续看脚本块,我们通过动态导入语句 import()
来异步加载 CompB
组件,当加载成功后,将 asyncComp
变量的值设置为 CompB
。这样就实现了 CompB
组件的异步加载和渲染。
不过,虽然用户可以自行实现组件的异步加载和渲染,但整体实现还是比较复杂的,因为一个完善的异步组件的实现,所涉及的内容要比上面的例子负责的多。通常在异步加载组件时,我们还要考虑以下几个方面。
- 如果组件加载失败或加载超时,是否要渲染 Error 组件?
- 组件在加载时,是否要展示占位的内容?例如渲染一个 Loading 组件。
- 组件加载的速度可能很快,也可能很慢,是否要设置一个延迟展示 Loading 组件的时间?如果组件在 200 ms 内没有加载成功才展示 Loading 组件,这样可以避免由组件加载过快所导致的闪烁。
- 组件加载时候后,是否需要重试?
为了替用户更好地解决上述问题,我们需要在框架层面为异步组件提供更好的封装支持,与之对应的能力如下:
- 允许用户指定加载出错时要渲染的组件。
- 允许用户指定 Loading 组件,以及展示该组件的延迟时间。
- 允许用户设置加载组件的超时时长。
- 组件加载失败时,为用户提供重试的能力。
以上这些内容就是异步组件真正要解决的问题。
异步组件的实现原理
封装 defineAsyncComponent
异步组件本质上是通过封装手段来实现友好的用户接口,从而降低用户层面的使用复杂度,如下面的用户代码所示:
<template>
<AsynComp />
</template>
<script>
export default {
components: {
// 使用 definedAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
AsyncComp: defineAsyncComponent(() => import('CompA'))
}
}
</script>
在上面这段代码中,我们使用 defineAsyncComponent
来定义异步组件,并直接使用 components 组件选项来注册它。这样,在模板中就可以像使用普通组件一样使用异步组件了。可以看到,使用 defineAsyncComponent
函数定义异步组件的方式,比我们自己实现的异步组件方式要简单直接得多。
defineAsyncComponent
是一个高阶组件,它最基本的实现如下:
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(loader) {
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
async: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
loader().then(c => {
InnerComp = c
loaded.value = true
})
return () => {
// 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
return loaded.value ? { type: InnerComp } : { type: Text, children: '' }
}
}
}
}
js
这里有以下几个关键点:
defineAsyncComponent
函数本质上是一个高阶组件,它的返回值是一个包装组件。- 包装组件会根据加载器的状态来决定渲染什么内容。如果加载器成功创建了组件,则渲染被加载的组件,否则只会渲染一个占位内容。
- 通常占位内容是一个注释节点。组件没有被加载成功后,页面中会渲染一个注释节点来占位。这里我们使用了一个空文本节点来占位。
超时与 Error 组件
异步组件通常是以网路请求的形式进行加载。前端发送一个 HTTP 请求,请求下载组件的 JavaScript 资源,或者从服务器直接获取组件数据。既然存在网络请求,那么必然要考虑网络较慢的情况,尤其是在弱网环境下,加载一个组件可能要需要很长时间。因此,我们需要为用户提供指定超时时长的能力,当加载组件的时间超过指定时长后,会触发超时错误。这时如果用户配置了 Error 组件,则会渲染组件。
首先,我们来设计用户接口。为了让用户能够指定超时时长,defineAsyncComponent
函数需要接收一个配置对象作为参数:
const AsyncComp = defineAsyncComponent({
loader: () => import('CompA.vue'),
timeout: 2000, // 超时时长,其单位为 ms
errorComponent: MyErrorComp // 指定出错时要渲染的组件
})
js
- loader:指定异步组件的加载器
- timeout:单位为 ms,指定超时时长
errorComponent
:指定一个 Error 组件,发生错误时会渲染它
设计好用户接口之后,我们就可以给出具体实现了:
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(options) {
// options 既可以是配置项,也可以是加载器
if (typeof options === 'function') {
// 如果是 options 是加载器,将其格式化配置项形式
options = {
loader: options
}
}
const { loader } = options
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
async: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 代表是否超时,默认为 false
const timeout = ref(false)
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
loader().then(c => {
InnerComp = c
loaded.value = true
})
let timer = null
if (options.timeout) {
// 如果指定超时时长,开启一个定时器
timer = setTimeout(() => {
// 超时后将 timeout 设置为 true
timeout.value = true
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUnmounted(() => clearTimeout(timer))
// 占位内容
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
// 如果异步组件加载成功,渲染该组件
return { type: InnerComp }
} else if (timeout.value) {
// 如果加载超时,并且用户指定 Error 组件,则渲染该组件
return options.errorComponent ? { type: options.errorComponent } : placeholder
}
// 渲染一个占位内容
return { type: Text, children: '' }
}
}
}
}
js
整体实现并不复杂,关键点如下:
- 需要一个标志变量来标识异步组件的加载是否已经超时,即
timeout.value
。 - 开始加载组件的同时,开启一个定时器进行计时。当加载超时后,将
timeout.value
的值设置为 true,代表加载已经超时。这里需要注意的是,当包装组件被卸载时,需要清除定时器。 - 包装组件根据 loaded 变量的值以及 timeout 变量的值来决定具体的渲染内容。如果异步组件加载成功,则渲染被加载的组件;如果异步组件加载超时,并且用户指定 Error 组件,则渲染 Error 组件。
这样,我们就实现了对加载超时的兼容,以及对 Error 组件的支持。除此之外,我们希望有更加完善的机制来处理异步组件加载过程中发生的错误,超时只是错误的原因之一。基于此,我们还希望为用户提供以下能力。
- 当错误发生时,把错误对象作为 Error 组件的 props 传递过去,以便用户后续能自行进行更细粒度的处理。
- 除了超时之外,有能力处理其他原因导致的加载错误,例如网络失败等。
为了实现这两个目标,我们需要对代码做一些调整。
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(options) {
// options 既可以是配置项,也可以是加载器
if (typeof options === 'function') {
// 如果是 options 是加载器,将其格式化配置项形式
options = {
loader: options
}
}
const { loader } = options
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
async: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 代表是否超时,默认为 false
const timeout = ref(false)
// 定义 error,当错误发生时,用来存储错误对象
const error = shallowRef(null)
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
loader()
.then(c => {
InnerComp = c
loaded.value = true
})
// 添加 catch 语句来捕获加载过程中的错误
.catch(err => error.value = err)
let timer = null
if (options.timeout) {
// 如果指定超时时长,开启一个定时器
timer = setTimeout(() => {
// 超时后创建一个错误对象,并赋值给 error.value
const err = new Error(`Async component timed out after ${ options.timeout }ms.`)
err.value = err
// 超时后将 timeout 设置为 true
timeout.value = true
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUnmounted(() => clearTimeout(timer))
// 占位内容
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
// 如果异步组件加载成功,渲染该组件
return { type: InnerComp }
} else if (timeout.value && options.errorComponent) {
// 如果加载超时,并且用户指定 Error 组件,则渲染该组件
// 同时将 error 作为 props 传递
return { type: options.errorComponent, props: { error: error.value } }
}
// 渲染一个占位内容
return { type: Text, children: '' }
}
}
}
}
js
观察上面的代码,我们对之前的实现做了一些调整,首先,为加载器添加 catch 语句来捕获所有加载错误。接着,当加载超时后,我们会创建一个新的错误对象,并将其赋值给 error.value
变脸。在组件渲染时,只要 error.value
值存在,且用户配置了 errorComponent
组件,就直接渲染 errorComponent
组件并将 error.value
的值作为该组件的 props 传递。这样,用户就可以在自己的 Error 组件上,通过定义名称 error 的 props 来接收错误对象,从而实现细粒度的控制。
延迟与 Loading 组件
异步加载组件受网络影响比较大,加载过程可能很慢,也可能很快。这时我们就会很自然地想到,对于第一种情况,我们能否通过展示 Loading 组件来提供更好的用户体验。这样,用户就不会有 “卡死” 的感觉了。这时一个好想法,但展示 Loading 组件的时机是一个需要仔细考虑的问题。通常,我们会从加载开始的那一刻起就展示 Loading 组件。但在组件状况良好的情况下,异步组件的加载速度会非常快,这会导致 Loading 组件刚完成渲染就立即进入卸载阶段,于是出现闪烁的情况。对于用户来说这是非常不好的体验。体验,我们需要为 Loading 组件设置一个延迟展示的时间。例如,当超过 200 ms 没有完成加载,才展示 Loading 组件。这样,对于在 200 ms 内能够完成加载的情况来说,就避免了闪烁问题的出现。
不过,我们首先要考虑的仍然是用户接口的设计,如下面的代码所示:
const AsyncComp = defineAsyncComponent({
loader: () => import('CompA.vue'),
// 延迟 200 ms 展示 Loading 组件
delay: 200,
// Loading 组件
loadingComponent: [
setup() {
return () => {
return { type: 'h2', children: 'Loading...' }
}
}
]
})
js
- delay,用于指定延迟展示的 Loading 组件的时长
loadingComponent
:类似于errorComponent
选项,用来配置 Loading 组件。
用户接口设计完成后,我们就可以着手实现了。
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(options) {
// options 既可以是配置项,也可以是加载器
if (typeof options === 'function') {
// 如果是 options 是加载器,将其格式化配置项形式
options = {
loader: options
}
}
const { loader } = options
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
async: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 定义 error,当错误发生时,用来存储错误对象
const error = shallowRef(null)
// 代表是否超时,默认为 false
const timeout = ref(false)
// 代表是否正在加载,默认为 false
const loading = ref(false)
let loadingTimer = null
// 如果配置项中存在 delay,则开启一个定时器计时
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
// 如果配置项中没有 delay,则直接标记为加载中
loaded.value = true
}
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
loader()
.then(c => {
InnerComp = c
loaded.value = true
})
// 添加 catch 语句来捕获加载过程中的错误
.catch(err => error.value = err)
// 加载完毕后,无论成功与否都要清除延迟定时器
.finally(() => {
loaded.value = false
clearTimeout(loadingTimer)
})
let timer = null
if (options.timeout) {
// 如果指定超时时长,开启一个定时器
timer = setTimeout(() => {
// 超时后创建一个错误对象,并赋值给 error.value
const err = new Error(`Async component timed out after ${ options.timeout }ms.`)
err.value = err
// 超时后将 timeout 设置为 true
timeout.value = true
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUnmounted(() => clearTimeout(timer))
// 占位内容
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
// 如果异步组件加载成功,渲染该组件
return { type: InnerComp }
} else if (timeout.value && options.errorComponent) {
// 如果加载超时,并且用户指定 Error 组件,则渲染该组件
// 同时将 error 作为 props 传递
return { type: options.errorComponent, props: { error: error.value } }
} else if (loaded.value && options.loadingComponent) {
// 如果异步组件正在加载,并且用户指定了 Loading 组件,则渲染 Loading 组件
return { type: options.loadingComponent }
}
// 渲染一个占位内容
return { type: Text, children: '' }
}
}
}
}
js
整体实现思路类似于超时时长与 Error 组件,有以下几个关键点:
- 需要一个标记变量 loading 来代表组件是否正在加载;
- 如果用户指定了延迟时间,则开启延迟定时器。定时器到时后,再将
loading.value
的值设置为 true; - 无论组件加载成功与否,都要清除延迟定时器,否则会出现组件已经加载成功,但仍然展示 Loading 组件的问题;
- 在渲染函数中,如果组件正在加载,并且用户指定了 Loading 组件,则渲染该 Loading 组件。
另外有一点需要注意,在异步组件加载成功后,会卸载 Loading 组件并渲染异步加载的组件。为了支持 Loading 组件的加载,我们需要修改 unmount 函数。
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
} else if (typeof vnode.type === 'object') {
// 对于组件卸载,本质上是要卸载组件所渲染的内容,即 subTree
unmount(vnode.component.subTree)
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
对于组件的卸载,本质上是要卸载组件所渲染的内容,即 subTree
。所以在上面的代码中,我们通过组件实例的 vnode.component
属性得到组件实例,再递归地调用 unmount
函数完成 vnode.component.subTree
的卸载。
重试机制
重试指的是当加载出错时,有能力重新发起加载组件的请求。在加载组件的过程中,发生错误的情况非常常见。尤其是在网络不稳定的情况下。因此,提供开箱即用的重试机制,会提升用户的开发体验。
异步组件加载失败后的重试机制,与请求服务端接口失败后的重试机制一样。所以,我们先来讨论接口请求失败后的重试机制是如何实现的。为此,我们需要封装一个 fetch 函数,用来模拟接口请求:
function fetch() {
return new Promise((resolve, reject) => {
// 请求会在 1 秒后失败
setTimeout(() => {
reject('err')
}, 1000)
})
}
js
假设调用 fetch 函数会发送 HTTP 请求,并且该请求会在 1 秒后失败。为了实现失败后的重试,我们需要封装一个 load 函数,如下面的代码所示:
// load 函数接收一个 onError 函数
function load(onError) {
// 请求接口,得到 Promise 实例
const p = fetch()
// 捕获错误
return p.catch(err => {
// 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调
// 同时将 retry 函数作为 onError 回调的参数
return new Promise((reoslve, reject) => {
// retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
const retry = () => resolve(load(onError))
const fail = () => reject(err)
onError(retry, fail)
})
})
}
js
load 函数内部调用了 fetch 函数来发送请求,并得到一个 Promise 实例。接着,添加 catch 语句块来捕获该实例的错误。当捕获到错误时,我们有两种选择:要么抛出错误,要么返回一个新的 Promise 实例,并把该实例的 resolve 和 reject 方法暴露给用户,让用户来决定下一步应该怎么做。这里,我们将新的 Promise 实例的 resolve 和 reject 分别封装为 retry 函数和 fail 函数,并将它们作为 onError
回调函数的参数。这样,用户就可以在错误发生时主动选择重试或直接抛出错误。下面的代码展示了用户是如何进行重试加载的。
// 调用 load 函数加载资源
load(
// onError 回调
(retry) => {
// 失败后重试
retry()
}
).then(res => {
// 成功
console.log(res)
})
js
基于这个原理,我们可以很容易地将它整合到异步组件的加载流程中。具体实现如下:
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(options) {
// options 既可以是配置项,也可以是加载器
if (typeof options === 'function') {
// 如果是 options 是加载器,将其格式化配置项形式
options = {
loader: options
}
}
const { loader } = options
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 记录重试次数
let retries = 0
// 封装 load 函数用来加载异步组件
function load() {
return loader()
// 捕获加载器的错误
.catch((error) => {
// 如果用户指定了 onError 回调,则将控制权交给用户
if (options.onError) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 重试
const retry = () => {
resolve(load())
retries++
}
// 失败
const fail = () => reject(err)
// 作为 onError 回调函数的参数,让用户决定如何处理
options.onError(retry, fail, retries)
})
} else {
throw error
}
})
}
// 返回一个包装组件
return {
async: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 定义 error,当错误发生时,用来存储错误对象
const error = shallowRef(null)
// 代表是否超时,默认为 false
const timeout = ref(false)
// 代表是否正在加载,默认为 false
const loading = ref(false)
let loadingTimer = null
// 如果配置项中存在 delay,则开启一个定时器计时
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
// 如果配置项中没有 delay,则直接标记为加载中
loaded.value = true
}
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
// loader()
// .then(c => {
// InnerComp = c
// loaded.value = true
// })
// // 添加 catch 语句来捕获加载过程中的错误
// .catch(err => error.value = err)
// // 加载完毕后,无论成功与否都要清除延迟定时器
// .finally(() => {
// loaded.value = false
// clearTimeout(loadingTimer)
// })
// 调用 load 函数加载组件
load()
.then(c => {
InnerComp = c
loaded.value = true
})
// 添加 catch 语句来捕获加载过程中的错误
.catch(err => error.value = err)
// 加载完毕后,无论成功与否都要清除延迟定时器
.finally(() => {
loaded.value = false
clearTimeout(loadingTimer)
})
// ...
}
}
}
js
如上面的代码及注释所示,其整体思路与普通接口请求的重试机制类似。
函数式组件
函数式组件的实现相对容易。一个函数式组件本质上就是一个普通函数,该函数的返回值是虚拟 DOM。之前我们提到过:“在 Vue.js 3 中使用函数式组件,主要是因为它的简单性,而不是因为它的性能好”。这是因为在 Vue.js 3 中,即使是有状态组件,其初始化性能消耗也非常小。
在用户接口层面,一个函数式组件就是一个返回虚拟 DOM 的函数。
function MyFuncComp(props) {
return { type: 'h1', children: props.title }
}
js
函数式组件没有自身状态,但它仍然可以接收由外部传入的 props。为了给函数式组件定义 props,我们需要在组件函数上添加静态的 props 属性。
function MyFuncComp(props) {
return { type: 'h1', children: props.title }
}
// 定义 props
MyFuncComp.props = {
title: String
}
js
在有状态组件的基础上,实现函数式组件将变得很容易,因为挂载组件的逻辑可以复用 mountComponent
函数。为此,我们只需要在 patch 函数内支持函数类型的 vnode.type
。
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// ...
} else if (type === Text) {
// ...
} else if (type === Fragment) {
// ...
} else if (
// 有状态组件
typeof type === 'object' ||
// 函数式组件
typeof type === 'function'
) {
// vnode.type 的值是选项对象,作为组件处理
if (!n1) {
// 挂载组件
mountComponent(n2, container, anchor)
} else {
// 更新组件
patchComponent(n1, n2, anchor)
}
}
}
js
在 patch 函数内部,通过检测 vnode.type
的类型来判断组件类型:
- 如果
vnode.type
是一个对象,则它是一个有状态组件,并且vnode.type
是组件选项对象; - 如果
vnode.type
是一个函数,则它是一个函数式组件。
无论有状态组件,还是函数式组件,我们都可以通过 mountComponent
函数来完成挂载,也都可以通过 patchComponent
函数来完成更新。
下面是修改后的 mountComponent
函数,它支持挂载函数式组件。
function mountComponent(vnode, container, anchor) {
// 检查是否是函数式组件
const isFunctional = typeof vnode.type === 'function'
// 通过 vnode 获取组件的选项对象,即 vnode.type
let componentOptions = vnode.type
if (isFunctional) {
// 如果是函数式组件,则将 vnode.type 作为渲染函数,将 vnode.type.props 作为 props 选项定义即可
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
// ...
// setupContext
const setupContext = { attrs, emit, slots }
// 调用 setup 函数之前,设置当前组件实例
setCurrentInstance(instance)
// 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外地修改 props 的值
// 将 setupContext 作为第二个参数传递
const setupResult = setup && setup(shallowReadonly(instance.props), setupContext)
// 在 setup 函数执行完毕之后,重置当前组件实例
setCurrentInstance(null)
// setupState 用来存储由 setup 返回的数据
let setupState = null
// 如果 setup 函数的返回值是函数,则将其作为渲染函数
if (typeof setupResult === 'function') {
if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
// 将 setupResult 作为渲染函数
render = setupResult
} else {
// 如果 setup 的返回值不是函数,则作为数据状态赋值给 setupState
setupState = setupResult
}
// 将组件实例设置到 vnode 上,用于后续更新
vnode.component = instance
// ...
}
js
可以看到,实现对函数式组件的兼容非常简单。首先,在 mountComponent
函数内检查组件的类型,如果是函数式组件,则直接将组件函数作为组件选项对象的 render 选项,并将组件函数的静态 props 属性作为组件的 props 选项即可,其他逻辑保持不变。当然,出于更加严谨的考虑,我们需要通过 isFunctional
变量选择性地执行初始化逻辑,因为对于函数式组件来说,它无需初始化 data 以及生命周期钩子。从这一点可以看出,函数式组件的初始化性能消耗小于有状态组件。
总结
本篇文章中,我们首先讨论了异步组件要解决的问题。异步组件在页面性能、拆包以及服务端下发组件等场景中尤为重要。从根本上来说,异步组件的实现可以完全在用户层面实现,而无须框架支持。但一个完善的异步组件仍需要考虑诸多问题,例如:
- 允许用户指定加载出错时要渲染的组件;
- 允许用户指定 Loading 组件,以及展示该组件的延迟时间;
- 允许用户设置加载组件的超时时长;
- 组件加载失败时,为用户提供重试的能力。
因此,框架有必要内建异步组件的实现。Vue.js 3 提供了 defineAsyncComponent
函数,用来定义异步组件。
接着,我们讲解了异步组件的加载超时问题,以及当加载错误发生时,如果指定 Error 组件,通过为 defineAsynComponent
函数指定选项参数,允许用户通过 timeout
选项设置超时时长。当加载超时后,会触发加载错误,这时会渲染用户通过 errorComponent
选项指定的 Error 组件。
在加载异步组件的过程中,受网络状态的影响较大。当网络状态较差时,加载过程可能很漫长。为了提供更好的用户体验,我们需要在加载时展示 Loading 组件。所以,我们设计了 loadingComponent
选项,以允许用户配置自定义的 Loading 组件。但展示 Loading 组件的时机是一个需要仔细考虑的问题。为了避免 Loading 组件导致的闪烁问题,我们还需要设计一个接口,让用户能指定延迟展示 Loading 组件的时间,即 delay 选项。
在加载组件的过程中,发生错误的情况非常常见。所以,我们设计了组件加载发生错误后的重试机制。在讲解异步组件的重试加载机制时,我们类比了接口请求发生错误时的重试机制,两者的思路类似。
最后,我们讨论了函数式组件。它本质上一个函数,其内部实现逻辑可以复用有状态组件的实现逻辑。为了给函数式组件定义 props,我们允许开发者在函数式组件的主函数上添加静态的 props 属性。处于更加严谨的考虑,函数式组件没有自身状态,也没有生命周期的概念。所以,在初始化函数式组件时,需要选择性地复用有状态组件的初始化逻辑。