Vue3.0數據響應系統分析(主要針對於reactive)

Vue3.0與Vue2.x的數據響應機制

Vue3.0採用了ES6的Proxy來進行數據監聽javascript

優勢:java

1. 對對象進行直接監聽, 能夠彌補Object.defineProperty沒法監聽新增刪除屬性的短板

2. 無需在遍歷對象進行設置監聽函數

3. 能夠適用於Array, 不須要再分紅兩種寫法
複製代碼

缺點:react

1. 兼容性不足, 致使目前IE11沒法使用
複製代碼

源碼導讀

在分析源碼以前,咱們須要得知幾個變量先:性能優化

rawToReactive:
    類型: <WeakMap>
    值: {
      原始對象: Proxy對象
    }

  reactiveToRaw:
    類型: <WeakMap>
    值: {
      Proxy對象: 原始對象
    }

  targetMap: 
    類型: <WeakMap>
    值: {
       原始對象: new Map([key, new Set([effect])]) // key是原始對象裏的屬性, 值爲該key改變後會觸發的一系列的函數, 好比渲染、computed
    }
複製代碼

首先咱們來看一下reactive函數bash

export function reactive(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
  )
}

複製代碼

首先咱們檢測了原始對象是不是隻讀的代理對象, 緊接着又檢測了是不是隻讀對象, 緣由是readonly對象是不容許進行修改編輯, 因此是不須要進行響應處理, 接下來, 主要的響應系統都在createReactiveObject裏, 下面對該函數進行分析函數

function createReactiveObject( target: any, toProxy: WeakMap<any, any>, // { originObj: proxyObj } toRaw: WeakMap<any, any>, // { proxyObj: originObj } 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
  }
複製代碼

首先對於類型進行了判斷,若不是對象形式則會報錯,而且不進行響應式處理 其次對是否已經處理過該對象進行了判斷,因爲Proxy對象與原對象已經不是同一個指針,因此Vue對兩個對象進行了分別的判斷性能

canObserve判斷是否符合如下四個條件優化

對象符合如下配置
   * 1. 不是Vue實例
   * 2. 不是虛擬DOM
   * 3. 是屬於Object|Array|Map|Set|WeakMap|WeakSet其中一種
   * 4. 不存在於nonReactiveValues
複製代碼

handlers這裏判斷了兩種狀況:ui

  1. 如果Map|Set|WeakMap|WeakSet的一種則採用collectionHandlers
  2. 不然採用baseHandlers

最後進行原對象的代理處理, 而且綁定了二者的關係, 在這裏咱們看見了targtMap的綁定, 這個WeakMap對於數據響應起到了很關鍵的做用,咱們下面會講到,先看下面, 緊接着就返回了代理對象spa

接下來咱們來看下代理對象handler的處理

因爲百分之99的處理都是由baseHandlers來處理,那麼咱們接下來就針對這個handlers進行分析

export const mutableHandlers: ProxyHandler<any> = {
    get: createGetter(false),
    set,
    deleteProperty,
    has,
    ownKeys
  }
複製代碼

很簡單的一個賦值, 咱們從get函數開始分析

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)
      return isObject(res)
        ? isReadonly
          ? // need to lazy access readonly and reactive here to avoid
            // circular dependency
            readonly(res)
          : reactive(res)
        : res
    }
  }
複製代碼

首先獲取了該屬性的值,而後咱們看見Vue對Symbol的一些類型進行分辨, 如果符合條件則直接返回, 接下來這裏劃重點,要考, track函數對於數據響應起到了相當重要的做用, 咱們來看下track函數源碼是怎麼寫的

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
          })
        }
      }
    }
  }
複製代碼

首先這裏定義了一個shouldTrack, 這個變量是用來控制調用生命週期的時候的開關,防止觸發屢次

獲取targetMap裏該對象各個屬性的值, 若沒有,則進行數據初始化new Set, 而且將effect添加到了該集合裏, 這裏咱們看見了 不止dep添加了, effect也添加了, 這裏是有緣由的,咱們等下進行分析

看到這裏, 相信你們都明白了track函數使用進行數據依賴採集的, 以便於後面數據更改可以觸發對應的函數

接下來咱們分析下set函數

function set( target: any, key: string | symbol, value: any, receiver: any // proxy對象 ): boolean {
    value = toRaw(value)
    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)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) { // 判斷更改對象是不是
      /* istanbul ignore else */
      if (__DEV__) {
        const extraInfo = { oldValue, newValue: value }
        if (!hadKey) {
          trigger(target, OperationTypes.ADD, key, extraInfo)
        } else if (value !== oldValue) {
          trigger(target, OperationTypes.SET, key, extraInfo)
        }
      } else {
        if (!hadKey) {
          trigger(target, OperationTypes.ADD, key)
        } else if (value !== oldValue) {
          trigger(target, OperationTypes.SET, key)
        }
      }
    }
    return result
  }
