vue3.0响应式原理(三)

讲完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] = '王五'

 

你可能感兴趣的:(vue,vue3,前端)