Vue 中的响应性是如何工作的
可以追踪对象属性的读写的。
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter /setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter /setter 用于 ref。下面的伪代码将会说明它们是如何工作的:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在 Vue 3 中,Vue 使用 Proxy
实现响应式系统,这比 Object.defineProperty()
更强大,因为 Proxy
可以直接监听整个对象的变化,包括新增或删除属性。
状态机
状态机是一种数据模型,用于描述应用可能处于的所有可能状态,以及从一种状态转换到另一种状态的所有可能方式。虽然对于简单的组件来说,这可能有些小题大做了,但它的确可以使得复杂的状态流更加健壮和易于管理。
XState 是 JavaScript 中一个比较常用的状态机实现方案。这里是集成它的一个例子:
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'
export function useMachine(options) {
const machine = createMachine(options)
const state = shallowRef(machine.initialState)
const service = interpret(machine)
.onTransition((newState) => (state.value = newState))
.start()
const send = (event) => service.send(event)
return [state, send]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
ref()
在组合式 API 中,推荐使用 ref()
函数来声明响应式状态:
import { ref } from 'vue'
const count = ref(0)
2
3
4
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
2
3
4
5
6
7
8
<div>{{ count }}</div>
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。 在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
为 ref()
标注类型
ref 会根据初始化时的值推导其类型:
import { ref } from 'vue'
// 推导出的类型:Ref<number>
const year = ref(2020)
// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'
2
3
4
5
6
7
有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref
这个类型:
import { ref } from 'vue'
import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!
2
3
4
5
6
或者,在调用 ref()
时传入一个泛型参数,来覆盖默认的推导行为:
// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // 成功!
2
3
4
如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined
的联合类型:
ts
// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()
2
<script setup>
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup>
来大幅度地简化代码:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
深层响应性
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map
。
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
2
3
4
5
6
7
8
9
10
11
12
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在 “next tick” 更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
2
3
4
5
6
7
reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同, reactive()
将使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
2
3
为 reactive()
标注类型
reactive()
也会隐式地从它的参数中推导类型:
import { reactive } from 'vue'
// 推导得到的类型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })
2
3
4
要显式地标注一个 reactive
变量的类型,我们可以使用接口:
import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 指引' })
2
3
4
5
6
7
8
TIP
不推荐使用 reactive()
的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
reactive()
的局限性
reactive()
API 有一些局限性:
有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地 “替换” 响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
jslet state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
1
2
3
4
5对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
jsconst state = reactive({ count: 0 }) // 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state count++ // 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性 callSomeFunction(state.count)
1
2
3
4
5
6
7
8
9
10
11
12