複製代碼

首先一開始也是對於各個類型進行分析而且處理對應的狀況, 咱們主要看一下trigger函數

export function trigger( target: any, // 原始對象 type: OperationTypes, // 判斷是替換仍是增長等等操做 key?: string | symbol, // 對象屬性 extraInfo?: any ) {
    const depsMap = targetMap.get(target) // new Map()
    // console.log('depsMap1', depsMap);
    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 {
      // console.log(key);
      // 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)
  }
複製代碼

在trigger函數裏咱們看到了兩個集合變量, effects與computedRunners, 兩個集合針對兩種類型進行數據採集我麼往下看

引入眼簾的估計就是addRunners, 咱們猜想下 這個addRunners顧名思義應該就是添加執行任務, 下面看下源碼

function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined // Effects集合 ) {
    // console.log('effectsToAdd', effectsToAdd);
    if (effectsToAdd !== void 0) {
      effectsToAdd.forEach(effect => {
        if (effect.computed) {
          computedRunners.add(effect)
        } else {
          effects.add(effect)
        }
      })
    }
  }
複製代碼

effectsToAdd就是track函數裏添加的對象屬性的值 new Set, 用於收集依賴的, 根據effect是不是計算屬性來分別添加到不一樣的集合下, 回到trigger函數裏, 咱們看見了effects與computedRunners進行遍歷執行, 那麼咱們在分析下具體的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()
    }
  }
複製代碼

這裏咱們看見了Vue對effect進行了兩種狀況的判斷, 首先判斷了effect.scheduler是否存在, 若存在則使用scheduler來調用effect, 不存在則進行直接調用, 那麼scheduler究竟是什麼呢? 這裏的scheduler就是Vue的性能優化點,放入隊裏裏, 等到miscroTask裏進行調用, 熟悉Vue2.x的同窗都知道nextTick函數, 這個scheduler能夠看作就是調用了nextTick函數

咱們來看下effect具體是什麼

const effect: ReactiveEffect = function effect(...args: any[]): any {
    return run(effect as ReactiveEffect, fn, args)
  }

  function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
    if (!effect.active) {
      return fn(...args)
    }

    if (activeReactiveEffectStack.indexOf(effect) === -1) {
      cleanup(effect)
      // 初始化mount的時候會執行effect函數, 當前effect是componentEffect, 也就是渲染函數, 此時因爲去獲取了變量數據,也就是觸發了get函數,get函數會觸發track函數, track函數就是用來收集effect, 
      try {
        activeReactiveEffectStack.push(effect)
        return fn(...args)
      } finally {
        activeReactiveEffectStack.pop()
      }
    }
  }
複製代碼

effect實際上就是運行了run函數, 咱們看下run函數的運行, 在運行以前會先cleanup, 這裏咱們就要返回以前所說的track函數, 你們還記得track函數裏, 不僅dep添加了effect, effect也同時添加了dep嗎? 緣由就在這裏, cleanup須要用到

function cleanup(effect: ReactiveEffect) {
    const { deps } = effect
    // console.log('deps', deps);
    if (deps.length) {
      for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect)
      }
      deps.length = 0
    }
  }
複製代碼

該函數清空了dep裏全部的依賴, 那麼膽大心細的同窗會發現一個問題:

在track函數裏已經添加了effect, 那麼爲何在這裏要從新清除掉全部的依賴呢?

理論上看起來是個很雞肋的操做, 可是實際上Vue已經考慮了全方面, 試想一個場景: A組件與B組件是經過v-if來控制展現, 當A組件首先渲染以後, 所對應的的數據就會採集對應的依賴, 此時更改v-if條件, 渲染了B組件, 如果B組件此時更改了A組件裏的變量, 如果A組件的依賴沒有被清除掉, 那麼會產生沒必要要的依賴調用, 因此Vue要事先清除掉全部的依賴, 確保依賴始終是最新的

分析到這咱們已經清楚了Vue3.0的數據響應到底是如何了!

總結

Vue3.0從根本上解決了Vue2.x數據響應系統留下的短板, 可是兼容性上還存在問題,採用尤大一句話, IE11百足之蟲,死而不僵, 暫時還不能徹底拋棄IE11, 但願後期能有新的突破!

相關文章
相關標籤/搜索