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
去阅读。当然,需要说的是这次的源码只是尝鲜版的,不排除之后正式的会做诸多优化,但是主体肯定是保持不变的。