1. 程式人生 > 實用技巧 >Vue3資料響應系統

Vue3資料響應系統

vue3 就是基於 Proxy對其資料響應系統進行了重寫,現在這部分可以作為獨立的模組配合其他框架使用。資料響應可分為三個階段:初始化階段 --> 依賴收集階段 --> 資料響應階段

Proxy代理須知

用 Proxy做代理時,我們需要了解幾個問題:

1、 Proxy代理是如何對其 trap進行處理來實現資料響應的?也就是其 get/set裡面是如何做攔截處理(其實這裡的trap預設行為可以通過 Reflect來返回, Reflect物件的方法與 Proxy物件的方法一一對應,只要是 Proxy物件的方法,就能在 Reflect物件上找到對應的方法。這就讓 Proxy物件可以方便地呼叫對應的 Reflect方法,完成預設行為,作為修改行為的基礎。這裡具體可以檢視阮大神的ES6入門)

2、 Proxy代理的物件只能代理到第一層,當代理的物件多層巢狀時,那麼物件內部的深度監測需要如何去實現?

3、當代理物件是陣列時,比如push操作會觸發多次 get/set,因為push操作除了增加陣列的資料項之外,也會引發陣列本身其他相關屬性的改變,因此會多次觸發 get/set,那麼要如何解決呢?

下面我們會稍微分析下vue3 針對這幾個問題做了哪些優化處理。

初始化階段

初始化過程相對比較簡單,通過reactive()方法將資料轉化成 Proxy物件,這裡注意一個比較重要的物件 targetMap,它在依賴收集階段起著比較重要的作用,具體下面會有分析。

export function react
ive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (readonlyToRaw.has(target)) { return target } // target is explicitly marked as readonly by user if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) } ... // 建立proxy物件 function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target already has corresponding Proxy let observed = toProxy.get(target) if (observed !== void 0) { return observed } // target is already a Proxy if (toRaw.has(target)) { return target } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target } const
handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }

Vue3 如何進行深度觀測的?先看下面這段程式碼

let data = { x: {y: {z: 1 } } }
let p = new Proxy(data, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    console.log('get value:', key)
    console.log(res)
    return res
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})
p.x.y = 2

// get value: x
// {y: 2}

上面程式碼我們可以知道 Proxy只會代理一層,因為這裡只是觸發了一次最外層屬性 x的 get,而重新賦值的其內部屬性 y,此時 set並沒有被觸發,所以改變內部屬性是不會監測到的。繼續看,Reflect.get返回的結果正是 target的內層結構,此時 p.x.y的值也已經變成 2了,我們可以判斷當前 Reflect.get返回的值是否為 object,若是則再通過 reactive做代理,這樣就達到了深度觀測的目的了。

Vue3實現過程具體我們可以看下面原始碼:

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    const res = Reflect.get(target, key, receiver)
    if (typeof key === 'symbol' && builtInSymbols.has(key)) {
      return res
    }
    if (isRef(res)) {
      return res.value
    }
    track(target, OperationTypes.GET, key)
    // 當代理的物件是多層結構時,Reflect.get會返回物件的內層結構,我們可以拿到當前res再做判斷是否為object,進而進行reactive,就達到了深度觀測的目的了
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

依賴收集階段

所謂的依賴在Vue3可簡單理解為各種 effect響應式函式,其中包括了屬性依賴的 effect,計算屬性 computedEffect以及元件檢視的 componentEffect

1、在檢視掛載渲染時會執行一個 componentEffect,觸發相關資料屬性getter操作來完成檢視依賴收集。

2、 effect函式執行也會觸發相關屬性的getter操作,此時操作了某個屬性的 effect也會被該屬性對應進行收集(注意這裡的屬性是可觀測的)。

之所以說是響應式的,是因為effect方法回撥中關聯了被觀測的資料屬性,而effect一般是立即執行的,此時觸發了該屬性的 getter,進行依賴收集,當該屬性觸發 setter時,便會觸發執行收集的依賴。另外,這裡每次effect執行時,當前的effect會被壓入一個名為 activeReactiveEffectStack的棧中,是在依賴收集的時候使用。

export function effect(
  fn: Function,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
  if ((fn as ReactiveEffect).isEffect) {
    fn = (fn as ReactiveEffect).raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // effect立即執行,觸發effect回撥函式fn中相關響應資料屬性的getter操作,從而進行依賴收集
    effect()
  }
  return effect
}
...
// 觸發getter操作,進行依賴收集
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
        })
      }
    }
  }
}

