Vue3.0-beta源码解读 - 响应式系统

一、目标对象标识

**
类似于渲染系统,vue3.0的响应式系统也有自己的一套flag,用于标记目标对象target(通常是我们传入的数据源)的一些特性

export const enum ReactiveFlags {
  skip = '__v_skip', 
  isReactive = '__v_isReactive',
  isReadonly = '__v_isReadonly',
  raw = '__v_raw',
  reactive = '__v_reactive',
  readonly = '__v_readonly'
}

// 为目标对象target,即待转化为响应式对象的源对象做标记
interface Target {
  __v_skip?: boolean // 跳过,不对target做响应式处理
  __v_isReactive?: boolean // target是响应式的
  __v_isReadonly?: boolean // target是只读的
  __v_raw?: any // target对应的原始数据源,未经过响应式代理
  __v_reactive?: any // target经过响应式代理后的数据源
  __v_readonly?: any // target经过响应式代理后的只读数据源
}

二、reactive模块核心方法

createReactiveObject
将target转化为响应式对象

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 被转化的target必须是object
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    return target
  }
  // target已经被转化为响应式对象,响应式对象的__v_readonly、__v_reactive分别挂载转化为
  // proxy的只读、非只读的对象
  if (
    hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
  ) {
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }
  // 只有target在可转化类型白名单里才会进行转化,否则直接返回target
  // canObserve会校验target的__v_skip是否为true、target类型是否在白名单里、target是否		     
  // 为冻结对象,三折同时满足才会继续转化
  if (!canObserve(target)) {
    return target
  }
  // 转化proxy对象,并将转化后的对象挂载到target的__v_readonly(只读模式)或__v_reactive(非只读模式)属性上
  const observed = new Proxy(
    target,
    // Set、WeakSet、Map、WeakMap类型用的collectionHandlers,其他对象类型使用baseHandlers
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  def(
    target,
    isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
    observed
  )
  return observed
}

数组元素查找方法拦截重写:
响应式系统对数组的indexOf、lastIndexOf、includes进行了拦截重写,调用元素查找方法时,会对数组中的元素进行依赖收集,手机依赖之后,会优先执行Array原型上对应自带的查找方法,但是有可能元素是响应式数据,比如ref数据,原始value被转化为ref对象,因此需要更改为查找响应式数据对应的raw数据。

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    // 重写的方法一定是通过proxy调用的,因此this指向proxy,所以对原数组操作的话
    // 需要访问对应的原始数据__v_raw
    const arr = toRaw(this) as any
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

