讲完effect的大概实现,重新回到在二中一开始讲到订阅的实现,这里重新给到订阅的源码:
function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (isSymbol(key) && builtInSymbols.has(key)) { return res } if (isRef(res)) { return res.value } // console.log(isObject(res)) track(target, OperationTypes.GET, key) return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } }讲完成effect副作用,接下来就容易理解了,首先是判断目标值是否为ref,如果是则返回其value属性,至于什么是ref,会在最后和computed一起讲。接下来的2段代码是createGetter的核心了首先就是依赖收集,源码如下:
export function track( target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack) { return } const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { effect.onTrack({ effect, target, type, key }) } } } }之前说过activeReactiveEffectStack是一个栈,每当首次执行effect副作用时,先将其入栈,当需要获取一个值时,会调用proxy get中的track,通过activeReactiveEffectStack[activeReactiveEffectStack.length - 1]可以获取当前的effect。
分别解释下这段代码,首先判断shouldTrack,为false不进行依赖收集,例如:
const observed = Vue.reactive({ foo:1}) Vue.pauseTracking() const reactiveEffect = Vue.effect(() => { console.log(observed.foo) // 只打印一次1 }) observed.foo = 10pauseTracking需要从api文件中引用出来
然后判断当前effect是否存在,如果一个effect都没有,直接返回。比如讲effect.active置为false,effect不会入栈,有可能会导致当前effect没有的情况,这个时候就不会做依赖收集了。
之后判断type是否为OperationTypes.ITERATE,如果是,则讲key置成名为ITERATE_KEY的symbol,OperationTypes为一个枚举,OperationTypes.ITERATE,字如其名为遍历的意思,至于为什么要设置key为ITERATE_KEY,也是因为当触发proxy的ownKeys时候,遍历target,ownKeys并无key,故这里人为创建一个key,可以看下ownKeys的源码,如下:
function ownKeys(target: any): (string | number | symbol)[] { track(target, OperationTypes.ITERATE) return Reflect.ownKeys(target) }源码比较简单,这里就不做解释了。
依赖收集的核心,就是如下这段代码:
let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { effect.onTrack({ effect, target, type, key }) } }targetMap是一个weakmap,为了方便理解,伪代码如下:
target = {name:'test',age:18} targetMap = [{target:objectMap}] objectMap = [{name:[fn,fn]},age:[fn,fn]]其中fn就是Vue.effect中的副作用函数。每次依赖收集通过activeReactiveEffectStack获取当前的effect,然后通过proxy代理的方法,将target的key与effect绑定,即完成了订阅。!dep.has(effect)主要是防止重复依赖收集,例如:
const observed = Vue.reactive({ foo:1}) const reactiveEffect = Vue.effect(() => { let a= observed.foo let b= observed.foo })第二个比较重要的点则是返回值判断:
return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res如果某个响应式对象,他的某个值也是对象的话,那么在依赖收集时,将其值也设为响应式对象,再对其依赖收集,这个相比于Vue2的好处在于,只有当使用到其值时,再将其设为响应式,不像Vue2需要循环遍历,例如:
const observed = Vue.reactive({foo:{age:13}}) const reactiveEffect = Vue.effect(() => { console.log(observed.foo.age) // 13,20 }) observed.foo.age = 20订阅讲完了,最后再讲一下发布trigger,源码如下:
export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } // console.log(depsMap) const effects = new Set<ReactiveEffect>() const computedRunners = new Set<ReactiveEffect>() if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } // 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) }首先判断targetMap中是否有target,即判断其是否为响应式对象,如果不是,后续代码就无需进行了,之后判断type是否为clear,第一篇有讲过,proxy的handle主要有baseHandlers和collectionHandlers,clear主要是为集合的handlers服务的,这边也不讲。接下来分别做解释,首先是:
// schedule runs for SET | ADD | DELETE if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) }第一段特别简单,就是讲depsMap中的effect副作用函数分发给effets与computedRunners(计算属性和ref最后会讲到),addRunners源码如下:
function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } }第二段的作用则是针对遍历的key和数组做判断的,ITERATE_KEY在依赖收集中有说过,当触发proxy的ownKey方法时,track的的type为Symbol('iterate'),这样做的作用是当遍历一个target,target中的key新增或者删除减少,也会触发副作用,例如:
const observed = Vue.reactive({ name:'test',age:18}) const reactiveEffect = Vue.effect(() => { for(let i in observed) { console.log(i) // 第一次name,age 第二次name,age,male } }) observed.sex = 'male'删除减少同样如此。数组在遍历循环的时候会读取length属性,这时每当数组发生变化触发length的相关副作用即可,这里明显能看出来Vue3使用proxy相比于Vue2的Object.defindProperty在处理数组方面简单了很多,不用像Vue2会对数组原生方法进行劫持,例如:
const observed = Vue.reactive(['张三','李四']) const reactiveEffect = Vue.effect(() => { console.log('触发副作用') // 第一次张三,李四 第二次张三,李四,王五 for(let i of observed) { console.log(i) } }) observed.push('王五')目前pop,splice还是有问题,例如当数组pop后,会将最后个值修改为undefined触发一次副作用,然后修改length触发一次副作用,这个问题后续应该会所调整,例如:
const observed = Vue.reactive(['张三','李四']) const reactiveEffect = Vue.effect(() => { console.log('依赖收集') // 触发了3次 for(let i of observed) { let a = i } }) observed.pop()接下来的代码其实没什么好讲的,就是触发相关副作用,主要用到了scheduleRun函数,源码如下:
function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( extend( { effect, target, key, type }, extraInfo ) ) } if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }主要作用就是触发onTrigger事件,并且运行effect副作用,如果effect有传入scheduler参数,则运行scheduler,即依赖收集与发布运行的不是同一个函数,例如:
const observed = Vue.reactive(['张三','李四']) const reactiveEffect = Vue.effect(() => { let name = observed[0] },{ scheduler() { console.log(observed[0]) // 王五 } }) observed[0] = '王五'