開頭說過 targetMap物件在依賴收集過程中的重要作用,看原始碼我們大概知道了,它維護了一個依賴收集的關係表, targetMap是一個 WeakMap,其 key值是當前被代理的物件 target,而 value則是該物件所對應的 depsMap,它是一個 Map, key值為觸發 getter時的屬性值,而 value值則是觸發過該屬性值所對應的各個 effect。

故 targetMap的關係對映可以看成 target --> key --> effect,可以看出 target被觀測後,其屬性 key在被觸發 getter操作時,收集了所依賴的 effect,可以說 targetMap是Vue3進行依賴收集的一個核心物件。

PPT模板下載大全https://www.wode007.com

響應階段

當觸發屬性 setter時,通過 trigger函式會執行屬性對應收集的 effects,也包括 computedEffects,此時通過 scheduleRun逐個呼叫 effect,最後完成檢視更新。

上面我們講過監測陣列的時候可能觸發多次 get/set, 那麼如何防止觸發多次的呢?先看Vue3的原始碼(簡寫省略了部分程式碼):

// setter操作觸發響應
function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  value = toRaw(value)
  // 判斷key是否為當前target自身屬性
  const hadKey = hasOwn(target, key)
  // 獲取舊值
  const oldValue = target[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const result = Reflect.set(target, key, value, receiver)
  ...
  if (!hadKey) {
    // 若屬性不存在標記為add操作
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    // 若值不相等在觸發,並且標記為set操作
    trigger(target, OperationTypes.SET, key)
  }
  ...
  return result
}

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
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 這裡遍歷找出相關依賴的effect
  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) {
      // 這裡當改變陣列length長度時也會觸發相關effect進行響應
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  // 遍歷執行依賴的effect
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  computedRunners.forEach(run)
  effects.forEach(run)
}

function scheduleRun(
  effect: ReactiveEffect,
  target: any,
  type: OperationTypes,
  key: string | symbol | undefined,
  extraInfo: any
) {
  ...
  if (effect.scheduler !== void 0) {
    effect.scheduler(effect)
  } else {
    effect()
  }
}

由原始碼我們可以分析出:1、判斷key是否為當前被代理物件target自身屬性; 2、判斷舊值與新值是否相等。只有這兩個條件其中一個滿足,才有可能執行 trigger。

怎麼理解呢,我們舉個:chestnut:,可以實現一個小的 reactive方法來做資料代理,程式碼如下:

function hasOwn(val, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(val, key)
}
function reactive(data) {
  let observed = new Proxy(data, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return res
    },
    set(target, key, value, receiver) {
      console.log(target, key, value)
      const hadKey = hasOwn(target, key)
      const oldValue = target[key]

      const result = Reflect.set(target, key, value, receiver)
      if (!hadKey) {
        console.log('trigger add operation...')
      } else if(value !== oldValue) {
        console.log('trigger set operation...')
      }

      return result
    }
  })
  return observed
}

let data = ['a', 'b']
let state = reactive(data)
state.push('c')

// ["a", "b"] "2" "c"
// trigger add operation...
// ["a", "b", "c"] "length" 3

state.push(‘c’)會觸發兩次 set,一次是push的值 c,一次是 length屬性設定。

1、設定值 c時,新增了索引 key為 2,*target 是原始的代理物件 [‘a’, ‘c’],這是一個add操作, 故 hasOwn(target, key)返回的是false,此時執行 trigger add operation…。注意在trigger方法中, length沒有對應的 effect,所以就沒有執行相關的 effect。

2、當傳入 key為 length時, length是自身屬性,故 hasOwn(target, key)返回 true, 此時 value是 3, 而 oldValue即為 target[‘length’]也是 3,故 value !== oldValue不成立,不執行 trigger方法

故只有當 hasOwn(target, key)返回true或者 value !== oldValue的時候才執行 trigger。

總結

在分析原始碼之前我們先列舉了用Proxy做代理實現資料響應需要解決的幾個問題,並帶著這些問題一步一步揭開Vue在資料響應系統處理這些問題的面紗,也讓我們進一步瞭解了Vue原始碼編寫有許多巧妙的地方,比如利用 Reflect.get返回值為 target當前觸發的第一層屬性 key值對應的 value值,從而再來判斷是否為Object來進行深度觀測,並且觀測的值存放在一個WeakMap下,這樣相比較遞迴Proxy,Vue的這種實現方式大大提高了資料響應的效能。