createGetter
在createReactiveObject方法里创建Proxy对象时会为Proxy设置handlers,createGetter就是创建getter拦截器的。
涉及到Reflect的可以参考下面链接或自行官网查找:
Reflect:Reflect详解
其实中心思想很简单,记住一点就可以:Reflect中的receiver指向属性的实际调用者。比如,在Reflect配合Proxy使用时,通常会涉及到receiver这个参数,当我们访问某个key时,会触发Proxy对应的拦截器(handler),而receiver正是指向访问属性并触发handler的真正源头对象,我们触发handler自然是通过Proxy实例对象,此时分两种情况:

  1. 直接访问Proxy实例中的key;
  2. 源头对象(source)原型链上有Proxy实例,我们访问某个key如果source对象本身没有,会顺着原型链往下游找,如果遇到Proxy实例,就会触发对应的handler,那么此时handler中的receiver就指向source。
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
  	// 访问对应标志位的处理逻辑
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (
      key === ReactiveFlags.raw && 
      receiver ===
        (isReadonly
          ? (target as any).__v_readonly
          : (target as any).__v_reactive) 
          // receiver指向调用者,这里的判断是为了保证触发拦截handler的是proxy对象本身
          // 而非proxy的继承者。触发拦截器的两种途径:1⃣️访问proxy对象本身的属性;2⃣️访问
          // 访问对象原型链上有proxy对象的对象的属性,因为查询属性会沿着原型链向下游依次
          // 查询,因此同样会触发拦截器
    ) {
      // 通过proxy对象本身访问__v_raw属性,返回target本身,即响应式对象的原始值
      return target
    }

	// 访问Array对象上的方法,vue3.0对数组的includes、indexOf、lastIndexOf方法
	// 进行了重写,存储在arrayInstrumentations里
    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
      // arrayInstrumentations[key]的this一定指向proxy实例
      // 也即receiver
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // Proxy预返回值
    const res = Reflect.get(target, key, receiver)

	// key是symbol或访问的是__proto__属性不做依赖收集和递归响应式转化,直接返回结果
    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
      return res
    }

    // 只读target无需收集依赖,因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
    if (!isReadonly) {
      // 通过track函数将依赖存储到对应的全局仓库中
      track(target, TrackOpTypes.GET, key)
    }

    // 浅转换至将target的第一层值转化为响应式,不做递归转化
    if (shallow) {
      return res
    }

    // 访问属性已经是ref对象,保证访问ref属性时得到的是ref对象的value属性值,数组除外
    if (isRef(res)) {
      // ref unwrapping, only for Objects, not for Arrays.
      return targetIsArray ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      // key对应的值如果是对象,需要递归的进行响应式转化,readonly和reactive函数最终
      // 调用的都是createReactiveObject函数,只是触发的响应式转化模式不同
      // ⚠️此处有一点需要注意,3.0版本对响应式做出了优化。在2.0版本中,响应式转化
      // 是在初始化阶段一次性递归转化完成,3.0的处理方式则更加优雅,只有getter拦截到
      // 对象时,才会继续向对象的下游做响应式转化,这样的好处就是:只有真正访问到的数据
      // 才会做转化,如果我们定义了某个数据,但是实际上并没有使用到它,那么我们不会触发
      // getter,自然也就不会继续做响应式转化,这样就做到了“按需转化”
      return isReadonly ? readonly(res) /* 只读响应式转化 */ : reactive(res) /* 非只读响应式转化 */
    }

    return res
  }
}

track
track方法是触发getter过程中的做依赖收集的函数,会将收集到的依赖添加到依赖仓库,结构为target -> key -> dep

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect是当前处于激活状态的effect,也就是触发getter时当前访问的effect,
  // 如果没有对应的激活态effect,也就没必要做依赖收集了
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap为全局数据响应关系仓库,为Map类型,key为组件数据源,每个数据源对应Map中的
  // 一个key,value是一个Set,Set中存放的是数据源中每个key对应的deps,即收集的依赖
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 数据源中每个key对应的依赖仓库
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 添加激活订阅者,deps里存储的effects是收集的订阅者们,相当于vue2.0中的watcher
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

createSetter:
响应式对象proxy对应的setter handler,用来做更新派发

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      // 通过setter入参的新值获取它的原始值,新传入值可能是响应式数据,如果直接和
      // target上的原始值比较是没有任何实际意义的
      value = toRaw(value)
      // target不是数组,且旧值为ref,新值非ref,直接将ref.value更新为新值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // 这里其实就是为了判断receiver是proxy实例还是原型链上有proxy的对象,只有前者会触发
    // 更新派发,防止通过原型链触发拦截器触发更新。
    // 这里处理的非常巧妙,看下createGetter代码,在getter handler中做了一层拦截,当访问
    // __v_raw属性时,只有receiver本身时proxy实例时才会返回target,即原始目标对象。再看
    // 下面的createSetter代码,toRaw会访问receiver的__v_raw属性,从而触发getter
    // handler,由于我们已经做了原型链访问拦截,所以在setter里如果receiver位于原型链上,
    // 那么是访问不到__v_raw属性的,因此确保了只有receiver本身是proxy实例才触发更新派发
    if (target === toRaw(receiver)) {
      // 区分是新增属性和固有属性
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

trigger:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>() // 普通effect集合
  const computedRunners = new Set<ReactiveEffect>() // 计算effect集合
  // add方法用来将依赖Map中的对应dep Set添加到局部集合中,以便后续触发(run)
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // effect和当前激活的effect引用相同,且shouldTrack = true时不会将
        // 遍历的effect推入局部Set,shouldTrack为true表示开启依赖收集模式,
        // 该模式下,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
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // 清空依赖时,将target对应的依赖map中的依赖全部添加到局部Set,准备trigger
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      // 数组的处理要特殊一些,比如push等方法的拦截会触发数组的length属性,
      // 具体可以看下文末的例子e.g.,会详细讲解为什么拦截数组的length属性
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // key不是undefined就会触发依赖局部添加,只不过如果是新增属性对应的dep为空
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    // 对于新增属性或者删除属性的处理
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    // 新增或删除属性,如果是数组,会触发length对应的依赖,如果书普通对象,会触发
    // ITERATE_KEY对应的依赖;如果是Map设置属性,会触发ITERATE_KEY
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      // Object新增属性不会触发getter,因此无依赖收集,需要触发ITERATE_KEY收集的依赖
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    // 对于Map新增或删除属性,统一触发MAP_KEY_ITERATE_KEY所对应的依赖
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  // run函数用来批量执行add方法添加过的effect
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }

    if (effect.options.scheduler) {
      // 非立即执行的effect,即需要使用schedular调度器的effect,比如计算effect
      effect.options.scheduler(effect)
    } else {
      // 立即执行的effect,无需特殊调度处理
      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)
}

