惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Troy Hunt's Blog
Last Week in AI
Last Week in AI
D
DataBreaches.Net
大猫的无限游戏
大猫的无限游戏
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyberwarzone
Cyberwarzone
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
月光博客
月光博客
博客园 - 司徒正美
C
CERT Recently Published Vulnerability Notes
L
LangChain Blog
Simon Willison's Weblog
Simon Willison's Weblog
The Register - Security
The Register - Security
T
The Blog of Author Tim Ferriss
V
V2EX
F
Fortinet All Blogs
AWS News Blog
AWS News Blog
T
Tor Project blog
V
Vulnerabilities – Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
小众软件
小众软件
L
Lohrmann on Cybersecurity
量子位
F
Full Disclosure
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
Intezer
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
Scott Helme
Scott Helme
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
J
Java Code Geeks
S
Schneier on Security

Anthony Fu

Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory (React ver.) Mental Health in Open Source The Evolution of Shiki v1.0 The Magic in Shiki Magic Move Anthony's Roads to Open Source - The Progressive Path Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory Now, and the Future of Nuxt Devtools Anthony's Roads to Open Source - The Set Theory Anthony Fu Anthony Fu Stable Diffusion QR Code 101 Refining AI Generated QR Code Stylistic QR Code with Stable Diffusion Anthony Fu How I Manage GitHub Notifications Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Dev SSR on Nuxt with Vite Why I don't use Prettier Anthony Fu Anthony Fu Ship ESM & CJS in one Package Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Reflection of Speaking in Public Anthony Fu Windi CSS and Tailwind JIT Typed Provide and Inject in Vue Color Scheme for VS Code Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Destructuring... with object or array? Anthony Fu Anthony Fu Make Libraries Working with Vue 2 and 3 Anthony Fu Anthony Fu Anthony Fu Anthony Fu
Anthony Fu
Anthony Fu · 2021-07-16 · via Anthony Fu

There is a major caveat when working with asynchronous functions in Vue Composition API, that I believe many of you have ever come across. I have acknowledged it for a while from somewhere, but every time I want to have a detailed reference and share to others, I can’t find it’s documented anywhere. So, I am thinking about writing one, with a detailed explanation while sorting out the possible solutions for you.

  • The Problem
  • The Mechanism
  • The Limitation
  • The Solutions

The Problem

When using asynchronous setup(), you have to use effects and lifecycle hooks before the first await statement. (details)

For example:

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'))

    // the await statement
    await someAsyncFunction() // <-----------

    // does NOT work!
    onUnmounted(() => console.log('Unmounted'))

    // still works, but does not auto-dispose
    // after the component is destroyed (memory leak!)
    watch(counter, () => console.log(counter.value * 2))
  }
})

After the await statement,

the following functions will be limited (no auto-dispose):

  • watch / watchEffect
  • computed
  • effect

the following functions will not work:

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

The Mechanism

Let’s take the onMounted API as an example. As we know, onMounted is a hook that registers a listener when the current component gets mounted. Notice that onMounted (along with other composition APIs) are global, for what I mean "global" is that it can be imported and called anywhere - there is no local context bound to it.

// local: `onMounted` is a method of `component` that bound to it
component.onMounted(/* ... */)

// global: `onMounted` can be called without context
onMounted(/* ... */)

So, how does onMounted know what component is being mounted?

Vue takes an interesting approach to solve this. It uses an internal variable to record the current component instance. There is a simplified code:

When Vue mounts a component, it stores the instance in a global variable. When hooks been called inside the setup function, it will use the global variable to get the current component instance.

let currentInstance = null

// (pseudo code)
export function mountComponent(component) {
  const instance = createComponent(component)

  // hold the previous instance
  const prev = currentInstance

  // set the instance to global
  currentInstance = instance

  // hooks called inside the `setup()` will have
  // the `currentInstance` as the context
  component.setup()

  // restore the previous instance
  currentInstance = prev
}

