1. 程式人生 > 其它 >Vue3中的響應式物件Reactive原始碼分析

Vue3中的響應式物件Reactive原始碼分析

Vue3中的響應式物件Reactive原始碼分析

ReactiveEffect.js 中的 trackEffects函式 及 ReactiveEffect類 在Ref隨筆中已經介紹,在本文中不做贅述

本文中所有的原始碼均為 JS簡易版本,便於閱讀理解

Vue3原始碼分析之 Ref 與 ReactiveEffect

Reactive流程圖

  1. reactive(obj) 都做了些什麼?

    reactive函式其實只做了 過濾只讀物件 的功能,建立 Proxy 代理是通過呼叫 createReactiveObject 函式來進行的

    reactive 函式原始碼

    // WeakMap 可以有效避免記憶體洩漏問題,因為他的鍵是弱引用的
    // const reactiveMap = new WeakMap(): 已收集的依賴的快取列表(屬於:effect模組)
    // const { mutableHandlers } = require('./baseHandlers模組')
    
    function reactive(target) {
      // 如果這個物件被標記為只讀,那麼我們只需要將他返回即可(因為只讀不存在修改)
      if(target && target[ReactiveFlags.IS_READONLY]) {
        return target
      }
      return createReactiveObject(
        target,
        mutableHandlers,
        reactiveMap
      )
    }
    

    createReactiveObject 函式返回一個 響應式代理(Proxy), 針對 目標物件(target) 進行過濾和特殊處理

    createReactiveObject 函式原始碼

    /**
     * 建立 reactive 響應式代理
     * @param {*} target 代理物件
     * @param {*} baseHandlers Array Object
     * @param {*} proxyMap 依賴 快取集合
     * 下方特殊型別暫時不實現,不影響核心邏輯
     * collectionHandlers:主要是處理:Map、Set、WeakMap、WeakSet 型別
     * isReadonly: 是否只讀
     * @returns Proxy
     */
    function createReactiveObject(target, baseHandlers, proxyMap /* collectionHandlers, isReadonly */) {
      // 如果不是一個物件(這裡沒有在 reactive 中做處理,是由於 readonly 類似的api也需要呼叫該函式)
      if(!isObject(target)) {
        return target
      }
    
      // 如果已經收集,將快取列表中的內容返回即可
      const existingProxy = proxyMap.get(target)
      if(existingProxy) {
        return existingProxy
      }
    
      // 建立代理物件(目前僅處理 Array、Object)
      const proxy = new Proxy(target, baseHandlers) // collectionHandlers)
    
      // 收集已經代理的物件
      proxyMap.set(target, proxy)
      // 將響應式物件 Proxy 返回
      return proxy
    }
    
  2. baseHandlers 模組中是如何建立 get 和 set 等方法的?

    mutableHandlers 物件中包含 get, set, deleteProperty, has, ownKeys 幾種操作方法

    /**
     * 以下方法本文不進行實現:
     * deleteProperty: 刪除屬性時*
     * has:檢查目標物件是否存在此某個屬性(因為 寫入/刪除 物件屬性會受到影響,需要收集)
     * ownKeys:獲取keys陣列列表(因為如果 寫入/刪除 物件中的屬性,keys陣列長度會變化,需要收集)
    */
    

    get = createGetter()實現程式碼

    const get = createGetter()
    
    function createGetter(isReadonly =false) {
      return function get(target, key, receiver) {
        // console.log('get: ', key)
        // const targetIsArray = Array.isArray(target)
        
        // 原始碼中陣列的特殊處理,主要是針對陣列的一些原生方法進行處理(原始碼中對應的函式:createArrayInstrumentations())
        // 處理:'push', 'pop', 'shift', 'unshift', 'splice'(避免陣列長度被追蹤,某些情況可能造成無限迴圈)
        // 處理:'includes', 'indexOf', 'lastIndexOf'(對可能產生依賴作用的方法進行追蹤)
        // if(!isReadonly && targetIsArray && Object.prototype.hasOwnProperty(arrayInstrumentations, key)) {
        // }
    
        const res = Reflect.get(target, key, receiver)
        // 如果是隻讀的,那麼不需要進行收集,因為無法set,就不會被更改
        if(!isReadonly) {
          // 若當前存在 activeEffect(活躍的 effect),那麼需要對其進行收集
          track(target, key)
        }
        
        if(isObject(res)) {
          // 如果結果是一個 Object,比如:const obj = { a: 1, b: { a: '2-1' } },獲取 obj.b.a
          // 如果不將 b 進行響應式代理,那麼在讀取 a 的時候無法觸發 get 方法,因為 proxy 只會作用第一層物件
          // 原始碼中有一個 shallow 引數來判讀是否 執行巢狀物件的深度轉換
          return isReadonly ? '' : reactive(res)
        }
    
        return res
      }
    }
    

    createSetter函式核心邏輯其實就是 執行 Reflect.set 方法,觸發trigger

    set = createSetter()實現程式碼

    const set = createSetter()
    // shallow:淺監聽
    function createSetter(shallow = false) {
      return function set(target, key, value, receiver) {
        // 要在 trigger 函式執行之前更改值,否則拿不到最新的值
        const result =  Reflect.set(target, key, value, receiver)
    
        trigger(target, key)
        // trigger函式執行完成後,返回結果
        return result
      }
    }
    

    baseHandlers 模組會匯出一個 mutableHandlers 物件

    export const mutableHandlers = {
      get,
      set,
      deleteProperty,
      has,
      ownKeys
    }
    
  3. effect 模組的拓展

    effect模組針對 reactive,主要增加了 track(收集依賴)、trigger(觸發依賴) 兩個方法及 targetMap(快取 effec) 屬性

    effct.js

    effect模組原始碼在講解 Ref 的隨筆中已進行分析(Vue3原始碼分析之 Ref 與 ReactiveEffect

    let activeEffect // 記錄當前活躍的物件
    let shouldTrack = false // 標記是否追蹤
    
    const isTracking = () => activeEffect && shouldTrack // 工具函式
    
    // 新增:儲存已經收集的依賴(reactive)
    const targetMap  = new WeakMap()
    // ...... 其他
    

    track 原始碼

    // 依賴收集
    function track(target, key /** trackType */) {
      if(!isTracking()) {
        return
      }
    
      // 從快取列表中嘗試獲取
      let depsMap = targetMap.get(target)
      if(!depsMap) {
        // 如果沒有被收集過,那麼進行收集, 那麼建立一個 depsMap
        targetMap.set(target, (depsMap = new Map()))
      }
    
      // 嘗試從已收集的依賴中獲取 effects
      let dep = depsMap.get(key)
      if(!dep) {
        // 如果這個這個 key 不存在,則建立一個 dep
        depsMap.set(key, (dep = new Set()))
      }
    
      // 進行 effect 收集,dep.add(activeEffect)
      trackEffects(dep)
    }
    

    trigger 原始碼

    /**
    * type, oldValue, newValue, oldTarget:這幾個引數在原始碼中,主要是為了 dev 環境中的日誌提示作用
    */
    function trigger(target, key /* type, oldValue, newValue, oldTarget */) {
      const depsMap = targetMap.get(target)
      // depsMap可能為空
      if (!depsMap) {
        return
      }
     
      const deps = depsMap.get(key)
      // 將 set物件 deps 轉為陣列
      triggerEffects([...deps])
    }
    

    測試程式碼

    const testObj = reactive({ a: 1 })
    
    // 模擬一個 computed
    const testComputed = () => {
      // 建立一個 effect
      const testEffect = new ReactiveEffect(() => { 
        // 在回撥函式中列印 testObj.a
        return console.log('-- textComputed --', testObj.a) 
      })
    
      // 此時,targetMap.get(target) = dep<Set>[testEffect<ReactiveEffect>]
      // testEffect.deps = [dep<Set>[testEffect<ReactiveEffect>]]
      testEffect.run()
    
      console.log('testEffect.deps', testEffect.deps)
    }
    
    testComputed() 
    
    testObj.a += 1 // console -> -- textComputed -- 2
    

備註:手寫的 JS 簡易版本原始碼,方便閱讀理解很多邊緣情況並沒有完全考慮,問題請留言