createInstrumentationGetter(ES6集合、映射):

vue3.0对ES6新增数据结构单独做了一套拦截处理机制,因为直接用普通对象的Proxy拦截机制无法对这些新的数据结构做统一拦截处理。vue3.0的做法简单直接,直接对这些新增class的方法进行了重写,然后在proxy的getter handler做拦截,这个做法是不是似曾相识,在2.0时代使用defineProperty时,由于对数组比较无奈,也是对数组的方法进行了重写以便拦截。

方法的重写比较简单,源码就不放了,有兴趣的可自行去阅读。重写涉及到的方法有has、delete、add、clear、get、set、forEach,属性有size,规则类似于普通对象和数组:

  1. 设置、添加、删除元素时trigger派发更新;
  2. 遍历、检索元素时track收集依赖;
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  // 指定对应版本的重写方法集
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    // ... 省略无关代码

    // 核心处理逻辑,如果访问的是重写的方法,直接食用重写方法,否则正常访问值即可
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

PS:
vue3.0除了对getter、setter进行了拦截,同时也对deleteProperty和has进行了拦截,但是相对比较简单,deleteProperty是在删除非原型属性时触发trigger,has则是触发track(和vue对数组重写的indexOf等方法类似,检索元素会收集依赖)。本文不展开介绍,有兴趣的可以自行去读源码~

三、effect模块

effect:
effect相当于vue2.0中的watcher,灵感来源于react的hooks,但vue3.0整体的设计风格是函数式的,融入了composition API将各个功能模块做的更加通用化,拥有很高的灵活性,使用时的约束要少得多,vue2.0主要是面向对象式的写法,很多功能模块都是封装在class中,推崇的主要是option API,虽然这样的好处是看似很多工作都在框架内部完成了,但是灵活性要低得多,而且做大的弊病是代码复用率很低,比如我想在多处复用一份响应式数据源,正常来说就实现起来不太容易。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // fn本身就是effect函数
  if (isEffect(fn)) {
    fn = fn.raw // effect.raw是effect的执行函数
  }
  // 创建一个新的effect函数
  const effect = createReactiveEffect(fn, options)
  // 执行effect函数
  if (!options.lazy) { 
    // lazy在computedEffect会用到,懒更新,不执行effect,而是暴露给外部,由computed
    // 控制流程来控制effect的执行时机。其他情况立即执行effect函数即可
    effect()
  }
  return effect
}

options参数为effect的配置项,结构如下:

export interface ReactiveEffectOptions {
  lazy?: boolean // 计算属性懒更新
  computed?: boolean // 计算属性
  scheduler?: (job: ReactiveEffect) => void // 调度处理
  onTrack?: (event: DebuggerEvent) => void // 依赖收集时的钩子
  onTrigger?: (event: DebuggerEvent) => void // 更新派发时的钩子
  onStop?: () => void // effect被stop时的钩子
}

createReactiveEffect:
创建effect函数,effect函数中的fn是执行函数,和vue2.0 watcher.run()类似,fn函数执行会触发对应target[key]的getter,完成依赖收集,在依赖收集前后采用栈数据结构(effectStack)来做effect的执行调度,保证当前effect的优先级最高,并及时清除已收集依赖的内存。

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    // effectStack是一个全局effect栈,用于effect调度。
    if (!effectStack.includes(effect)) { // 防止effect重复添加
      // effect不在effect栈中,即首次做当前effect的收集工作,
      // 将依赖仓库中对应的dep清除掉当前effect,保证effect栈中
      // 存储的effect都是已经被依赖仓库收集的effect,同时保证
      // 依赖仓库中不包含未被收集的依赖
      cleanup(effect)
      try {
      	
        enableTracking()
        // effect压栈
        effectStack.push(effect)
        // 将当前effect设置为全局激活effect,在getter中会收集activeEffect持有的effect
        // activeEffect表示当前依赖收集系统正在处理的effect
        activeEffect = effect
        // 执行effect的执行函数,比如访问target[key],会触发getter,在getter中
        // 将activeEffect收集到依赖仓库。
        // fn在render时就是render逻辑,即生成vnode并patch到真实dom那一套流程
        return fn(...args)
      } finally {
     	// 此时fn执行完毕,依赖已收集到依赖仓库,因此将当前effect出栈,及时释放内存
        effectStack.pop()
        resetTracking()
        // 将activeEffect恢复到effect栈中最后一个effect,即上次的activeEffect,继续
        // 做该effect的收集工作
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++ // effect的uid
  effect._isEffect = true // 标记是否是effect
  effect.active = true // effect睡否处于激活状态
  effect.raw = fn // effect的执行函数
  effect.deps = [] // 包含当前effect的dep数组
  effect.options = options // effect的配置项,包含配置项及hooks
  return effect
}

四、ref模块核心方法

createRef
用于创建ref对象,用于将某个值转化为响应式对象,不同于reactive方法的是,reactive只能接受object类型的入参,ref可以接受基本类型和引用类型。比如,我们可以这样创建一个响应式对象:

const state = ref('ref test');

这样生成的state会挂载getter和setter对value属性进行代理拦截,访问value时会通过track函数进行依赖收集,设置value同样会通过track派发更新。
通常在业务中使用时更推荐直接使用ref方法来生成响应式对象,原因之一如下:

  • ref可接受基本类型数据和引用类型数据,你可以直接将一个基本类型的数据转化为基于全局依赖仓库托管的响应式对象。
  • ref给予createReactive方法封装,当传入引用类型数据源时,ref中会调用createReactive方法对数据源进行响应式转化,因此可以将ref看作createReactive方法的一种扩展。

ref可接受多类型

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

function createRef(rawValue: unknown, shallow = false) {
  // 已经是ref对象直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  // shallow开启时,代表是浅转化模式,只对value进行拦截代理,不对value进行递归响应式转化
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    __v_isRef: true, // 标示是否为ref对象
    get value() {
      // 依赖收集
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      // 当值发生变化时派发更新,需要比较原始值(raw),因为newVal有可能是proxy响应式
      // 对象,比较proxy的原始值具有更强的准确性
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        // 将新设置的值进行对应的响应式转化
        value = shallow ? newVal : convert(newVal)
        trigger(
          r,
          TriggerOpTypes.SET,
          'value',
          __DEV__ ? { newValue: newVal } : void 0
        )
      }
    }
  }
  return r
}

四、计算属性

计算属性是vue对发布订阅设计模式和响应式系统的一个很妙的实践,源码实现非常简洁,缺实现了很实用的使用效果,在实际开发中一个computed真的能帮你节省很多不必要的代码。
我们先抛开vue源码,如果让我们自己设计一个computed,作为业务使用方,它的伪代码应该是类似这样的:

targetVal = computed(() => {
	return val1 + val2;
})

