【源码】Reactive 源码
前言
用了很长时间的 componsition-api
了,最近想看看源码,抱着单纯的学习心态先从 reactive
开始吧。
个人习惯:
- 看代码要带着问题去看,不要盲目的去看
- 问题就是这次看源码的主线,要围绕着主线去展开,过程中和主线没有多大关系的该忽略掉就忽略掉
- 开源项目一般都封装的比较好,有可能一个函数中会引用多个文件中的函数,每次跳转的时候将跳转的目录记下来,避免跳着跳着就不知道跳哪去了
- 进入到一个文件中先将函数都收起来,便于查看
前置准备
-
把
vue-next
从 github 上把项目clone
下来。 -
通过
yarn install
安装依赖。 -
将
package.json
中dev
脚本增加sourcemap
最终命令为:"dev": "node scripts/dev.js --sourcemap"
。 -
因为
vue3
是通过rollup
打包的,所以还需要安装rollup
。 -
执行
npm run dev
在 package/vue/ 目录下会生成一个dist
文件夹。 -
在 package/vue/examples/ 目录下新建
init.html
文件。<!DOCTYPE html> <html lang="en"> <body><div id="app"><h1>hello</h1></div><script src="../dist/vue.global.js"></script><script>const { watch, watchEffect, createApp, reactive } = Vuedebuggerconst data = reactive({a: 1,b: 2,count: 0})</script> </body> </html>
-
然后将这个文件以服务的形式跑起来进入
debug
,通过断点可以进入到reactive
函数。
数据响应式 Reactive
reactive
函数一开始就判断了如果传入的 target
如果是一个只读的对象则 return target
。
紧接着调用了 createReactiveObject
函数,先来看一下函数对应的参数:
target
将要被代理的对象- 是否只读
baseHandlers
和collectionHandlers
都是proxy
的handler
,对应的实参分别是mutableHandlers
和mutableCollectionHandlers
,根据target
类型来决定proxy
的handler
reactiveMap
是当前文件中声明的一个常量,用于存储依赖,现在只需要它是一个weakMap
类型数据就好
函数一开始对 target
做了几种情况的判断,针对不同情况做了对应的 return
。
主要关注点是这一段代码
const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
这里会根据 target
的类型来选择对应的 handler
,targetType
是调用 getTargetType
函数的返回值,targetType
为 Map/Set/WeakMap/WeakSet
的时候会将 collectionHandlers
传入 Proxy
,反之采用 baseHandlers
。
baseHandlers
baseHandlers
对应的 mutableHandlers
get
get
对应的 createGetter
的返回值
createGetter
接收两个参数 isReadonly
和 shallow
,在 get
方法中先判断了三种特殊情况,针对每种不同情况 return 了不同的值,其中在这几个判断中用到的 shallowReadonlyMap/readonlyMap/shallowReactiveMap
它们的类型和 reactiveMap
的类型都一样都是 weakMap
,只不过是对应不同的状态。
下边判断了是不是数组,如果是数组并且 isReadonly
为 false
且 key
是 includes/indexOf/lastIndexOf/push/pop/shift/unshift/splice
其中的一个的话则执行
return Reflect.get(arrayInstrumentations, key, receiver)
也就是对上述那些方法进行了重写。这段代码是为了解决边缘情况
includes/indexOf/lastIndexOf
是为了避免产生以下情况const obj = {} const arr = reactive([obj]) arr.indexOf(obj) // -1 正常情况下应该返回的是 0
push/pop/shift/unshift/splice
是为了避免这些改变数组长度的方法在某些情况下进入死循环
再往下获取了 key
在 target
中对应的值
const res = Reflect.get(target, key, receiver)
接着判断 key
是 symbol
的情况。如果 isReadonly
为 false
则调用 track
函数进行依赖收集。
track
这个函数往后放一下,看完这个 get
函数后再回头来看 track
。
接着又判断了 createGetter
传入的 shallow
为 true
和 res
为 ref
类型的这两种情况。
如果 res
还是一个对象并且 isReadonly
为 false
则递归调用 reactive
函数,反之调用 readonly
函数。
从调用 reactive
函数这里可以看出,vue3
中的响应式和 vue2
的差别不仅在 defineProperty
和 proxy
上,在处理响应式的时机上也有变化,defineProperty
是一上来就将 target
上的所有属性都变成响应式的,但是 vue3
是在当你去读取这个 key
的时候,采取将 key
对应的 value 转换成响应式的。
接下来去看一下 track
函数,这个函数的作用是用来收集依赖的。
一开始是一个判断条件,接下来的这段代码是为了获取当前 key
对应的数据同时构造一个数据结构
let depsMap = targetMap.get(target)
if (!depsMap) {targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {depsMap.set(key, (dep = new Set()))
}
构造出来的数据结构是这样的
targetMap = {target: {key: [dep,...] // dep, set 类型} // map 类型
} // weakMap 类型
后边将 activeEffect
添加到 dep
中。这里的 activeEffect
其实就是我们要收集的依赖。
后边又将 dep
添加到了 activeEffect
的 deps
中,这一块当时在看的时候并不明白,后来是查资料明白的,我们放到后边说。如果是开发环境还调用了 onTrack
函数,这个函数在文档中有说,用于调试侦听器的行为。
至此,track
函数就结束了。
遇到的问题
-
activeEffect.deps.push(dep)
是什么,为什么要这样做语义上来看是将当前
key
所对应的dep
push
到了activeEffect
的deps
中,但是为什么要这样做还有待思考。网上查了一番,这篇文章写的还是很不错的,对这个问题点也有一定的解析。文章内的总结: 这个操作是在为在
reactiveEffect
方法中提到的cleanup
方法做准备,每次收集activeEffect
之前,会先将activeEffect
中的deps
清空,然后再进行收集依赖。先清空再收集就是为了避免如果activeEffect
中有在特定条件下才会触发的依赖收集,之前已经收集过了,但是这次不需要收集,所以会先把之前收集的清空掉,然后再针对当前这次需要收集的依赖进行收集,保证当前收集的依赖肯定是当前需要被收集的。这里又引申除了reactiveEffect
方法,这个方法在watchEffect
中会用到,本文不涉及,所以不展开说。set
set
对应的 createSetter
的返回值
createSetter
接收一个参数 shallow
用来表示是不是浅层对象,然后直接 return
了 set
函数。
首先判断了不是浅层对象则处理 oldValue
是 ref
类型,但 newValue
不是 ref
类型的情况,因为 ref
类型的数据已经是响应式的了,所以不需要再次通过 trigger
函数来再次触发依赖。
ref
和reactive
差不多,都可以返回一个响应式的数据,但是ref
需要通过.value
的形式来获取值。
后边的代码就是判断 target
是不是数组,用 Reflect.set()
将 value
添加到 target
中,后续又判断了 key
在不在 target
中,如果 key
不在 target
中则按 ADD
类型调用 trigger
函数触发依赖,反之以 SET
类型调用 trigger
函数。调用完之后将 Reflect.set()
的返回值 return
出去。
接下来看一下 trigger
函数
从 targetMap
中获取 target
对应的依赖,没有则 return
。
定义了 set
类型的 effects
和 add
方法,add
方法主要是将传入的 deps
遍历,然后将每个 effect
添加到 effects
中,这样 effects
中就保存了所有待执行的 effect
。
下边就是根据传入的 type
来执行对应的 add
函数。
再往下就定义了 run
函数,它负责来执行 effect
,如果是开发环境 effect
中有 onTrigger
方法的话会先执行 onTrigger
方法,这个方法和 track
函数中调用的 onTrack
函数是一个意思,文档中有说,用于调试侦听器的行为。如果 effect
中有调度器的话会选择用调度器来执行 effect
否则直接执行 effect
。
run
函数的定义完了就是遍历 effects
,将 run
函数传入并执行。
以上就是 trigger
函数的所有了。
deleteProperty
这个方法就没什么好说的了,用 Reflect.deleteProperty()
执行删除,判断这个 key
是 target
自身的则调用 trigger
触发依赖,然后 return
Reflect.deleteProperty()
的返回值。
has
执行 Reflect.has()
,在 key
是非 Symbol
类型的时候调用 track
函数收集依赖,然后 return
Reflect.has()
的返回值。
ownKeys
调用 track
收集依赖,return Reflect.ownKeys(target)
。
collectionHandler
collectionHandlers
对应的 mutableCollectionHandlers
get
get
对应的 createInstrumentationGetter
的返回值
createInstrumentationGetter
函数定义了 isReadonly
和 shallow
两个参数,这里在调用的时候传入的都是 false
。
方法内也根据这两个参数去获取了对应的 handlers
定义为 instrumentations
,后边判断了 key
的三种特殊情况,这里的 key
代表的是调用 get
方法时的 key
,根据 key
的不同 return 对应的值。
最后判断 key
是不是 instrumentations
自身的属性,如果是则 Reflect.get()
传入的第一个值是 instrumentations
,反之直接将 target
,最后将 Reflect.get()
的返回值 return。
这个 collectionHandlers
不进行具体展开,主要还是围绕着我们的主题进行。
collectionHandlers
到此结束。
总结
-
vue3
的响应式将target
对象传入proxy
,然后利用handlers
,在执行get/has/ownKeys
等获取值方法的时候进行依赖收集,在执行set/deleteProperty
等更改值方法的时候进行触发依赖。 -
vue3
对于target
的某一个key
的value
值还是对象的时候,只有在读取到这个key
的时候才将value
进行响应式处理,而vue2
的处理是初始化的时候直接将所有属性及属性值进行响应式处理。以上就是我关于
reactive
源码的阅读过程,在我最终读完之后去跑 demo 的时候发现进入到track
函数的时候,在一开始那个判断的地方就被return
出去了,并没有真正的收集依赖。这也就产生了我的另外一个问题:什么时候才会收集依赖呢。有了这个问题,可以继续把源码看下去了。
参考链接
- 源码系列:Vue3深入浅出(一)
- Vue3 文档
- Vue3最啰嗦的Reactivity数据响应式原理解析