今天我要跟你讨论一下上下文
。
上下文是天然存在于代码中的概念,举一个经典例子:
ts
let one = 1
{
let one = 2
}
one === 1 // ?
上面这段代码的结果是什么呢?显而易见是true,也就是说经过中间大括号内的一顿操作后,one的值并没有发生变化,这是因为大括号中声明的变量只在大括号的作用域内。
作用域大家都知道,这是一个很基础的概念,而作用域是上下文的一种。所谓上下文,顾名思义就是一段代码附近的区域 ,或者说这段代码在哪儿 。上下文是要有对象的,一定是"对于某段代码"的上下文。比如这里,对于大括号中声明的one
的生命周期,其上下文就是大括号内。
vue
<script setup>
usePointer()
function test() {
usePointer()
}
</script>
<template>
<button @click="test()">test</button>
</template>
假设usePointer
是一个组合式函数(composables),上面这段代码有什么问题呢?这里我抄录一段vue官方文档的原文:
组合式函数只能在
<script setup>
或setup()
钩子中被调用。在这些上下文中,它们也只能被同步 调用。在某些情况下,你也可以在像onMounted()
这样的生命周期钩子中调用它们。这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:
- 将生命周期钩子注册到该组件实例上
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
也就是说,在上面的代码中,第一次调用时正确的,第二次调用时错误的。这是因为第一次调用时在setup上下文中,第二次调用时上下文是不确定的,从逻辑上说,你无法推断用户在什么时候点下这个按钮,所以其上下文一定是不确定的:
scss
<script setup>
usePointer() // ✔
function test() {
usePointer() // ✖
}
</script>
当然这是大部分情况。如果一个组合式函数只是对基本的响应式系统进行无状态的算法操作,倒也可以在其他地方调用,只是这种情况很少见,因为我们在使用组合式函数时通常都是要依赖于组件的生命周期的。
新的问题出现了,什么是生命周期?vue的组件有三种常见的生命周期,按顺序他们的钩子分别是:
- setup - 组件初始化时
- onMounted - html挂载后,意味着到此时你才能操作html相关的内容
- onUnmounted - 卸载时
setup不必多说。我们是这样使用生命周期钩子的:
ts
onMounted(() => {
// 随便做点啥
})
发现了吗?"随便做点啥"位于一个大括号内,这意味着其实我们使用生命周期钩子时,其上下文就是这个钩子内。在vue中,我们所说的上下文,大部分都和生命周期的上下文有关
,接下来会详细阐述这一点
而生命周期钩子本身也有上下文,那就是setup:
xml
<script setup>
onMounted() // ✔
function test() {
onMounted() // ✖
}
</script>
<template>
<button @click="test()">test</button>
</template>
还是一样的例子,既然已知生命周期钩子需要在setup上下文内,那么答案显而易见,我就不赘述了。
由于组件拥有生命周期,也就是说组件会经历一个"活了然后死了"的过程,那么同样的,从逻辑上说,你无法推断用户在什么时候点下这个按钮,那么第二次调用可能会发生mounted周期已经结束了的时候,那么必须要将其限制在setup内,也就是保证那个生命周期的阶段还没开始前,先说好到了那个时候要做的事。
非常常用的watchEffect也是可以依赖于生命周期的,如果watchEffect在setup阶段被声明,那么在组件开始时侦测将会开始,在组件销毁时侦测同样也自动被销毁。所以最好我们只在setup中使用这个函数。
而watchEffect本身也有上下文,这和computed类似,当你在其上下文内使用响应式变量,就会触发这个侦测器:
ts
const a = ref('one')
const b = ref(1)
watchEffect(() => {
// 随便做点啥
b.value = a.value === 'one' ? 1 : 2
})
和上面的例子类似,可以注意到"随便做点啥"位于一个大括号内,相信你已经敏锐的意识到了,我们知道在watchEffect内使用的响应式变量会触发watchEffect的更新,换而言之,就是在这个上下文内使用响应式变量会触发watchEffect更新,computed也一样。
那么如果这样呢?
ts
const a = ref('one')
const b = ref(1)
watchEffect(async () => {
await fetchSomething() // 一个异步函数
b.value = a.value === 'one' ? 1 : 2
})
在这个例子中,computed传入了一个异步函数。你可能在日常经验,或者其他文章中学到,最好不要在watchEffect中使用异步函数了。显然,此时b失去了响应。原因也很简单,从逻辑上说,或许当这个异步任务结束时,已经经过了setup阶段,甚至说不定这个组件都被销毁了,那么他肯定不能被算作这个上下文了。虽然这段代码在watchEffect传入函数的作用域内,但是他却不在watchEffect的上下文中。
那这样呢?
ts
const a = ref('one')
const b = ref(1)
watchEffect(async () => {
b.value = a.value === 'one' ? 1 : 2
await fetchSomething() // 一个异步函数
})
看起来只是把一行代码调换了顺序,但其实有天大的不同。这段代码是能正常运行的
。可是这不是在异步函数中吗?仔细分析,你会发现截止到await
之前的代码是同步的,换而言之,此前的代码都在外层的上下文内,而直到await,也就是等待异步函数开始,从那之后就脱离了上下文了。
ts
const one = ref(1)
function test() {
one.value = 2
}
watchEffect(() => {
test()
})
这段代码会触发更新吗?一些新手会觉得,watchEffect并没有出现响应式变量,所以不会触发更新。但按照此前的理解来看,在执行到test()时,test()也是一个同步函数,换而言之这个watchEffect内部都是同步发生的,所以test正处于watchEffect上下文内,他可以正常更新。
至此,我们能清晰的意识到一件事,那就是异步函数/用户操作通常会导致上下文的丢失。这也是为什么vue的组合式函数经常要在setup周期中以同步的方式运行。
ts
// utils.ts
function useXXX() {
watchEffect()
onMounted()
// ...
}
vue
<script setup>
useXXX()
</script>
这也正应了vue最核心的内容之一:响应式语法。看起来"用户操作通常会导致上下文的丢失"好像会导致上下文的概念很垃圾,毕竟不涉及用户操作与其叫程序不如叫美术。那么实际使用时如何防止异步函数/用户操作丢失上下文呢?很简单,既然上下文注定保不住了,那何不用响应式变量
来保存状态呢?
ts
const a = ref('one')
const b = ref(1)
const ready = ref(false)
watchEffect(async () => {
b.value = a.value === 'one' ? 1 : 2
ready.value = false
await fetchSomething() // 一个异步函数
ready.value = true
})
vue
<script setup>
const click= ref(false)
const { /* ... */ } = usePointer()
watchEffect(() => {
// ...
})
function test() {
click.value = true
}
</script>
<template>
<button @click="test()">test</button>
</template>
我们通过一个简单的布尔值来保存了异步任务/用户操作的状态,然后setup中的其他部分比如watchEffect就可以通过这个响应式变量来获悉异步任务状态/用户操作内容。其核心思想在于:你应该在如setup这种最初的时候就准备好一切,而后面只是借助于响应式自然而然的发生,如此就能避免上下文的丢失,也正应了响应式思想的核心。
类似的例子有很多,比如地图类的库通常都依赖于html的初始化,需要等到页面挂载后才能获得dom,才能初始化地图,而此时已经失去了setup周期,你就无法挂载组合式函数了,而解决办法就是在最初的时候用一个响应式变量存储你需要使用的内容,在组合式函数里应对好这个响应式变量空值时、初始化后等各种情况,然后在初始化只需要更新这个响应式变量,你所需要在初始化后执行的逻辑就自然而然发生。
这篇文章是我要成为vue高手
的第一篇文章,所讲的也是基础中的基础,毕竟如果对上下文都处于模糊的认知下,那有再多的骚操作也难以正确的使用了。总结一下,本文的核心也就是最后的一段话,其核心思想在于:你应该在如setup这种最初的时候就准备好一切,而后面只是借助于响应式自然而然的发生,如此就能避免上下文的丢失,也正应了响应式思想的核心。