Vue3響應式原始碼分析 - reactive篇

小綿羊發表於2022-06-23

最近一階段在學習Vue3,Vue3中用 reactiveref 等方法將資料轉化為響應式資料,在獲取時使用 trackeffect 中收集依賴,在值改變時,使用 trigger 觸發依賴,執行對應的監聽函式,這次就先來看一下 reactive 的原始碼。

reactive的原始碼在官方原始碼的packages/reactivity/src/reactive.ts檔案中,原始碼中提供了四個Api來建立reactive類物件:

  • reactive:建立可深入響應的可讀寫物件
  • readonly:建立可深入響應的只讀物件
  • shallowReactive:建立只有第一層響應的淺可讀寫物件(其他層,值改變檢視不更新)
  • shallowReadonly:建立只有一層響應的淺只讀物件

它們都是呼叫createReactiveObject方法來建立響應式物件,區別在於傳入不同的引數,本文只講reactive,其他幾個大同小異:

export function reactive(target: object) {
  // 如果是隻讀的話直接返回
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    // 目標物件
    target,
    // 標識是否是隻讀
    false,
    // 常用型別攔截器
    mutableHandlers,
    // 集合型別攔截器
    mutableCollectionHandlers,
    // 儲了每個物件與代理的map關係
    reactiveMap
  )
}

export const reactiveMap = new WeakMap<Target, any>()

createReactiveObject程式碼如下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果代理的資料不是物件,則直接返回原物件
  if (!isObject(target)) {
    return target
  }

  // 如果傳入的已經是代理了 並且 不是readonly 轉換 reactive的直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 檢視當前代理物件之前是不是建立過當前代理,如果建立過直接返回之前快取的代理物件
  // proxyMap 是一個全域性的快取WeakMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 如果當前物件無法建立代理,則直接返回源物件
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  //  根據targetType 選擇集合攔截器還是基礎攔截器
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )

  // 向全域性快取Map裡儲存
  proxyMap.set(target, proxy)
  return proxy
}

其中有個方法是 getTargetType,用來獲取傳入target的型別:

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

export const enum ReactiveFlags {
  SKIP = '__v_skip',              // 標記阻止成為代理物件
  IS_REACTIVE = '__v_isReactive', // 標記一個響應式物件
  IS_READONLY = '__v_isReadonly', // 標記一個只讀物件
  IS_SHALLOW = '__v_isShallow',   // 標記只有一層響應的淺可讀寫物件
  RAW = '__v_raw'                 // 標記獲取原始值
}

