Vue3源码之响应系统Reactive模块解读(干货满满,不容错过~)

3,014 阅读18分钟

前言

本文是关于Vue3源码的reactive响应式模块重点分析, 自己看了源码,运行测试用例,同时参考了当前网上的一些优秀的源码分析,在此感谢,很有帮助;关于本文,如有不妥之处,欢迎之处~

Vue3源码模块化分

  • runtime-core、runtime-dom、runtime-test这三个文件夹都是Vue 运行时相关的核心代码。
  • compiler-core、compiler-dom、compiler-sfc这三个文件夹是 Vue 实现的编译器相关代码。
  • server-renderer 是服务端渲染部分。
  • vue 文件夹是整个项目打包构建之后的出口文件。
  • reactive 文件夹是响应式系统部分

本文只分析reactive模块, 因为其他模块还未阅读, [捂脸]

reactivity 模块

整体框架流程图

Vue3.0响应式系统整体框架流程图

注:

  1. isObservableType: Object, Array, Set, Map, WeakMap, WeakSet几种类型
  2. 基本类型: string, number, boolean 及isObservable
  3. 不同颜色标识对应的阶段, 在后文将分模块详细总结

ref.ts

主要逻辑

const isRefSymbol = Symbol() // 生成一个唯一key

export interface Ref<T = any> {
    [isRefSymbol]: true // 用此唯一key,来做Ref接口的一个描述符,让isRef函数做类型判断
    value: UnwrapRef<T> // Ref类型的值是UnwrapRef类型,下面有定义
}

// 通过判断val 是否为对象(非null), 来决定是否采用reactive进行Proxy代理
const convert = <T extends unknown>(val: T): T =>
    isObject(val) ? reactive(val) : val
}

// 根据_isRef判断是否是Ref类型
export function isRef(r: any): r is Ref {
    return r ? r._isRef === true : false
}