A simplified onMounted implementation would be like:

// (pseudo code)
export function onMounted(fn) {
  if (!currentInstance) {
    warn(`"onMounted" can't be called outside of component setup()`)
    return
  }

  // bound listener to the current instance
  currentInstance.onMounted(fn)
}

With this approach, as long as the onMounted is called inside the component setup(), it will be able to get the instance of the current component.

The Limitation

So far so good, but what’s wrong with asynchronous functions?

The implementation would work based on the fact that JavaScript is single-threaded. Single thread makes sure the following statements will be executed right next to each other, which in other words, there is no one could accidentally modify the currentInstance at the same time (a.k.a. it’s atomic).

currentInstance = instance
component.setup()
currentInstance = prev

The situation changes when the setup() is asynchronous. Whenever you await a promise, you can think the engine paused the works here and went to do another task. If we await the function, during the time period, multiple components creation will change the global variable unpredictably and end up with a mess.

currentInstance = instance
await component.setup() // atomic lost
currentInstance = prev

If we don’t use await to check the instance, calling the setup() function will make it finish the tasks before the first await statement, and the rest will be executed whenever the await statement is resolved.

async function setup() {
  console.log(1)
  await someAsyncFunction()
  console.log(2)
}

console.log(3)
setup()
console.log(4)
// output:
3
1
4
(awaiting)
2

This means, there is no way for Vue to know when will the asynchronous part been called from the outside, so there is also no way to bound the instance to the context.

The Solutions

This is actually a limitation of JavaScript itself, unless we have some new proposal to open the gate on the language level, we have to live with it.

But to work around it, I have collected a few solutions for you to choose from based on your needs.

Remember the Caveat and Avoid It

This is, of course, an obvious "solution". You can try to move your effect and hooks before the first await statement and carefully remember not to have them after that again.

Luckily, if you are using ESLint, you can have the vue/no-watch-after-await and vue/no-lifecycle-after-await rules from eslint-plugin-vue enabled so it could warn you whenever you made some mistakes (they are enabled by default within the plugin presets).

Wrap the Async Function as "Reactive Sync"

In some situations, your logic might be relying on the data that fetched asynchronously. In this way, you could consider using the trick I have shared on VueDay 2021 to turn your async function into a sync reactive state.

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

const user = data.user
const data = ref(null)

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

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

This approach make the "connections" between your logic to resolve first, and then reactive updates when the asynchronous function get resolved and filled with data.

There is also some more general utilities for it from VueUse:

useAsyncState

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

import { useFetch } from '@vueuse/core'

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

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

Explicitly Bound the Instance

Lifecycle hooks actually accept a second argument for setting the instance explicitly.

export default defineAsyncComponent({
  async setup() {
    // get and hold the instance before `await`
    const instance = getCurrentInstance()

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

    onUnmounted(
      () => console.log('Unmounted'),
      instance // <--- pass the instance to it
    )
  }
})

However, the downside is that this solution does not work with watch / watchEffect / computed / provide / inject as they does not accept the instance argument.

To get the effects work, you could use the effectScope API in the upcoming Vue 3.2.

import { effectScope } from 'vue'

export default defineAsyncComponent({
  async setup() {
    // create the scope before `await`, so it will be bond to the instance
    const scope = effectScope()

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

    scope.run(() => {
      /* Use `computed`, `watch`, etc. ... */
    })

    // the lifecycle hooks will not be available here,
    // you will need to combine it with the previous snippet
    // to have both lifecycle hooks and effects works.
  }
})

Compile-time Magic!

In the recent <script setup> proposal update, a new compile-time magic is introduced.

The way it works is to inject a script after each await statement for restoring the current instance state.

<script setup>
  const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
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)

    // current instance context preserved
    // e.g. onMounted() will still work.

    return { post }
  }
}

With it, the async functions will just work when using with <script setup>. The only shame is it does not work outside of <script setup>.