【译】组合式 API 与异步编程

大家好,这里是大家的林语冰。今天分享的是来自 Vue 官方资讯安利的一篇旧文,作者是"Vitest 之父兼 VueUse 之父兼 UnoCss 之父" ------ Anthony Fu 大佬(下文简称"托尼老师"),祂同时也是 Vue/Vite/Nuxt 核心团队成员。

免责声明

本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 Async with Composition API

在 Vue 组合式 API 中使用异步函数时有一个重要的警告,我相信你们中的大多数人都邂逅过。我从某处知道祂已经有一段时间了,但每次我想有一个详细的参考并分享给祂者时,我都找不到祂的任何记录。因此,我考虑自己码字,为您整理可能的解决方案,同时详细说明一下。

  • 问题所在
  • 工作机制
  • 局限性
  • 解决方案

问题所在

每当使用异步 setup() 时,您不得不在首个 await 语句之前使用 effect(作用)和生命周期 hook(钩子)。(详情

举个栗子:

js 复制代码
import { onMounted, onUnmounted, ref, watch } from 'vue'

export default defineAsyncComponent({
  async setup() {
    const counter = ref(0)

    watch(counter, () => console.log(counter.value))

    // OK!
    onMounted(() => console.log('Mounted'))

    // await 语句
    await someAsyncFunction() // <-----------

    // 无法奏效!
    onUnmounted(() => console.log('Unmounted'))

    // 仍然奏效,组件销毁后但无法自动回收(内存泄漏!)
    watch(counter, () => console.log(counter.value * 2))
  }
})

await 语句之后,下列功能会有所限制(no auto-dispose,无法自动回收):

  • watch / watchEffect
  • computed
  • effect

下列功能不再奏效

  • onMounted / onUnmounted / onXXX
  • provide / inject
  • getCurrentInstance
  • ......

工作机制

让我们拿 onMounted API 充当小白鼠。地球人都知道,onMounted 是一个 hook,当挂载当前组件时祂会注册一个侦听器。请注意,onMounted(以及其他组合式 API)都是全局的 ,我此处提及的"全局"指的是祂可以在任何地方导入和调用 ------ 没有和祂绑定的局部上下文(local context)

js 复制代码
// 局部:onMounted 是其绑定的组件的一个方法
component.onMounted(/* ... */)

// 全局:onMounted 可以无上下文调用
onMounted(/* ... */)

那么,onMounted 如何知道什么组件正在挂载呢?

Vue 诉诸一种有趣的方法来解决这个问题。祂使用一个内部变量来记录当前组件实例。有一个简化的代码:

当 Vue 挂载一个组件时,祂将该实例存储在一个全局变量中。当在 setup() 函数中调用 hook 时,祂将使用该全局变量来获取当前组件实例。

js 复制代码
let currentInstance = null

// 伪代码
export function mountComponent(component) {
  const instance = createComponent(component)

  // 保留先前的实例
  const prev = currentInstance

  // 将实例设置为全局
  currentInstance = instance

  // setup() 中的 hook 调用会将 currentInstance 作为上下文
  component.setup()

  // 恢复先前的实例
  currentInstance = prev
}

onMounted 的简版实现如下所示:

js 复制代码
// 伪代码
export function onMounted(fn) {
  if (!currentInstance) {
    warn(`"onMounted" can't be called outside of component setup()`)
    return
  }

  // 当前实例绑定的侦听器
  currentInstance.onMounted(fn)
}

诉诸此方法,只要 onMounted 在组件内部的 setup() 调用,祂就可以获取当前组件的实例。

局限性

目前为止一切顺利,虽然但是,异步函数有什么 bug 呢?

基于 JS 是单线程 的事实,该实现才会奏效。单线程确保下列语句会逐个执行,换而言之,无人可以在同一时间意外修改 currentInstance(又名 原子操作)。

js 复制代码
currentInstance = instance
component.setup()
currentInstance = prev

setup() 是异步时,情况有所不同。每当您 await 一个 promise,您都可以认为引擎暂停了这里的工作,并且跑去做另一项任务。如果我们 await 该函数,在这段时间内,创建多个组件会不可预测地更改全局变量并最终一塌糊涂。

js 复制代码
currentInstance = instance
await component.setup() // 原子性丢失
currentInstance = prev

如果我们不诉诸 await 来检查该实例,调用该 setup() 函数将使祂完成首个 await 语句之前的任务,其余任务将在 await 语句 resolved(决议)后才执行。

js 复制代码
async function setup() {
  console.log(1)
  await someAsyncFunction()
  console.log(2)
}

console.log(3)
setup()
console.log(4)
md 复制代码
// 输出结果:
3
1
4
(awaiting)
2

这意味着,Vue 无法知道异步部分何时会被外部调用,因此也无法将实例绑定到上下文中。

解决方案

这实际上是 JS 本身的局限性,除非我们有某些新提案来打开语言层级的新世界,否则我们不得不执子之魂,与子共生。

但是为了解决这个问题,我收集了若干解决方案供您按需选择。

牢记该警告并敬而远之

当然,这个"解决方案"一目了然。您可以尝试将您的 effect 和 hook 搬运到首个 await 语句之前,并牢记之后不要再使用祂们。

幸运的是,如果您有使用 ESLint,您可以启动 eslint-plugin-vuevue/no-watch-after-awaitvue/no-lifecycle-after-await 的规则,每当您犯错时祂都会警告你(祂们在插件预设中被默认启动)。

将异步函数封装为"响应式同步"

在某些情况下,您的逻辑可能依赖于异步请求的数据。这种场景下,您可以考虑使用我在 VueDay 2021 上分享的奇技淫巧您的异步函数转换为同步的响应式状态

js 复制代码
const data = await fetch('https://api.github.com/').then(r => r.json())

const user = data.user
js 复制代码
const data = ref(null)

fetch('https://api.github.com/')
  .then(r => r.json())
  .then(res => (data.value = res))

const user = computed(() => data?.user)

此方法首先会 resolve 您的逻辑之间的"连接",然后在异步函数 resolve 后并填充数据时进行响应式更新。

VueUse 还有若干更通用的工具人:

useAsyncState

js 复制代码
import { useAsyncState } from '@vueuse/core'

const { state, ready } = useAsyncState(async () => {
  const { data } = await axios.get('https://api.github.com/')
  return { data }
})

const user = computed(() => state?.user)

useFetch

js 复制代码
import { useFetch } from '@vueuse/core'

const { data, isFetching, error } = useFetch('https://api.github.com/')

const user = computed(() => data?.user)

显示绑定实例

生命周期 hook 实际上接受第二个参数来显式设置实例。

js 复制代码
export default defineAsyncComponent({
  async setup() {
    // await 之前获取并保留该实例
    const instance = getCurrentInstance()

    await someAsyncFunction() // <-----------

    onUnmounted(
      () => console.log('Unmounted'),
      instance // <--- 将实例传递给祂
    )
  }
})

虽然但是,缺点是此方案不适用watch / watchEffect / computed / provide / inject,因为祂们不接受该实例参数。

要让 effect 奏效,您可以使用即将推出的 Vue 3.2 的 effectScope API。

js 复制代码
import { effectScope } from 'vue'

export default defineAsyncComponent({
  async setup() {
    // 在 await 之前创建 scope,那么祂会绑定此实例
    const scope = effectScope()

    const data = await someAsyncFunction() // <-----------

    scope.run(() => {
      /* 使用 computed、watch 等等...... */
    })

    // 生命周期 hook 在这里不奏效,
    // 您需要将其与先前的片段结合起来,
    // 以使生命周期 hook 和 effect 奏效。
  }
})

编译时魔法!

在最近的 <script setup> 提案 更新中,引入了新型的 编译时魔法

祂的工作方式是在每个 await 语句之后注入一个脚本,用于还原当前实例状态。

js 复制代码
import { withAsyncContext } from 'vue'

export default {
  async setup() {
    let __temp, __restore

    const post =
      (([__temp, __restore] = withAsyncContext(() =>
        fetch(`/api/post/1`).then(r => r.json())
      )),
      (__temp = await __temp),
      __restore(),
      __temp)

    // 保留当前实例上下文
    // 比如,onMounted() 依然奏效。

    return { post }
  }
}

有了祂,当和 <script setup> 梦幻联动时,该异步函数才会奏效 。唯一的耻辱在于祂在 <script setup> 之外无法奏效。

您现在收看的是直男翻译系列,学废了的小伙伴可以点赞友情赞助本系列,我们每天佛系投稿,欢迎关注和订阅最新动态。谢谢大家的彼芯,掰掰~

相关推荐
twins352032 分钟前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky1 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
Front思2 小时前
vue使用高德地图
javascript·vue.js·ecmascript
花花鱼5 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
流烟默5 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
蒲公英10016 小时前
vue3学习:axios输入城市名称查询该城市天气
前端·vue.js·学习
杨荧9 小时前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游
一 乐14 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
小御姐@stella15 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js