// 重载
export function ref<T extends Ref>(raw: T): T
export function ref<T>(raw: T): Ref<T>
export function ref<T = any>(): Ref<T>
export function ref(raw?: unknown) {
  // 非Ref直接返回,不处理
  if (isRef(raw)) {
    return raw
  }
  // 是Ref类型,进行转换, 即只有是Ref类型同时非null的对象需要进行代理
  raw = convert(raw)
  const r = {
    _isRef: true,
    // get 和 set处理与Proxy类似,都是依赖追踪和响应依赖
    get value() {
     // 访问属性时, 依赖追踪ref类型的value属性, 并返回经过转换后的值
      track(r, TrackOpTypes.GET, 'value')
      return raw
    },
    set value(newVal) {
    // 更改属性值时, 触发响应, 并对新值进行转换处理
      raw = convert(newVal)
      trigger(
        r,
        TriggerOpTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  // 最后返回组装后的Ref
  return r
}

Ref函数可以处理任何类型, 将其转化为响应式对象, 但一般用来处理基本类型(string/number/boolean等)

测试:

setup() {
    const state = ref(0) // value => ref
    console.log(state.value) // 0
    state.value = 1
    console.log(state.value) // 1
}

上面的例子, ref函数的参数是数字类型0, 通过添加getter和setter方法, 使其成为响应式数据, 最后经过处理后成为Ref类型(包含_isRef, value属性)并返回. 当访问.value属性时触发getter, 同时进行依赖追踪track; 当改变value属性值时, 重新对新值进行上一步转换处理

UnwrapRef

type UnwrapArray<T> = { [P in keyof T]: UnwrapRef<T[P]> }

// Recursively unwraps nested value bindings.
// 根据泛型T类型递归地展开(object, Array, ComputedRef, Ref)嵌套值并绑定, 主要作用是在以下嵌套情况中,关于ref类型的值,不用使用`.value`访问, 直接解构
// 以下类型声明是访问对象属性obj['prop'], 返回属性值
export type UnwrapRef<T> = {
cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
? 'cRef'
: T extends Ref
  ? 'ref'
  : T extends Array<any>
    ? 'array'
    : T extends Function | CollectionTypes
      ? 'ref' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'ref']

UnwrapRef作为类型, 在reactive模块中, 主要用来约束代理后的数据, 自动解嵌套, 涉及到的地方有以下两处:

第一是在reactive.ts中用来约束定义reactive返回值

// reactive.ts
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
export function reactive(T = any): UnwrapNestedRefs<T>

第二是在ref.ts中, 用来约束ref函数的返回值的value属性

// ref.ts
export interface Ref<T = any> {
    [isRefSymbol]: true
    value: UnwrapRef<T> 
}

综合一起举个栗子:

// reactive<Ref<{[key]: Ref}>>
setup() {
    const r1 = ref({a: ref(0)})
    const r = reactive(r1)
    console.log(r.value.a) // 0, 此处直接解嵌套, 不用r.value.a.value
}

上面的栗子, 结构是reactive<Ref<{[key]: Ref}>>, 当reactive函数传入Ref类型, 返回值UnwrapNestedRefs<T>类型根据三元条件, 最终为T(即,传入的参数);ref({a: ref(0)})返回{_isRef:true, value: {a:ref(0)}}根据Ref类型定义, 可知value属性是UnwrapRefs类型, 所以可自动展开嵌套<{[key]:Ref}>, 递归的流程是Object -> Ref -> Ref -> T. 这块可能有点绕, 需要自己好好捋捋~

以下是源码ref.spec.ts文件关于此块的相关测试用例:

ref.spec.ts-1

ref.spec.ts-2

个人觉得(实际不清楚), Vue3这样设计,是为了简化结构,类似ES6解构的思想, 在嵌套的结构中, 自动展开嵌套的值或Ref

具体可查看源码的ref.spec.ts文件

toRefs

export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  if (__DEV__ && !isReactive(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = {}
  // 遍历对象的所有key,将其值转化为Ref数据
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    _isRef: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  } as any
}

通过toRefs(object)函数, 其中参数Object 只有是响应式数据reactive, 返回的数据才具有响应式; 在开发环境,同时isReative(object)为false, 将会给出警告.
toRefs 解决的是对象的解构丢失原始数据引用的问题, 开发者在函数中错误的解构 reactive,来返回基本类型。 const { x, y } = = reactive({ x: 1, y: 2 }),这样会使 x, y 失去响应式,于是官方提出了 toRefs 方案,在函数返回时,将 reactive 转为 refs,通过遍历对象,将每个属性值都转成Ref数据,这样解构出来的还是Ref数据,自然就保持了响应式数据的引用, 来避免这种情况。

reactive.ts

reactive: 本库的核心方法,传递一个object类型的原始数据,通过Proxy,返回一个代理数据。在这过程中,劫持了原始数据的任何读写操作。进而实现访问或改变代理数据时,能触发依赖其的监听函数effect。

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始数据 和 响应式数据的映射
const reactiveToRaw = new WeakMap<any, any>() // 响应式数据 和 原始数据的映射
const rawToReadonly = new WeakMap<any, any>() // 原始数据 和 只读的映射
const readonlyToRaw = new WeakMap<any, any>() // 只读数据 和 原始数据的映射

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>() // nonReactiveValues 存储非响应式对象, 如: DOM

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const isObservableType = /*#__PURE__*/ makeMap(
  'Object,Array,Map,Set,WeakMap,WeakSet'
)

//  可以被观察的值同时具备的条件:
// 非Vue对象 && 非虚拟节点 && 在可被观察的类型(Object,Array,Map,Set,WeakMap,WeakSet)中 && 不是非响应式
// toRawType: 获取原生的数据类型
// makeMap: 过滤类型, 返回筛选函数
const canObserve = (value: any): boolean => {
  return (
    !value._isVue &&
    !value._isVNode &&
    isObservableType(toRawType(value)) &&
    !nonReactiveValues.has(value)
  )
}

// only unwrap nested ref 只展开嵌套ref
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 若target在只读=>原生数据映射中, 直接返回
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // target 被用户标记为只读, 按只读类别处理
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  
  // 调用createReactiveObject()函数创建响应式对象
  return createReactiveObject(
    target, // 需要被代理的目标对象
    rawToReactive, // 原生=>响应式数据的映射(weakMap)
    reactiveToRaw, // 响应式=>原生数据的映射(weakMap)
    mutableHandlers, // 可变数据(Object,Array)的处理回调
    mutableCollectionHandlers // 可变集合(Map,Set,WeakMap,WeakSet)的处理回调
  )
}


// 创建响应式对象
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 情形1. target非对象直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  // 情形2. target已经有对应的Proxy代理(已经被代理过)
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 情形3. target本身是一个Proxy对象, 直接返回
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 情形4. 不在被观察的白名单中
  if (!canObserve(target)) {
    return target
  }
  // 根据target.cnnstructor区分不同target的handler, 关于两者此处不展开讲,可自行阅读源码
  // baseHandlers: 普通对象的handler对象
  // collectionHandlers: Set/WeakSet/Map/WeakMap的handler对象
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 存储原生=>响应式数据映射表, 联系情形2, 可知目的: 防止reactive已经被reactive的值, 导致多次Proxy
  toRaw.set(observed, target) // 存储响应式=>原生数据映射表, 联系情形3, 可知目的: 防止reactive已经被reactive的值, 导致多次Proxy
  return observed
}

举例:

const origin = {count: 0, info: {name: 'xxl', age: 18}}
const state = reactive(origin)

const fn1 = () => {console.log(state.count)}
const fn2 = () => {console.log(state.info.name)}
const fn3 = () => {console.log(state.info.name, state.count)}

// 依赖收集
const effect1 = effect(fn1)
const effect2 = effect(fn2)
const effect3 = effect(fn3)

// 响应触发
state.count
state.count++
state.info.name = 'ada'

初始化阶段(代理):

reactive(target:any):UnwrapNestedRef
=> 调用 CreateReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers)
=> 调用 new Proxy(target, baseHandlers|collectionHandlers), 返回observed
若target是深层结构, 重复以上步骤

该模块流程图如下:

初始化流程图

effect.ts

effect 类似于 Vue2.x中的 Watcher(观察者)

依赖收集器部分

首先先看下依赖收集器:

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

上面的代码可以用以下图来更清晰的表示:

整体可表示:
targetMap:WeakMap = <target:any, Map<target.key:any, Set<ReactiveEffect>>>
举个栗子:

const origin = {count: 0, info: {name: 'xxl', age: 18}}
const statte = reactive(origin)
effect(() => {consoel.log(state.count)}) // effect1
effect(() => {consoel.log(state.info.name)}) // effect2
effect(() => {consoel.log(state.info.name +'is'+ state.count)}) // effect3

上面代码的依赖如下:

综合以上, 从代码设计来看, 个人感觉和Vue2.x的Dep依赖收集器思路基本相同

依赖收集部分

// 监听函数接口(混合类型接口)
export interface ReactiveEffect<T = any> {
    (): T // 函数类型
    _isEffect: true // 监听函数的标志
    active: boolean // 监听函数是否在运行, stop后为false
    raw: () => T // raw === fn , 监听函数的原始函数
    deps: Array<Dep> // 包含与此effect相关的所有Dep的数组
    options: ReactiveEffectOptions // 配置项, 见下
}

// 监听函数配置项
export interface ReactiveEffectOptions {
    lazy?: boolean // 是否延迟创建监听函数
    computed?: boolean
    scheduler?: (run: Function) => void //调度器,可以看作是节点,当effect因为依赖改变而需要运行时,需要手动运行调度器运行
    onTrack?: (event: DebuggerEvent) => void // 追踪事件,监听effect内的set操作
    onTrigger?: (event: DebuggerEvent) => void // 触发事件,监听effect的依赖项set
    onStop?: () => void
}

// 根据监听函数的标志`_isEffect`来判断是否为监听函数
export function isEffect(fn: any): fn is ReactiveEffect {
    return fn != null && fn._isEffect === true
}

// EMPTY_OBJ = {}
export function effect<T = any>(
    fn: () => T,
    options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
    //如果fn已经是effect,则将fn重置为它的原始函数
    if (isEffect(fn)) {
      fn = fn.raw
    }
    // 创建监听函数
    const effect = createReactiveEffect(fn, options)  // function reactiveEffect(...args: unknown[]): unknown {return run(effect, fn, args)}
    if (!options.lazy) {
      effect() // options.lazy 默认为false, 所以立即执行一次监听函数
    }
    return effect
}

// 创建 effect
function createReactiveEffect<T = any>(
    fn: () => T,
    options: ReactiveEffectOptions
): ReactiveEffect<T> {
    const effect = function reactiveEffect(...args: unknown[]): unknown {
      return run(effect, fn, args) // 执行effect函数时, 调用下面的run()
    } as ReactiveEffect
    effect._isEffect = true
    effect.active = true
    effect.raw = fn // 把回调fn赋值给.raw属性
    effect.deps = []
    effect.options = options
    return effect 
}

// effect堆栈
export const effectStack: ReactiveEffect[] = []

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
    if (!effect.active) { // 当调用stop()停止监听函数的响应式, 直接返回执行的原始函数, 不会被依赖收集
      return fn(...args)
    }
    if (!effectStack.includes(effect)) { 
      cleanup(effect) // 每次执行run(), 清除该effect下的deps, 是为了防止 fn 函数中访问的响应数据属性改动的情况,此时需要重新收集相关属性依赖
      try {
        effectStack.push(effect) // 运行前先把effect压入栈
        return fn(...args) // 执行原始函数并返回, 因为fn中引用了依赖数据, 执行fn触发track依赖收集
      } finally {
        effectStack.pop() // 运行完再把effect推出栈
      }
    }
}


// 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  const effect = effectStack[effectStack.length - 1] // 取出栈顶的effect, 即是与当前key相关的effect, 因为执行effect()函数=>run()函数, push入该effect
  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) // 为dep添加相关effect
    effect.deps.push(dep) // 并把与此effect相关的dep全push到它的deps数组中
    if (__DEV__ && effect.options.onTrack) {
      effect.options.onTrack({
        effect,
        target,
        type,
        key
      })
    }
  }
}

依赖收集阶段:

依赖收集流程图
上图中橙色线路是依赖收集的过程, 下面是具体的代码调用过程:

effect(T=any)
1 --> createReactiveEffect(fn:()=>T, options: ReactiveEffectOptions):ReactiveEffect<T>, 返回effect
2 --> 先执行一次`effect()`
3 --> 调用 run(effect: ReactiveEffect, fn: Function, args: unknown[]): 1. cleanup(effect) => 2. effectStack.push(effect) => 3. fn(...args) => fn()中含有依赖数据, 触发getter `track()`  依赖收集开始({dep.add(effect); effect.deps.push(dep);}) => 4. effectStack.pop(effect)

响应触发部分

// 触发响应, 根据操作类型获取effect, 并区分computed添加到队列之后遍历执行effect
export function trigger(
    target: object,
    type: OperationTypes,
    key?: unknown,
    extraInfo?: DebuggerEventExtraInfo
) {
    const depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      // never been tracked
      return
    }
    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  数组的push/pop
      if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
        const iterationKey = 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.
    // 必须先运行computed effects,以便computed getter可能在运行任何依赖于它们的normal effects之前失效
    computedRunners.forEach(run)
    effects.forEach(run)
}
    
    // 添加effect
    function addRunners(
        effects: Set<ReactiveEffect>,
        computedRunners: Set<ReactiveEffect>,
        effectsToAdd: Set<ReactiveEffect> | undefined
    ) {
        if (effectsToAdd !== void 0) {
          effectsToAdd.forEach(effect => {
            if (effect.options.computed) {
              computedRunners.add(effect)
            } else {
              effects.add(effect)
            }
          })
        }
    }

    function scheduleRun(
        effect: ReactiveEffect,
        target: object,
        type: OperationTypes,
        key: unknown,
        extraInfo?: DebuggerEventExtraInfo
    ) {
        if (__DEV__ && effect.options.onTrigger) {
          const event: DebuggerEvent = {
            effect,
            target,
            key,
            type
          }
          effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
        }
        if (effect.options.scheduler !== void 0) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
    }

响应触发阶段: 整体流程图图下(绿色线条部分):

