You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constarrayInstrumentations: Record<string,Function>={};['includes','indexOf','lastIndexOf'].forEach(key=>{arrayInstrumentations[key]=function(...args: any[]): any{// 如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。// 所以这里的 this 指向 receiver,即 proxy 实例,toRaw 为了取得原始数据constarr=toRaw(this)asany// 对数组的每个值进行 track 操作,收集依赖for(leti=0,l=(thisasany).length;i<l;i++){track(arr,TrackOpTypes.GET,i+'')}// we run the method using the original args first (which may be reactive)// 参数有可能是响应式的,函数执行后返回值为 -1 或 false,那就用参数的原始值再试一遍constres=arr[key](...args)if(res===-1||res===false){// if that didn't work, run it again using raw values.returnarr[key](...args.map(toRaw))}else{returnres}}})
constset=/*#__PURE__*/createSetter()// 参考文档《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10functioncreateSetter(shallow=false){returnfunctionset(target: object,key: string|symbol,value: unknown,receiver: object): boolean{constoldValue=(targetasany)[key]if(!shallow){value=toRaw(value)// 如果原来的值是 ref,但新的值不是,将新的值赋给 ref.value 即可。if(!isArray(target)&&isRef(oldValue)&&!isRef(value)){oldValue.value=valuereturntrue}}else{// in shallow mode, objects are set as-is regardless of reactive or not}consthadKey=hasOwn(target,key)constresult=Reflect.set(target,key,value,receiver)// don't trigger if target is something up in the prototype chain of originalif(target===toRaw(receiver)){if(!hadKey){// 如果 target 没有 key,就代表是新增操作,需要触发依赖trigger(target,TriggerOpTypes.ADD,key,value)}elseif(hasChanged(value,oldValue)){// 如果新旧值不相等,才触发依赖// 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter// 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变// 但由于 length 也是自身属性,所以 value === oldValuetrigger(target,TriggerOpTypes.SET,key,value,oldValue)}}returnresult}}
// 触发依赖exportfunctiontrigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown,unknown>|Set<unknown>){constdepsMap=targetMap.get(target)// 如果没有收集过依赖,直接返回if(!depsMap){// never been trackedreturn}// 对收集的依赖进行分类,分为普通的依赖或计算属性依赖// effects 收集的是普通的依赖 computedRunners 收集的是计算属性的依赖// 两个队列都是 set 结构,为了避免重复收集依赖consteffects=newSet<ReactiveEffect>()constcomputedRunners=newSet<ReactiveEffect>()constadd=(effectsToAdd: Set<ReactiveEffect>|undefined)=>{if(effectsToAdd){effectsToAdd.forEach(effect=>{// effect !== activeEffect 避免重复收集依赖if(effect!==activeEffect||!shouldTrack){// 计算属性if(effect.options.computed){computedRunners.add(effect)}else{effects.add(effect)}}else{// the effect mutated its own dependency during its execution.// this can be caused by operations like foo.value++// do not trigger or we end in an infinite loop}})}}// 在值被清空前,往相应的队列添加 target 所有的依赖if(type===TriggerOpTypes.CLEAR){// collection being cleared// trigger all effects for targetdepsMap.forEach(add)}elseif(key==='length'&&isArray(target)){// 当数组的 length 属性变化时触发depsMap.forEach((dep,key)=>{if(key==='length'||key>=(newValueasnumber)){add(dep)}})}else{// schedule runs for SET | ADD | DELETE// 如果不符合以上两个 if 条件,并且 key !== undefined,往相应的队列添加依赖if(key!==void0){add(depsMap.get(key))}// also run for iteration key on ADD | DELETE | Map.SETconstisAddOrDelete=type===TriggerOpTypes.ADD||(type===TriggerOpTypes.DELETE&&!isArray(target))if(isAddOrDelete||(type===TriggerOpTypes.SET&&targetinstanceofMap)){add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))}if(isAddOrDelete&&targetinstanceofMap){add(depsMap.get(MAP_KEY_ITERATE_KEY))}}construn=(effect: ReactiveEffect)=>{if(__DEV__&&effect.options.onTrigger){effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})}if(effect.options.scheduler){// 如果 scheduler 存在则调用 scheduler,计算属性拥有 schedulereffect.options.scheduler(effect)}else{effect()}}// Important: computed effects must be run first so that computed getters// can be invalidated before any normal effects that depend on them are run.computedRunners.forEach(run)// 触发依赖函数effects.forEach(run)}
// track 的类型exportconstenumTrackOpTypes{GET='get',// get 操作HAS='has',// has 操作ITERATE='iterate'// ownKeys 操作}// trigger 的类型exportconstenumTriggerOpTypes{SET='set',// 设置操作,将旧值设置为新值ADD='add',// 新增操作,添加一个新的值 例如给对象新增一个值 数组的 push 操作DELETE='delete',// 删除操作 例如对象的 delete 操作,数组的 pop 操作CLEAR='clear'// 用于 Map 和 Set 的 clear 操作。}
type 主要用于标识 track() 和 trigger() 的类型。
trigger() 中的连续判断代码
if(key!==void0){add(depsMap.get(key))}// also run for iteration key on ADD | DELETE | Map.SETconstisAddOrDelete=type===TriggerOpTypes.ADD||(type===TriggerOpTypes.DELETE&&!isArray(target))if(isAddOrDelete||(type===TriggerOpTypes.SET&&targetinstanceofMap)){add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))}if(isAddOrDelete&&targetinstanceofMap){add(depsMap.get(MAP_KEY_ITERATE_KEY))}
前言
学习 Vue3.0 源码必须对以下知识有所了解:
这些知识可以看一下阮一峰老师的《ES6 入门教程》。
如果不会 ts,我觉得影响不大,了解一下泛型就可以了。因为我就没用过 TS,但是不影响看代码。
阅读源码,建议先过一遍该模块下的 API,了解一下有哪些功能。然后再看一遍相关的单元测试,单元测试一般会把所有的功能细节都测一边。对源码的功能有所了解后,再去阅读源码的细节,效果更好。
proxy 术语
友情提醒
在阅读源码的过程中,要时刻问自己三个问题:
正所谓知其然,知其所以然。
阅读源码除了要了解一个库具有什么特性,还要了解它为什么要这样设计,并且要问自己能不能用更好的方式去实现它。
如果只是单纯的停留在“是什么”这个阶段,对你可能没有什么帮助。就像看流水账似的,看完就忘,你得去思考,才能理解得更加深刻。
正文
reactivity 模块是 Vue3.0 的响应式系统,它有以下几个文件:
接下来按重要程度顺序来讲解一下各个文件的 API 用法和实现。
reactive.ts 文件
在 Vue.2x 中,使用
Object.defineProperty()
对对象进行监听。而在 Vue3.0 中,改用Proxy
进行监听。Proxy
比起Object.defineProperty()
有如下优势:reactive()
reactive()
的作用主要是将目标转化为响应式的 proxy 实例。例如:如果是嵌套的对象,会继续递归将子对象转为响应式对象。
reactive()
是向用户暴露的 API,它真正执行的是createReactiveObject()
函数:这个函数的处理逻辑如下:
__v_readonly
,否则为__v_reactive
),指向这个 proxy 实例,最后返回这个实例。添加这个属性就是为了在第 2 步做判断用的,防止对同一对象重复监听。其中第 3、4 点需要单独拎出来讲一讲。
什么是可观察的对象
canObserve()
函数就是用来判断 value 是否是可观察的对象,满足以下条件才是可观察的对象:__v_skip
,__v_skip
是用来定义这个对象是否可跳过,即不监听。Object,Array,Map,Set,WeakMap,WeakSet
才可被监听。传递给 proxy 的处理器对象是什么
根据上面的代码可以看出来,在生成 proxy 实例时,处理器对象是根据一个三元表达式产生的:
这个三元表达式非常简单,如果是普通的对象
Object
或Array
,处理器对象就使用 baseHandlers;如果是Set, Map, WeakMap, WeakSet
中的一个,就使用 collectionHandlers。collectionHandlers 和 baseHandlers 是从
collectionHandlers.ts
和baseHandlers.ts
处引入的,这里先放一放,接下来再讲。有多少种 proxy 实例
createReactiveObject()
根据不同的参数,可以创建多种不同的 proxy 实例:reactive()
。浅层响应的 proxy 实例是什么?
之所以有浅层响应的 proxy 实例,是因为 proxy 只代理对象的第一层属性,更深层的属性是不会代理的。如果确实需要生成完全响应式的 proxy 实例,就得递归调用
reactive()
。不过这个过程是内部自动执行的,用户感知不到。其他一些函数介绍
baseHandlers.ts 文件
在
baseHandlers.ts
文件中针对 4 种 proxy 实例定义了不对的处理器。由于它们之间差别不大,所以在这只讲解完全响应式的处理器对象:
处理器对五种操作进行了拦截,分别是:
其中 ownKeys 可拦截以下操作:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
其中 get、has、ownKeys 操作会收集依赖,set、deleteProperty 操作会触发依赖。
get
get 属性的处理器是用
createGetter()
函数创建的:这个函数的处理逻辑看代码注释应该就能明白,其中有几个点需要单独说一下:
Reflect.get()
builtInSymbols.has(key)
为 true 或原型对象不收集依赖Reflect.get()
Reflect.get()
方法与从对象(target[key])
中读取属性类似,但它是通过一个函数执行来操作的。为什么直接用
target[key]
就能得到值,却还要用Reflect.get(target, key, receiver)
来多倒一手呢?先来看个简单的示例:
运行这段代码会报错:
但做一些小改动就能够正常运行:
这段代码可以正常运行。为什么呢?
区别在于新的这段代码在
set()
方法上多了一个return true
。我在 MDN 上查找到的解释是这样的:set()
方法应当返回一个布尔值。true
代表属性设置成功。set()
方法返回false
,那么会抛出一个TypeError
异常。这时我又试了一下直接执行
p[3] = 100
,发现能正常运行,只有执行push
方法才报错。到这一步,我心中已经有答案了。为了验证我的猜想,我在代码上加了console.log()
,把代码执行过程的一些属性打印出来。从上面的代码可以发现执行
push
操作时,还会访问length
属性。推测执行过程如下:根据length
的值,得出最后的索引,再设置新的置,最后再改变length
。结合 MDN 的解释,我的推测是数组的原生方法应该是运行在严格模式下的(如果有网友知道真相,请在评论区留言)。因为在 JS 中很多代码在非严格模式和严格模式下都能正常运行,只是严格模式会给你报个错。就跟这次情况一样,最后设置
length
属性的时候报错,但结果还是正常的。如果不想报错,就得每次都返回true
。然后再看一下
Reflect.set()
的返回值说明:所以上面代码可以改成这样:
另外,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。
通过上面的示例,不难理解为什么要通过
Reflect.set()
来代替 Proxy 完成默认操作了。同理,Reflect.get()
也一样。数组的处理
在执行数组的
includes
,indexOf
,lastIndexOf
方法时,会把目标对象转为arrayInstrumentations
再执行。从上述代码可以看出,Vue3.0 对
includes
,indexOf
,lastIndexOf
进行了封装,除了返回原有方法的结果外,还会对数组的每个值进行依赖收集。builtInSymbols.has(key)
为 true 或原型对象不收集依赖从
p.toString()
的执行结果来看,它会触发两次 get,一次是我们想要的,一次是我们不想要的(我还没搞明白为什么会有Symbol(Symbol.toStringTag)
,如果有网友知道,请在评论区留言)。所以就有了这个判断:builtInSymbols.has(key)
为true
就直接返回,防止重复收集依赖。再看
p.__proto__
的执行结果,也触发了一次 get 操作。一般来说,没有场景需要单独访问原型,访问原型都是为了访问原型上的方法,例如p.__proto__.toString()
这样使用,所以 key 为__proto__
的时候也要跳过,不收集依赖。set
set()
的函数处理逻辑反而没那么难,看注释即可。track()
和trigger()
将放在下面和 effect.ts 文件一起讲解。deleteProperty、has、ownKeys
这三个函数比较简单,看代码即可。
effect.ts 文件
等把 effect.ts 文件讲解完,响应式模块基本上差不多结束了。
effect()
effect()
主要和响应式的对象结合使用。真正创建 effect 的是
createReactiveEffect()
函数。其中
cleanup(effect)
的作用是让 effect 关联下的所有 dep 实例清空 effect,即清除这个依赖函数。从代码中可以看出来,真正的依赖函数是 activeEffect。执行
track()
收集的依赖就是 activeEffect。趁热打铁,现在我们再来看一下
track()
和trigger()
函数。track()
targetMap 是一个
WeakMap
实例。弱引用是什么意思呢?
当 obj 置为空后,对于
{ a: 1 }
的引用已经为零了,下一次垃圾回收时就会把 weakmap 中的对象回收。但如果把 weakmap 换成 map 数据结构,即使把 obj 置空,
{ a: 1 }
依然不会被回收,因为 map 数据结构是强引用,它现在还被 map 引用着。trigger()
对依赖函数进行分类后,需要先运行计算属性的依赖,因为其他普通的依赖函数可能包含了计算属性。先执行计算属性的依赖能保证普通依赖执行时能得到最新的计算属性的值。
track() 和 trigger() 中的 type 有什么用?
这个 type 取值范围就定义在
operations.ts
文件中:type 主要用于标识
track()
和trigger()
的类型。trigger() 中的连续判断代码
在
trigger()
中有这么一段连续判断的代码,它们作用是什么呢?其实它们是用于判断数组/集合这种数据结构比较特别的操作。看个示例:
effect(() => (dummy = counter.join()))
生成一个依赖,并且自执行一次。在执行函数里的代码
counter.join()
时,会访问数组的多个属性,分别是join
和length
,同时触发track()
收集依赖。也就是说,数组的join
length
属性都收集了一个依赖。当执行
counter.push(1)
这段代码时,实际上是将数组的索引 0 对应的值设为 1。这一点,可以通过打 debugger 从上下文环境看出来,其中 key 为 0,即数组的索引,值为 1。设置值后,由于是新增操作,执行
trigger(target, TriggerOpTypes.ADD, key, value)
。但由上文可知,只有数组的 key 为join
length
时,才有依赖,key 为 0 是没有依赖的。从上面两个图可以看出来,只有
join
length
属性才有对应的依赖。这个时候,
trigger()
的一连串 if 语句就起作用了,其中有一个 if 语句是这样的:如果 target 是一个数组,就添加
length
属性对应的依赖到队列中。也就是说 key 为 0 的情况下使用length
对应的依赖。另外,还有一个巧妙的地方。待执行依赖的队列是一个 set 数据结构。如果 key 为 0 有对应的依赖,同时
length
也有对应的依赖,就会添加两次依赖,但由于队列是 set,具有自动去重的效果,避免了重复执行。示例
仅看代码和文字,是很难理解响应式数据和
track()
trigger()
是怎么配合的。所以我们要配合示例来理解:上述代码执行过程如下:
{ num: 0 }
进行监听,返回一个 proxy 实例,即 counter。effect(fn)
创建一个依赖,并且在创建时会执行一次fn
。fn()
读取 num 的值,并赋值给 dummy。counter.num = 7
这个操作会触发 proxy 的属性设置拦截操作,在这个拦截操作里,除了把新的值返回,还会触发刚才收集的依赖。在这个依赖里把 counter.num 赋值给 dummy(num 的值已经变为 7)。用图来表示,大概这样的:
collectionHandlers.ts 文件
collectionHandlers.ts 文件包含了
Map
WeakMap
Set
WeakSet
的处理器对象,分别对应完全响应式的 proxy 实例、浅层响应的 proxy 实例、只读 proxy 实例。这里只讲解对应完全响应式的 proxy 实例的处理器对象:为什么只监听 get 操作,set has 等操作呢?不着急,先看一个示例:
运行上面的代码会报错。其实这和 Map Set 的内部实现有关,必须通过 this 才能访问它们的数据。但是通过 Reflect 反射的时候,target 内部的 this 其实是指向 proxy 实例的,所以就不难理解为什么会报错了。
那怎么解决这个问题?通过源码可以发现,在 Vue3.0 中是通过代理的方式来实现对 Map Set 等数据结构监听的:
把最后一行代码简化一下:
其中 instrumentations 的内容是:
从代码可以看到,原来真正的处理器对象是 mutableInstrumentations。现在再看一个示例:
生成 proxy 实例后,执行
proxy.set('key', 100)
。proxy.set
这个操作会触发 proxy 的属性读取拦截操作。打断点可以看到,此时的 key 为
set
。拦截了set
操作后,调用Reflect.get(target, key, receiver)
,这个时候的 target 已经不是原来的 target 了,而是 mutableInstrumentations 对象。也就是说,最终执行的是mutableInstrumentations.set()
。接下来再看看 mutableInstrumentations 的各个处理器逻辑。
get
get 的处理逻辑很简单,拦截 get 之后,调用
get(this, key, toReactive)
。set
set 的处理逻辑也较为简单,配合注释一目了然。
还有剩下的
has
add
delete
等方法就不讲解了,代码行数比较少,逻辑也很简单,建议自行阅读。ref.ts 文件
在 Vue2.x 中,基本数值类型是不能监听的。但在 Vue3.0 中通过
ref()
可以实现这一效果。ref()
会把 0 转成一个 ref 对象。如果给ref(value)
传的值是个对象,在函数内部会调用reactive(value)
将其转为 proxy 实例。computed.ts 文件
下面通过一个示例,来讲解一下 computed 是怎么运作的:
computed()
生成计算属性对象,当对 cValue 进行取值时(cValue.value
),根据 dirty 判断是否需要运行 effect 函数进行取值,如果 dirty 为 false,直接把值返回。() => value.foo
) 取值。在取值过程中,读取 foo 的值(value.foo
)。value.foo = 1
),就会 trigger 这个 activeEffect 函数。scheduler()
将 dirty 设为 true,这样 computed 下次求值时会重新执行 effect 函数进行取值。index.ts 文件
index.ts 文件向外导出 reactivity 模块的 API。
Vue3 系列文章
参考资料
The text was updated successfully, but these errors were encountered: