划了一个礼拜的水,都不晓得干了点啥,写一写目前对vue3的响应式系统源码阅读一些心得吧。
目录: vue-next/reactivity/dist/reactivity.cjs.js
总体来看分为三部分 1)effect
讲道理没怎么看懂这effect,只是粗略的知道每次变动触发响应式系统的时候,就用一个effect来描述。每个effect其实是一个function
2)vue3著名的proxy
这部分写的是如何把传入的对象处理变成一个proxy
3)暴露的接口如ref()/computed()
这部分是利用上面的两部分完成的,写这篇博客的时候暂时只用到了ref和computed,其他不了解的接口先不写了
##effect部分 假如effect是一个类,那么大概是这样的 class effect{ isLazy : Boolean, // 代表生成后是否马上触发一次
computed : Boolean, // 代表是否computed属性
scheduler : Function, // 触发effect
_isRef : Boolean, // 是否ref
fn : Function, // 处理effect造成的影响,比如是计算属性的话会修改计算值取值
deps : Map // 用于存储effect会影响的所有响应式对象
}
源码的最前面是targetMap对象,该map用于存储所有的依赖关系。key是被监听的对象,value是该对象涉及的全部effect的一个set。(其实每个effect里面也保存了相关的target,确保effect触发的时候能知道需要修改哪些target)
然后是effectStack数组,用于存储目前为止的所有effect(有可能每次修改其实会触发的effect不止一种,可能一种effect里面会触发另一种effect,所以通过这栈来实现结算顺序的控制)
还有一个trackStack数组,这对应是effectStack数组,貌似有一些effect触发的时候是不希望对对象进行监听的,trackStack就是用来标记栈中每个effect是否希望对对象进行监听的。
重点讲一下track(target, type, key)函数,target和key都很好理解,type就是操作类型,包含get,has,iterate,每次调用track就说明对象多了一个依赖,于是对象会被纳入targetMap,并且activeEffect会被添加到target的set里面去
还有一个重点trigger(target, type, key, newValue, oldValue, oldTarget) type包括set,add,delete,clear。 首先会从targetmap看看target是否有被监听,如果没有就省事了,然后声明了两个set:effects和computedRunners。大概就是用来装载修改这属性会导致的UI修改,以及computed的修改。
接下来根据type的类型进行处理:
第一种情况,如果是clear,那么肯定全部相关的依赖都要修改,因此把对象关联的depsMap中的所有effect,根据是否computed分为两组,分别加入上面的effects和computedRunners
第二种情况,如果修改的key是length且target是数组的话,那么遍历数组中所有下标在newVal后面的值,因为修改length的话相当于把length后面的都抛弃了,所以新的length后面的值都需要进行处理,同样分成普通effect和computed加入上面说的两个set中。
最后一种情况,将depsMap中和key关联的effect拿出来,分成两种effect加入上面的两个set中。如果是增/删/map的set,同样需要修改target的遍历器对应的依赖。
最后就是遍历两个set中的每一个effect,如果effect带有scheduler方法的话,则调用scheduler方法,否则直接调用effect。
effect的部分到这里就结束了
##第二部分 接下来到重中之重的proxy部分
(先说一下一段我比较在意的代码builtInSymbols,这是用来返回当前在Symbol原型上目前已有的属性对应的值,并把其中是symbo的装起来,比如Symbol.iterator)
首先定义了一个数组的工具对象,用于封装对'includes', 'indexOf', 'lastIndexOf'这三个方法的访问。先拿到被代理的原数组,然后将方法参数传入,如果有结果则直接返回,没有的话,对参数进行toRaw之后再传入,再返回。(因为vue3对引用的调用都要通过obj.value这样去调用对象原生的值,所以这里先用obj去传入include等方法,如果不行再把obj.value传入)
是定义getter们和setter们,有正常的/浅响应的/只读的/浅响应只读的。总的来说,先判断是否数组。如果是数组且调用的是上面说的三个方法,则直接调用上面的方法并返回;接下来把key对应的value从target上拿出来。如果是浅响应,则track记录一下依赖,返回value;如果value是ref,且target是array,track一下,返回value;如果value是ref且target不是array,返回target[key].value(因为取ref的时候,其实希望返回的是ref.value,备注说ref unwrapping, only for Objects, not for Arrays.) 在getter的最后,如果target不是只读就要track,如果target[key]是一个对象,把对象做成proxy再返回出去,如果是普通类型则直接返回。
接着是setter,只有普通的和浅响应两个版本
先把target[key]现在的值取出保存,如果是浅响应且target不是数组,且目前的值已经是ref且新的value不是ref,则直接赋值返回true;进行set,set之后再判断,如果修改的是原型链上的东西,不做trigger直接返回;如果修改的target本身的属性,判断是新增属性还是修改属性,进行对应的trigger
然后,vue对于proxy的遍历/foreach也做了封装。具体看起来都差不多……就是各种toRaw和track
这里还有很多对get/set/delete等方法的封装,其实就是为了在调用的时候加入track和trigger,就不一一看了
##第三部分,
上来先声明四个map,这里的目的是能够根据任何一个对象找到其对应的proxy,或者根据proxy找到对应的对象
rawToReactive reactiveToRaw rawToReadonly readonlyToRaw
还有一个set,用于存储只读或者不需要响应的值
rawValues
讲一个重点的函数createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers)
简单的说,就是每创建一个proxy,就会在toProxy和toRaw里面添加一次关系,并且同一个对象的话只会添加一次。
然后就是vue.ref,先判断是不是浅响应,如果是深响应的话要对对象的每一层都做监听(其实就是再调用一次ref)。 处理好value之后,创建r,设置r._ref = true,所有get都调用track之后直接返回value;调用set的时候(这里相当于把ref整个换掉了),如果检查到前后的值不同,判断一下是否浅响应,然后触发trigger,结束。
vue.customRef传入工厂函数,用于自定义ref
vue.toRefs 用于创建__ref为true的普通对象……意义是什么? 文档说把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。(但是还是不能响应啊)
最后压轴的是computed
如果传入的是函数,则直接赋值为get,set用默认报错的函数;如果是对象,那么分别取其中的set和get.每次调用计算属性的时候,先检查依赖是否有变化,如果有变化先重新跑一次;然后track一下,返回。