具体代码调用过程如下:

trigger(target: object, type: TriggerOpTypes, key?: unknown) => 根据TriggerOpTypes和key获取effects, 调用 addRunner(effects, computedRunners, effectsToAdd)分类, 分为effects(normal effects)和computedRunners(computed effects) => 遍历effects,执行回调函数run() => 调用scheduleRun()执行effect, 即fn()

: 关于其中的具体实现细节,需要自己好好捋捋, 大多数是各种边界问题,可结合测试用例, 更加快速定位问题~

computed.ts

// 返回值是一个Ref类型数据
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  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

  // runner是effect函数, 返回effect
  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    // 将效果标记为计算,以便在触发期间获得优先级
    computed: true,
    // 因为这里设置的调度器,依赖触发tirgger事件只是将dirty变为true
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner) // computed(fn)的返回值再次被监听
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
}


// 让依赖computed的effect实现监听逻辑
function trackChildRun(childRunner: ReactiveEffect) {
    if (effectStack.length === 0) {
      return
    }
    // 获取父级effect
    const parentRunner = effectStack[effectStack.length - 1]
    // 遍历子级,也即是本effect,的deps
    for (let i = 0; i < childRunner.deps.length; i++) {
      const dep = childRunner.deps[i]
      // 如果子级的某dep中没有父级effect,则将父级effect添加本dep中,然后更新父级effect的deps
      if (!dep.has(parentRunner)) {
        dep.add(parentRunner) // (1)
        parentRunner.deps.push(dep) // (2)
      }
    }
}

计算函数自身也是一个effect,之前我们说过,它的deps存着所有存着它的dep。而这个dep又指向targetMap中的相应数据。 由于都是引用数据,所以只要把父级effect补充到computed.deps(见上面的(1):dep.add(parentRunner)),就等同于做到了父级effect依赖于computed函数内部依赖的响应数据。

trackChildRun会将子Effect的依赖加入父Effect的依赖,这样在子Effect的依赖触发trigger事件时,子effect不会调用,但会把dirty变为true,父effect会调用,父effect内部对Ref值进行读操作,这时子effect调用将内部value改为新值。这样父effect就不会错过子effect的trigger事件了

例子如下:

const value = reactive({
  foo: 1
})
const cValue = computed(() => value.foo)  // effect 子
let dummy
// 父
effect(() => {
  dummy = cValue.value
})

console.log(dummy) // 1
value.foo = 4 //dummy === 4

而把computed.deps添加到父级effect的deps中(见上面的(2):parentRunner.deps.push(dep)), 是为了链式操作(多个computed存在依赖关系), 例子如下:

const value = reactive({ foo: 0 })
const getter1 = () => value.foo
const getter2 = () => {
  return c1.value + 1
}
const c1 = computed(getter1)
const c2 = computed(getter2)

let dummy
effect(() => {
  dummy = c2.value
})
// console.log(dummy) // 1
value.foo++ // dummy === 2

注: 这里也有点绕[一开始看可能头晕], 需要好好理解下,多看几遍代码, 也可自己注释下相关代码, 运行下测试用例
计算属性:

computed(fn) => 生成computed 的 effect[即依赖收集阶段中第1步], 并返回 Ref => 使用返回的 Ref的value, 触发它的getter, 将运行runner() 函数 => 依赖收集阶段的第3步

Q:

ref VS reactive

对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。

  1. 通过创建一个对象(Ref), 将原始数据保存在Ref的属性value当中,再将它的引用返回给使用者
  2. 通过toRefs(object)函数, 解决对象的解构丢失原始数据引用的问题 通过遍历对象,将每个属性值都转成Ref数据,这样解构出来的还是Ref数据,自然就保持了响应式数据的引用 toRefs 解决的问题就是,开发者在函数中错误的解构 reactive,来返回基本类型。const { x, y } = = reactive({ x: 1, y: 2 }),这样会使 x, y 失去响应式,于是官方提出了 toRefs 方案,在函数返回时,将 reactive 转为 refs,来避免这种情况。

