在Vue 中使用选项式API ,去声明响应式对象是很简单的。我们只需要将需要响应式的变量属性放进 data 函数中,并return 出来。 Vue 框架会帮我们把数据变为响应式,并在模板中可用。 如下面所示。
export defaut {
// other
data() {
return {
hello: 'world'
}
}
//... other code
}
但对于 Composition API,事情就没那么简单了。状态声明必须使用Ref 和 Reactive 函数来实现,这两个函数的行为表现也不一样。
让我们讨论一下 Vue 3 中发生了什么变化以及为什么我们需要两个不同的助手。
Vue3 中的响应式实现
大家都知道Vue2 中,是通过Object.defineProperty 实现的依赖收集和响应式处理。但在Vue3 使用了javascript 的Proxy API 完全重写了响应式核心。
Proxy 是一种更现代更优雅的API 可以实现对一个对象的代理和劫持。
你可以通过下边一段代码来了解Proxy 是如何工作的:
const userInfo = {
name: "sean",
age: 35,
};
const handler = {
get(target, property) {
if (property === "name") {
const name = target[property]
return name.charAt(0).toUpperCase() + name.slice(1);
}
if (property === "age") {
return '--'
}
return target[property]
},
};
const proxyObj = new Proxy(userInfo, handler);
console.log(proxyObj.name) // "Sotis"
console.log(proxyObj.age) // "--"
handler 内的 get 方法称为 trap ,每次访问对象的属性时都会调用该方法。
同样的道理,我们很容易就能推断出一个 set 的 trap 方法。
const userInfo = {
name: "Sean",
age: 35,
};
const handler = {
set(target, prop, value) {
if (prop === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("age 类型错误");
}
if (value > 200) {
throw new RangeError("额,好像超过了范围了");
}
}
target[prop] = value;
return true;
},
};
const proxy = new Proxy(userInfo, handler);
proxy.age = 12 // OK
proxy.age = 300 // Error: 额,好像超过了范围了
这就是Vue3 的响应式实现的核心原理,当我们使用Reactive 工具方法声明响应式属性的时候, 框架底层会通过Proxy 来实现,属性的变更追踪和依赖处理。
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)
}
})
}
当然,Vue 框架中的响应式,实现要比这复杂的多。 框架需要处理很多边界情况。 但是核心的原理还是使用了Proxy。 如果你对Vue 的Reactive的具体实现感兴趣,请看如下连接。
https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L83
上面的代码片段解释了为什么将响应式变量解构或重新分配给局部变量不再具有响应式的特性,因为它不再触发源对象上的 get/set 代理track。
这看起来是一个让一切变得响应式的完美解决方案。但是有一个问题!根据定义,Proxy 仅适用于复杂类型(对象、数组、映射和集合)。
想要将原始值( Boolean、Number、BigInt、String、Symbol、undefined 和 null 等类型的值) 变成响应式数据,就必须对其做一层包裹,也就是我们接下来要介绍的 ref。
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
通过将原始值,进行包装成Object 我们实现了, 原始值的响应式。
这也就解释了为什么必须在脚本设置中使用的烦人的 .value。
同样,重组或重新分配给局部变量是行不通的。
总结
所以为什么同时需要 Ref 和 Reactive 的答案是:
- 代理。对于复杂类型,可以直接使用它们,但对于原始类型数据需要创建代理对象。
希望了解 Vue 的底层工作原理可以让您更加高效,并消除 ref 和 Reactive 之间的混淆。