引言
今年,对于从事前端开发的同学而言,很是期待的一件事就是 Vue3.0
的发布。但是,Vue3.0
离发布还是有点时间的,并且正式发布也不代表我们就马上就可以用于业务开发。它还需要完善相应的生态工具。不过正式使用是一码事,我们自己玩又是一码事(hh)。
Vue3.0
特地准备了一个尝鲜版的项目供大家体验 Vue3.0
即将会出现的一些 API
,例如 setup
、reactive
、toRefs
、readonly
等等, 顺带附上Composition API文档 的地址,还没看过的同学赶紧去 Get
,别等到发布才知道(笨鸟要先飞,聪明鸟那更要先飞是吧)。
同样地,我也 Clone
了下来玩了一会,对这个 reactive API
颇感兴趣。所以,今天我们就来看看 reactive API
是什么(定义)?怎么实现的(源码实现)?
一、定义及优点
1.1 定义
reactive API
的定义为传入一个对象并返回一个基于原对象的响应式代理,即返回一个 Proxy
,相当于 Vue2x
版本中的 Vue.observer
。
首先,我们需要知道在 Vue3.0
中彻底废掉了原先的 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 的介绍。
1.2 优点
回到本篇文章所关注的,很明显 reactive API
是对标 data
选项,那么相比较 data
选项有哪些优点?
首先,在 Vue 2x
中数据的响应式处理是基于 Object.defineProperty()
的,但是它只会侦听对象的属性,并不能侦听对象。所以,在添加对象属性的时候,通常需要这样:
// vue2x添加属性
Vue.$set(object, 'name', wjc)
reactive API
是基于 ES2015 Proxy
实现对数据对象的响应式处理,即在 Vue3.0
可以往对象中添加属性,并且这个属性也会具有响应式的效果,例如:
// vue3.0中添加属性
object.name = 'wjc'
1.3 注意点
使用 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.0
用 TypeScript
重构了。所以,大家可能会以为这次会看到一堆 TypeScript
的类型之类的。出于各种考虑,本次我只是讲解编译后,转为 JS 的源码实现(没啥子门槛,大家放心 hh)。
2.1 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.0
用 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;
}
2.2 toRefs
前面讲了使用 reactive
需要关注的点,提及 toRefs
可以让我们方便地使用解构和展开运算符,其实是最近 Vue3.0 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
去阅读。当然,需要说的是这次的源码只是尝鲜版的,不排除之后正式的会做诸多优化,但是主体肯定是保持不变的。
推荐阅读下一篇《4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)》
写作不易,如果你觉得有收获的话,可以帅气三连击!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。