const enum TargetType {
  // 無效的 比如基礎資料型別
  INVALID = 0,
  // 常見的 比如object Array
  COMMON = 1,
  // 集合型別比如 map set
  COLLECTION = 2
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

當target被標記為 ReactiveFlags.SKIP 或是 不可擴充的,則會返回 TargetType.INVALID,無法建立代理,因為Vue需要對Target代理附加很多東西,如果是不可擴充的則會附加失敗;或是使用者主動呼叫 markRaw 等方法將資料標記為非響應式資料,那麼也無法建立代理。

export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

看完了入口函式,接下來就是建立Proxy物件的過程了,Vue3會根據getTargetType返回的資料型別來選擇是使用collectionHandlers集合攔截器還是baseHandlers常用攔截器,原因下面講到集合攔截器的時候再說。

常用攔截器baseHandlers:
  1. get 攔截器:

    function createGetter(isReadonly = false, shallow = false) {
      return function get(target: Target, key: string | symbol, receiver: object) {
     if (key === ReactiveFlags.IS_REACTIVE) { // 獲取當前是否是reactive
       return !isReadonly
     } else if (key === ReactiveFlags.IS_READONLY) { // 獲取當前是否是readonly
       return isReadonly
     } else if (key === ReactiveFlags.IS_SHALLOW) { // 獲取當前是否是shallow
       return shallow
     } else if (
       // 如果獲取源物件,在全域性快取WeakMap中獲取是否有被建立過,如果建立過直接返回被代理物件
       key === ReactiveFlags.RAW &&
       receiver ===
         (isReadonly
           ? shallow
             ? shallowReadonlyMap
             : readonlyMap
           : shallow
           ? shallowReactiveMap
           : reactiveMap
         ).get(target)
     ) {
       return target
     }
    
     // 是否是陣列
     const targetIsArray = isArray(target)
    
     // arrayInstrumentations相當於一個改造器,裡面定義了陣列需要改造的方法,進行一些依賴收集等操作
     // 如果是陣列,並且訪問的方法在改造器中,則使用改造器獲取
     if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
       return Reflect.get(arrayInstrumentations, key, receiver)
     }
    
     // 獲取結果
     const res = Reflect.get(target, key, receiver)
    
     if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
       return res
     }
    
     // 如果不是隻讀則收集依賴,Vue3中用track收集依賴
     if (!isReadonly) {
       track(target, TrackOpTypes.GET, key)
     }
    
     // shallow只有表層響應式,不需要下面去深度建立響應了
     if (shallow) {
       return res
     }
    
     // 如果獲取的值是ref型別
     if (isRef(res)) {
       // 如果是陣列 並且 是int型別的 key,則返回,否則返回.value屬性
       return targetIsArray && isIntegerKey(key) ? res : res.value
     }
    
     if (isObject(res)) {
       // *獲取時才建立相對應型別的代理,將訪問值也轉化為reactive,不是一開始就將所有子資料轉換
       return isReadonly ? readonly(res) : reactive(res)
     }
    
     return res
      }
    }

    注意點是當代理型別是 readonly 時,不會收集依賴。
    Vue3對於深層次的物件是使用時才建立的,還有如果結果是ref型別,則需要判斷是否要獲取它的.value型別,舉個?:

    const Name = ref('張三')
    const Array = ref([1])
    
    const data = reactive({
      name: Name,
      array: Array
    })
    
    console.log(Name)          // RefImpl型別
    console.log(data.name)     // 張三
    console.log(data.array[0]) // 1

    Vue3中使用 arrayInstrumentations對陣列的部分方法做了處理,為什麼要這麼做呢? 對於 pushpopshiftunshiftsplice 這些方法,寫入和刪除時底層會獲取當前陣列的length屬性,如果我們在effect中使用的話,會收集length屬性的依賴,當使用這些api是也會更改length,就會造成死迴圈:

     let arr = []
     let proxy = new Proxy(arr, {
    get: function(target, key, receiver) {
      console.log(key)
      return Reflect.get(target, key, receiver)
    }
     })
     proxy.push(1)
     /* 列印 */
     // push
     // length
    // 當把這個程式碼註釋掉時
    // if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
    //     return Reflect.get(arrayInstrumentations, key, receiver);
    // }
    
    const arr = reactive([])
    
    watchEffect(() => {
     arr.push(1)
    })
    
    watchEffect(() => {
     arr.push(2)    
     // 上面的effect裡收集了對length的依賴,push又改變了length,所以上面的又會觸發,以此類推,死迴圈
    })
    
    // [1,2,1,2 ...] 死迴圈
    console.log(arr)

    對於 includesindexOflastIndexOf,內部會去獲取每一個的值,上面講到如果獲取出來的結果是Obejct,會自動轉換為reactive物件:

    let target = {name: '張三'}
    
    const arr = reactive([target])
    
    console.log(arr.indexOf(target)) // -1

    因為實際上是 reactive(target)target 在對比,當然查不到。

  2. set 攔截器

    function createSetter(shallow = false) {
     return function set(target, key, value, receiver) {
         // 獲取舊資料
         let oldValue = target[key];
         if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
             return false;
         }
         // 如果當前不是shallow並且不是隻讀的
         if (!shallow && !isReadonly(value)) {
             if (!isShallow(value)) {
                 // 如果新value本身是響應物件,就把他變成普通物件
                 // 在get中講到過如果取到的值是物件,才轉換為響應式
                 // vue3在代理的時候,只代理第一層,在使用到的時候才會代理第二層
                 value = toRaw(value);
                 oldValue = toRaw(oldValue);
             }
             // 如果舊的值是ref物件,新值不是,則直接賦值給ref物件的value屬性
             if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                 // 這裡不觸發trigger是因為,ref物件在value被賦值的時候會觸發寫操作,也會觸發依賴更新
                 oldValue.value = value;
                 return true;
             }
         }
         const hadKey = isArray(target) && isIntegerKey(key)
             ? Number(key) < target.length
             : hasOwn(target, key);
         const result = Reflect.set(target, key, value, receiver);
         // 這個判斷主要是為了處理代理物件的原型也是代理物件的情況
         if (target === toRaw(receiver)) {
             if (!hadKey) {
                 // key不存在就 觸發add型別的依賴更新
                 trigger(target, "add" /* ADD */, key, value);
             }
             else if (hasChanged(value, oldValue)) {
                 // key存在就觸發set型別依賴更新
                 trigger(target, "set" /* SET */, key, value, oldValue);
             }
         }
         return result;
     };
    }

    set中還有一個要注意的地方就是 target === toRaw(receiver),這主要是為了處理代理物件的原型也是代理物件的情況:

    const child = reactive({})
    
    let parentName = ''
    const parent = reactive({
      set name(value) {
         parentName = value
      },
      get name() {
         return parentName
      }
    })
    
    Object.setPrototypeOf(child, parent)
    
    child.name = '張三'
    
    console.log(toRaw(child)) // {name: 張三}
    console.log(parentName) // 張三

    當這種時候,如果不加上這個判斷,由於子代理沒有name這個屬性,會觸發原型父代理的set,加上這個判斷避免父代理也觸發更新。