想要达到的效果:***由响应式数据自动计算出的一个target值供业务使用,当计算依赖的响应式数据发生改变时,动态的计算出最新的target。***并且,期待computed函数包装后的同样返回一个响应式数据。
既然是数据发生变化就做某件事,那么大方向上自然而然的就会想到用发布订阅模式来处理这类场景,只是细节如何处理的问题。
知道了想要什么,那我们再看源码就会清晰很多了。

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // computed函数可接受两种类型的入参,1⃣️ComputedGetter,也就是我们传入的
  // () => {return a + b}这种回调,这类computed是只读的,如果我们为生成的
  // computedRef赋值,会warning;2⃣️包含ComputedGetter和ComputedSetter
  // 的options,该模式下允许使用者为computedRef.value赋值,此时的computedRef
  // 完全就是一个响应式对象,因此需要在getter中收集依赖
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  // 当触发getter时,getter中的响应式数据会触发数据对应的getter拦截器,因此下面的
  // runner会被当作依赖收集到各数据对应的dep中
  const runner = effect(getter, {
    // lazy用于标示effect是懒更新,即effect包装时不立即执行,而是
    // 返回外部由外部决定执行时机
    lazy: true, 
    // mark effect as computed so that it gets priority during trigger
    // 标示是计算属性,在trigger时保证优先执行
    computed: true,
    // 调度,trigger中检查effect.options中到有scheduler时,会优先执行scheduler回调
    // 通常非立即执行effect可借助scheduler做一些中间态的过度行为,配合effect外部执行
    // effect本身的时机,形成一条时序可控的操作流程,比如计算属性的时序如下:
    // 1⃣️getter中的响应数据更新,触发trigger派发更新,trigger中会触发effect的
    // scheduler回调,将dirty置为true,等待触发computedRef;2⃣️getter中的响应数据更新
    // 同时会触发渲染effect的执行,渲染effect立即执行,其getter会触发render函数,重新
    // 生成vnode -> patch,期间会访问计算属性,触发computedRef的getter,此时由于
    // 已经在scheduler中将dirty置为true,因此computedEffect的getter会重新计算最新值。
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    __v_isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      // dirty控制get新的计算值的时机,只有dirty为true时才会触发新的计算
      if (dirty) {
      	// 执行runner时会触发effect的getter,也就是我们传入的计算函数,
      	// 计算函数会触发对应响应式数据的getter -> track,执行依赖收集,因此计算依赖
      	// 的值会将computed对应的effect存入想对象的dep,当依赖的响应数据
      	// 变化时,触发对应的setter -> trigger,trigger中对computedEffect有
      	// 相应的处理逻辑,可移步trigger查看~
        value = runner()
        dirty = false
      }
      // 如果computed非只读,就需要收集依赖,用于值改变时的trigger
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

简单示例

数组方法的触发

const arr = [2, 3, 678]
const proxy = new Proxy(arr, {
    get(target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
        console.log('set', key, value)
        return Reflect.set(target, key, value, receiver);
    }
})
proxy.push(2)

打印结果如下:
get push
get length
set 3 2
set length 4

说明调用push方法,会访问数组的push和length属性,然后会设置key为3的值等于2,并修改数组长度,因此setter会拦截3和length。pop、shift、unshift也是类似。

proxy.splice(1, 1)

再看下splice的触发结果:
get splice
get length
get constructor
get 1
get 2
set 1 6
get 3
set 2 7
get 4
set 3 9
set length 4
因为涉及到遍历访问和移位赋值操作,因此触发的log较多。
总结:
push这类增加元素的方法,由于是新增元素,因此需要关注getter和setter中均触发过的key,如果某个key只是setter中触发了,但是getter并未触发因此该key就不会收集依赖,因此在trigger时无法派发有效的更新。
仔细观察会发现push的过程中getter和setter同时触发了length属性,因此getter时length属性做了对应的依赖收集工作,那么我们在setter时只需要拦截数组的length属性,对length属性的dep进行更新派发就可以了。

你可能感兴趣的:(VUE3.0源码全系列解读)