Vue3源碼之響應系統Reactive模塊解讀(乾貨滿滿,不容錯過~)

前言

本文是關於Vue3源碼的reactive響應式模塊重點分析, 本身看了源碼,運行測試用例,同時參考了當前網上的一些優秀的源碼分析,在此感謝,頗有幫助;關於本文,若有不妥之處,歡迎之處~vue

Vue3源碼模塊化分

  • runtime-core、runtime-dom、runtime-test這三個文件夾都是Vue 運行時相關的核心代碼。
  • compiler-core、compiler-dom、compiler-sfc這三個文件夾是 Vue 實現的編譯器相關代碼。
  • server-renderer 是服務端渲染部分。
  • vue 文件夾是整個項目打包構建以後的出口文件。
  • reactive 文件夾是響應式系統部分

本文只分析reactive模塊, 由於其餘模塊還未閱讀, [捂臉]react

reactivity 模塊

總體框架流程圖

Vue3.0響應式系統總體框架流程圖

注:git

  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等)github

測試:typescript

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屬性值時, 從新對新值進行上一步轉換處理npm

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模塊中, 主要用來約束代理後的數據, 自動解嵌套, 涉及到的地方有如下兩處:api

第一是在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.im/post/5d99be…

其餘細節問題

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.im/post/5d9da4…

最後的最後, 說下本身的感受,不喜勿噴,看源碼應該有耐心,可參考他人優秀的分析文章,再認真理思緒,同時最重要的是思考這樣設計的用意,是否可再優化. 哈哈,終於寫完了~~~

參考

juejin.im/post/5d9da4…
juejin.im/post/5db837…
juejin.im/post/5d99be…
zhuanlan.zhihu.com/p/85978064
jooger.me/article/5da…
github.com/vuejs/rfcs/…

相關文章
相關標籤/搜索