集合攔截器collectionHandlers:

集合型別的資料比較特殊,其相關例項方法Proxy沒有提供相關的捕獲器,但是因為方法呼叫屬於屬性獲取操作,所以都可以通過捕獲get操作來實現,所以Vue3也只定義了get攔截:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 注意這裡
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

之前的文章《代理具有內部插槽的內建物件》中說過Proxy代理具有內部插槽的內建物件,訪問Proxy上的屬性會發生錯誤。Vue3中是如何解決的呢?

Vue3中新建立了一個和集合物件具有相同屬性和方法的普通物件,在集合物件 get 操作時將 target 物件換成新建立的普通物件。這樣,當呼叫 get 操作時 Reflect 反射到這個新物件上,當呼叫 set 方法時就直接呼叫新物件上可以觸發響應的方法,這樣訪問的就不是Proxy上的方法,是這個新物件上的方法:

function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {
    get(key: unknown) {
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }
  
  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
  })

  return [
    mutableInstrumentations
  ]
}

接下來看一看幾個具體的攔截器:

  1. get 攔截器:

    function get(
      target: MapTypes,
      key: unknown,
      isReadonly = false,
      isShallow = false
    ) {
      // 如果出現readonly(reactive())這種巢狀的情況,在readonly代理中獲取到reactive()
      // 確保get時也要經過reactive代理
      target = (target as any)[ReactiveFlags.RAW]
      const rawTarget = toRaw(target)
      const rawKey = toRaw(key)
      // 確保 包裝後的key 和 沒包裝的key 都能訪問得到
      if (!isReadonly) {
         if (key !== rawKey) {
           track(rawTarget, TrackOpTypes.GET, key)
         }
         track(rawTarget, TrackOpTypes.GET, rawKey)
      }
      const { has } = getProto(rawTarget)
      const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
      if (has.call(rawTarget, key)) {
         return wrap(target.get(key))
      } else if (has.call(rawTarget, rawKey)) {
         return wrap(target.get(rawKey))
      } else if (target !== rawTarget) {
         target.get(key)
      }
    }

    集合攔截器裡把 keyrawKey 都做了處理,保證都能取到資料:

    let child = {
      name: 'child'
    }
    
    const childProxy = reactive(child)
    
    const map = reactive(new Map())
    
    map.set(childProxy, 1234)
    
    console.log(map.get(child)) // 1234
    console.log(map.get(childProxy)) // 1234
  2. set 攔截器:

    // Map set攔截器
    function set(this: MapTypes, key: unknown, value: unknown) {
      // 存origin value
      value = toRaw(value);
      // 獲取origin target
      const target = toRaw(this);
      const { has, get } = getProto(target);
    
      // 檢視當前key是否存在
      let hadKey = has.call(target, key);
      // 如果不存在則獲取 origin
      if (!hadKey) {
         key = toRaw(key);
         hadKey = has.call(target, key);
      } else if (__DEV__) {
         // 檢查當前是否包含原始版本 和響應版本在target中,有的話發出警告
         checkIdentityKeys(target, has, key);
      }
    
      // 獲取舊的value
      const oldValue = get.call(target, key);
      // 設定新值
      target.set(key, value);
      if (!hadKey) {
         trigger(target, TriggerOpTypes.ADD, key, value);
      } else if (hasChanged(value, oldValue)) {
         trigger(target, TriggerOpTypes.SET, key, value, oldValue);
      }
      return this;
    }
  3. has 攔截器:

    function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
      // 獲取代理前資料
      const target = (this as any)[ReactiveFlags.RAW]
      const rawTarget = toRaw(target)
      const rawKey = toRaw(key)
      // 如果key是響應式的都收集一遍
      if (key !== rawKey) {
         !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
      }
      !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
    
      // 如果key是Proxy 那麼先訪問 proxyKey 在訪問 原始key 獲取結果
      return key === rawKey
     ? target.has(key)
     : target.has(key) || target.has(rawKey)
    }
  4. forEach 攔截器:

    function createForEach(isReadonly: boolean, isShallow: boolean) {
      return function forEach(
         this: IterableCollections,
         callback: Function,
         thisArg?: unknown
      ) {
     const observed = this as any
     const target = observed[ReactiveFlags.RAW]
     const rawTarget = toRaw(target)
     const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
     !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
     // 劫持傳遞進來的callback,讓傳入callback的資料轉換成響應式資料
     return target.forEach((value: unknown, key: unknown) => {
       // 確保拿到的值是響應式的
       return callback.call(thisArg, wrap(value), wrap(key), observed)
     })
      }
    }

結尾

我是周小羊,一個前端萌新,寫文章是為了記錄自己日常工作遇到的問題和學習的內容,提升自己,如果您覺得本文對你有用的話,麻煩點個贊鼓勵一下喲~

相關文章