讲完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 = 10
pauseTracking需要从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()
const computedRunners = new Set()
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,
computedRunners: Set,
effectsToAdd: Set | 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] = '王五'