reactive
reactive API 的定义为传入一个对象并返回一个基于原对象的响应式代理,即返回一个 Proxy,相当于 Vue2x 版本中的 Vue.observer。
首先,我们需要知道在 Vue3 中除了可以使用原先的 Options API,还可以使用新的语法 Composition API,简易版的 Composition API 看起来会是这样的:
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
可以看到,没有了我们熟悉的data、computed、methods等等。看起来,似乎有点 React风格,这个提出确实当时社区中引发了很多讨论,说Vue越来越像React....很多人并不是很能接受,具体细节大家可以去阅读 RFC 的介绍。
优点
回到本篇文章所关注的,很明显 reactive API 是对标 data 选项,那么相比较 data 选项有哪些优点?
首先,在 Vue 2x 中数据的响应式处理是基于 Object.defineProperty() 的,但是它只会侦听对象的属性,并不能侦听对象。所以,在添加对象属性的时候,通常需要这样:
// vue2x添加属性
Vue.$set(object, "name", wjc);
reactive API 是基于 ES2015 Proxy 实现对数据对象的响应式处理,即在 Vue3 可以往对象中添加属性,并且这个属性也会具有响应式的效果,例如:
// vue3.0中添加属性
object.name = "wjc";
注意点
使用 reactive API 需要注意的是,当你在 setup 中返回的时候,需要通过对象的形式,例如:
export default {
setup() {
const pos = reactive({
x: 0,
y: 0,
});
return {
pos: useMousePosition(),
};
},
};
或者,借助 toRefs API 包裹一下导出,这种情况下我们就可以使用展开运算符或解构,例如:
export default {
setup() {
let state = reactive({
x: 0,
y: 0,
});
state = toRefs(state);
return {
...state,
};
},
};
toRefs() 具体做了什么,接下来会和 reactive 一起讲解
源码实现
首先,相信大家都有所耳闻,Vue3 用 TypeScript 重构了。所以,大家可能会以为这次会看到一堆 TypeScript 的类型之类的。出于各种考虑,本次我只是讲解编译后,转为 JS 的源码实现(没啥子门槛,大家放心 hh)。
reactive
1.先来看看 reactive 函数的实现:
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target;
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target);
}
if (isRef(target)) {
return target;
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
);
}
可以,看到先有 3 个逻辑判断,对 readonly、readonlyValues、isRef 分别进行了判断。我们先不看这些逻辑,通常我们定义 reactive 会直接传入一个对象。所以会命中最后的逻辑 createReactiveObject()。
2.那我们转到 createReactiveObject() 的定义:
function createReactiveObject(
target,
toProxy,
toRaw,
baseHandlers,
collectionHandlers
) {
if (!isObject(target)) {
if (process.env.NODE_ENV !== "production") {
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target already has corresponding Proxy
let observed = toProxy.get(target);
if (observed !== void 0) {
return observed;
}
// target is already a Proxy
if (toRaw.has(target)) {
return target;
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target;
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
observed = new Proxy(target, handlers);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
createReactiveObject() 传入了四个参数,它们分别扮演的角色:
target是我们定义reactive时传入的对象toProxy是一个空的WeakSet。toProxy是一个空的WeakSet。baseHandlers是一个已经定义好get和set的对象,它看起来会是这样:
const baseHandlers = {
get(target, key, receiver) {},
set(target, key, value, receiver) {},
deleteProxy: (target, key) {},
has: (target, key) {},
ownKey: (target) {}
};
collectionHandlers是一个只包含 get 的对象。
然后,进入 createReactiveObject(), 同样地,一些分支逻辑我们这次不会去分析。
看源码时需要保持的一个平常心,先看主逻辑
所以,我们会命中最后的逻辑,即:
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
observed = new Proxy(target, handlers);
toProxy.set(target, observed);
toRaw.set(observed, target);
它首先判断 collectionTypes 中是否会包含我们传入的 target 的构造函数,而 collectionTypes 是一个 Set 集合,主要包含 Set, Map, WeakMap, WeakSet 等四种集合的构造函数。
如果 collectionTypes 包含它的构造函数,那么将 handlers 赋值为只有 get 的 collectionHandlers 对象,否则,赋值为 baseHandlers 对象。
这两者的区别就在于前者只有 get,很显然这个是留给不需要派发更新的变量定义的,例如我们熟悉的 props 它就只实现了 get。
然后,将 target 和 handlers 传入 Proxy,作为参数实例化一个 Proxy 对象。这也是我们看到一些文章常谈的 Vue3 用 ES2015 Proxy 取代了 Object.defineProperty。
最后的两个逻辑,也是非常重要,toProxy() 将已经定义好 Proxy 对象的 target 和 对应的 observed 作为键值对塞进 toProxy 这个 WeakMap 中,用于下次如果存在相同引用的 target 需要 reactive,会命中前面的分支逻辑,返回定义之前定义好的 observed,即:
// target already has corresponding Proxy target 是已经有相关的 Proxy 对象
let observed = toProxy.get(target);
if (observed !== void 0) {
return observed;
}
而 toRaw() 则是和 toProxy 相反的键值对存入,用于下次如果传进的 target 已经是一个 Proxy 对象时,返回这个 target,即:
// target is already a Proxy target 已经是一个 Proxy 对象
if (toRaw.has(target)) {
return target;
}
toRefs
前面讲了使用 reactive 需要关注的点,提及 toRefs 可以让我们方便地使用解构和展开运算符,其实是最近 Vue3 issue 也有同学讲解过这方面的东西。有兴趣的同学可以移步 When it's really needed to use toRefs in order to retain reactivity of reactive value了解。
我当时也凑了一下热闹,如下图:
可以看到,toRefs 是在原有 Proxy 对象的基础上,返回了一个普通的带有 get 和 set 的对象。这样就解决了 Proxy 对象遇到解构和展开运算符后,失去引用的情况的问题。
总语
好了,对于 reactive API 的定义和大致的源码实现就如上面文章中描述的。而分支的逻辑,大家可以自行走不同的 case 去阅读。当然,需要说的是这次的源码只是尝鲜版的,不排除之后正式的会做诸多优化,但是主体肯定是保持不变的。