总的来说, reactive 目前支持的类型为 Object|Array|Map|Set|WeakMap|WeakSet , refs支持的类型为基本数据类型, toRefs解决对象解构赋值后引用丢失问题
具体可参考: vue-composition-api-rfc.netlify.com/#ref-vs-rea…

// test case: 

test('toRefs', () => {
  const a = reactive({
    x: 1,
    y: 2
  })

  const { x, y } = toRefs(a)

  expect(isRef(x)).toBe(true)
  expect(isRef(y)).toBe(true)
  expect(x.value).toBe(1)
  expect(y.value).toBe(2)

  // source -> proxy
  a.x = 2
  a.y = 3
  expect(x.value).toBe(2)
  expect(y.value).toBe(3)

  // proxy -> source
  x.value = 3
  y.value = 4
  expect(a.x).toBe(3)
  expect(a.y).toBe(4)
}

深度侦测实现

function createGetter() {
    return function get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return isObject(res) ? reactive(res) : res // 通过对Reflect.get()的返回结果进行reactive递归调用, 达到深度侦测
    }
}

参考: juejin.cn/post/684490…

其他细节问题

Q: reactive.ts中createReactiveObject函数中已经有toProxy.set(target, observed), 为啥还需要 toRaw.set(observed, target), 它存在的意义?
A: 具体可看文中该部分注释, 联系上下文, 主要是为了优化包装后的对象再次被传入的情况,防止多次proxy, 其实两者都 起到了缓存的作用

Q: computed.ts 中 trackChildRun函数的作用? 或者可以说dep.add(parentRunner); parentRunner.deps.push(dep);代码的作用分别是?
A:
1.parentRunner.deps.push(dep);作用:

const value = reactive({ foo: 0 })
const getter1 = () => value.foo
const getter2 = () => {
    return c1.value + 1
}
const c1 = computed(getter1)
const c2 = computed(getter2)

let dummy
effect(() => {
    dummy = c2.value
})
// console.log(dummy)
value.foo++

上面的代码的effect与dep的关系图如下:

effect与dep关系图
若注释掉源码中的parentRunner.deps.push(dep);这句代码, 进行单元测试, 或是上面的代码, 将会发现effect3无法加入到foo对应的dep中, 是由于effect2.deps中没有关联到fooDep, 而从代码可知effect3的raw(即()=>{dummy = c2.value})依赖于c2.value(即effect2中的raw), effect3是effect2的parentRunner, 去除parentRunner.deps.push(dep);后,失去依赖联系,故得到如下结果:

其中

effect1: 是指effect.raw = ()=>{value.foo}的effect
effect2: 是指effect.raw = ()=>{return c1.value + 1}的effect
effect3: 是指effect.raw = ()=>{dummy = c2.value}的effect

关于这个问题, 可自行实验, [累死了]

2.dep.add(parentRunner)作用:

// 依赖于computed的effect 依赖追踪
const value = reactive({
    foo: 1
})
const cValue = computed(() => value.foo)  // effect 子, const runner = effecf, 返回Ref
let dummy
// 父
effect(() => {
    dummy = cValue.value
})

value.foo = 4
console.log(dummy)
// console.log(cValue.value)

捋清了上面的问题,这个就很好解了,具体不详细描述了,理解不来,可在自己的草稿本画画~

最后

说下关于如何调试阅读源码, 我自己的方法是:

  1. 单元测试: 先到根目录安装下包 npm install, 再运行下 reactive模块下的测试用例 jest packages/reactivity/__tests__/xxxx.spec.ts
  2. 打包编译成js源码, 再根据API, 写栗子测试, 这个在浏览器运行, 可以通过断点查看数据结构及流程, 关于如何打包可参考 juejin.cn/post/684490…

最后的最后, 说下自己的感觉,不喜勿喷,看源码应该有耐心,可参考他人优秀的分析文章,再认真理思绪,同时最重要的是思考这样设计的用意,是否可再优化. 哈哈,终于写完了~~~

参考

juejin.cn/post/684490…
juejin.cn/post/684490…
juejin.cn/post/684490…
zhuanlan.zhihu.com/p/85978064
jooger.me/article/5da…
github.com/vuejs/rfcs/…