1. 程式人生 > 其它 >Vue3原始碼分析之 Ref 與 ReactiveEffect

Vue3原始碼分析之 Ref 與 ReactiveEffect

Vue3中的響應式實現原理

完整 js版本簡易原始碼 在最底部

ref 與 reactive 是Vue3中的兩個定義響應式物件的API,其中reactive是通過 Proxy 來實現的,它返回物件的響應式副本,而Ref則是返回一個可變的ref物件,只有一個 .value屬性指向他內部的值,本文則重點來分析一下 Ref 的實現原理

ref:接受一個內部值並返回一個響應式且可變的 ref 物件。ref 物件僅有一個 .value property,指向該內部值。

  1. Ref依賴收集

    首先我們需要了解 ReactiveEffect 類,建立這個類需要傳入一個 副作用函式 和 scheduler,在這個類中有 active、deps 兩個重要的屬性:

    active:是否為啟用狀態,預設為:true

    deps:所有依賴這個 effect 的響應式物件

    ReactiveEffect 簡易 js 版本原始碼

    // 記錄當前活躍的物件
    let activeEffect
    // 標記是否追蹤
    let shouldTrack = false
    
    class ReactiveEffect{
      active = true // 是否為啟用狀態
      deps = [] // 所有依賴這個 effect 的響應式物件
      onStop = null // function
      constructor(fn, scheduler) {
        this.fn = fn // 回撥函式,如: computed(/* fn */() => { return testRef.value ++ })
        // function型別,不為空那麼在 TriggerRefValue 函式中會執行 effect.scheduler,否則會執行 effect.run
        this.scheduler = scheduler
      }
    
      run() {
        // 如果這個 effect 不需要被響應式物件收集
        if(!this.active) {
          return this.fn()
        }
    
        // 原始碼這裡用了兩個工具函式:pauseTracking 和 enableTracking 來改變 shouldTrack的狀態
        shouldTrack = true
        activeEffect = this
        
        // 在設定完 activeEffect 後執行,在方法中能夠對當前活躍的 activeEffect 進行依賴收集
        const result = this.fn()
        
        shouldTrack = false
        // 執行完副作用函式後要清空當前活躍的 effect
        activeEffect = undefined
    
        return result
      }
    
      // 暫停追蹤
      stop() {
        if (this.active) {
          // 找到所有依賴這個 effect 的響應式物件
          // 從這些響應式物件裡面把 effect 給刪除掉
          cleanupEffect(this)
          // 執行onStop回撥函式
          if (this.onStop) {
            this.onStop();
          }
          this.active = false;
        }
      }
    }
    

    瞭解了 ReactiveEffect 類,再來分析 Ref

    ref 簡易JS版本原始碼

    class RefImpl{
      // Set格式,儲存 effect
      dep
      // 儲存原始值
      _rawValue
      // 標記這是一個 Ref 物件,isRef API就可以直接通過判斷這個內部屬性即可
      __v_isRef = true
    
      // _shallow:這個值是為了實現 shallowRef API 而存在
      // 目前這裡沒有使用到
      constructor(value, _shallow) {
        this._value = value
        // 儲存原始值,用來與新值做對比
        this._rawValue = _shallow ? value : toRaw(value)
        // _value 內部值
        // toReactive => isObject(value) ? reactive(value) : value
        // 對 value 進行包裝
        this._value = _shallow ? value : toReactive(value)
      }
    
      get value() {
        // 在獲取這個 Ref 內部值時 進行依賴收集
        // trackRefValue() 依賴收集
        trackRefValue(this)
        return this._value
      }
    }
    

    trackRefValue方法實現

    /**
    * activeEffect 當前活躍的 effect 物件
    * shouldTrack 是否允許追蹤
    * const isTracking = () => activeEffect && shouldTrack
    */
    
    function trackRefValue(ref) {
      // 若沒有活躍的 effect 物件或 不需要進行追蹤
      if(!isTracking()) {
        return
      }
      // 如果這個 Ref 物件還沒有對 dep 進行初始化(Ref 中 dep 屬性預設為 undefined)
      if(!ref.dep) {
        ref.dep = new Set()
      }
      
      trackEffects(ref.dep)
    }
    
    function trackEffects(dep) {
      // 若改 effect 物件已經收集,跳過
      if(!dep.has(activeEffect)) {
        // 將 effect 物件新增到 Ref 物件的 dep 中
        dep.add(activeEffect)
        // 將這個Ref的dep存放到這個 effect 的 deps 中
        // 目的是為了在停止追蹤時,從 響應式物件將 effect 移除掉
        activeEffect.deps.push(dep)
      }
    }
    

    寫個 testComputed 函式來回顧 和 測試一下上面的流程

    const testRef = new RefImpl(1)
    
    const testComputed = (fn) => {
      // 建立一個 effect 物件
      const testEffect = new ReactiveEffect(fn)
      // 預設執行 effect.run() 函式(設定 activeEffect=this -> 執行 fn(依賴收集) -> 清空 activeEffect=undefined)
      return testEffect.run()
      // 此時,依賴情況:
      // testRef.dep = [ testEffect:effect ]
      // testEffect.deps = [ testRef.dep ]
    }
    
    const fn = () => {  console.log('textComputed', testRef.value) }
    testComputed(fn)
    
  2. Ref 觸發更新

    在 RefImpl 類中增加 set value 方法

    class RefImpl{
        // ......
        // 增加 set value () 方法
        set value(newVal) {
            newVal = this._shallow ? newVal : toRaw(newVal)
            // 對比 新老值 是否相等,這裡不能簡單的使用 == 或 ===
            // Object.is() 方法判斷兩個值是否為同一個值。
            if (!Object.is(newVal, this._rawValue)) {
              this._rawValue = newVal
              this._value = this._shallow ? newVal : toReactive(newVal)
              // 在內部值被改變的時候會觸發依賴更新
              triggerRefValue(this) // ,newValue) 在dev環境下使用了 newValue,這裡忽略
            }
          }
        // ......
    }
    

    triggerRefValue 方法實現

    function triggerRefValue(ref) {
      triggerEffects(ref.dep);
    }
    
    function triggerEffects(dep) {
      // 執行 Ref.dep 中收集的所有 effect
      for (const effect of dep) {
        // 這裡做個判斷,執行的 effect 不是 當前活躍的 effect
        if(effect !== activeEffect) {
          if(effect.scheduler) {
            // effect.scheduler 文章前面有講過
            /** Vue3中模板更新,就是通過建立了一個 scheduler,然後推入 微任務佇列 中去執行的  
               const effect = new ReactiveEffect(componentUpdateFn,() => queueJob(instance.update))
               const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
             */
            effect.scheduler()
          } else {
            effect.run()
          }
        }
      }
    }
    

    修改 testComputed 函式進行測試

    const testRef = new RefImpl(1)
    
    const testComputedWatch = (fn) => {
      // 建立一個 effect 物件
      const testEffect = new ReactiveEffect(fn)
      // 預設執行 effect.run() 函式(設定 activeEffect=this -> 執行 fn(依賴收集) -> 清空 activeEffect=undefined)
      return testEffect.run()
      // 此時,依賴情況:
      // testRef.dep = [ testEffect:effect ]
      // testEffect.deps = [ testRef.dep ]
    }
    
    const fn = () => {  console.log('textComputed', testRef.value) }
    testComputed(fn)
    
    testRef.value ++ // -> 'textComputed', 2
    testRef.value ++ // -> 'textComputed', 3
    
    

    原始碼:

    effect.js

    // 記錄當前活躍的物件
    let activeEffect
    // 標記是否追蹤
    let shouldTrack = false
    // 儲存已經收集的依賴
    // const targetMap  = new WeakMap()
    
    const isTracking = () => activeEffect && shouldTrack
    
    function trackEffects(dep) {
      if(!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
      }
    }
    
    function triggerEffects(dep) {
      for (const effect of dep) {
        // 這裡做個判斷,執行的 effect 不是 當前活躍的 effect
        if(effect !== activeEffect) {
          if(effect.scheduler) {
            effect.scheduler()
          } else {
            effect.run()
          }
        }
      }
    }
    
    class ReactiveEffect{
      active = true
      deps = []
      onStop = null
    
      constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
      }
    
      run() {
        if(!this.active) {
          return this.fn()
        }
    
        // 原始碼這裡用了兩個工具函式:pauseTracking 和 enableTracking 來改變 shouldTrack的狀態
        shouldTrack = true
        activeEffect = this
        
        // 在設定完 activeEffect 後執行,在方法中能夠對當前活躍的 activeEffect 進行依賴收集
        const result = this.fn()
        
        shouldTrack = false
        // 執行完副作用函式後要清空當前活躍的 effect
        activeEffect = undefined
    
        return result
      }
    
      // 暫停追蹤
      stop() {
        if (this.active) {
          // 找到所有依賴這個 effect 的響應式物件
          // 從這些響應式物件裡面把 effect 給刪除掉
          cleanupEffect(this)
          // 執行onStop回撥函式
          if (this.onStop) {
            this.onStop();
          }
          this.active = false;
        }
      }
    }
    function cleanupEffect(effect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }
    module.exports = {
      ReactiveEffect,
      isTracking,
      trackEffects,
      triggerEffects
    }
    

    ref.js

    // const { toReactive, toRaw } = require('./reactive.js')
    // start reactive.js 模組
    const isObject = (val) => val !== null && typeof val === 'object'
    
    const toReactive = val =>
      isObject(val) ? reactive(val) : val
    
    function toRaw(observed) {
      // __v_raw 是一個標誌位,表示這個是一個 reactive 
      const raw = observed && observed['__v_raw']
      return raw ? toRaw(raw) : observed
    }
    // end reactive.js
    
    const { ReactiveEffect, isTracking, trackEffects, triggerEffects } = require('./effect.js')
    
    function trackRefValue(ref) {
      if(!isTracking()) {
        return
      }
    
      if(!ref.dep) {
        ref.dep = new Set()
      }
    
      trackEffects(ref.dep)
    }
    
    function triggerRefValue(ref) {
      triggerEffects(ref.dep);
    }
    
    class RefImpl{
      dep
      _rawValue
      __v_isRef = true
      constructor(value, _shallow) {
        this._value = value
        // 儲存原始值,用來與新值做對比
        this._rawValue = _shallow ? value : toRaw(value)
        this._value = _shallow ? value : toReactive(value)
      }
    
      get value() {
        // 收集依賴
        trackRefValue(this)
        return this._value
      }
    
      set value(newVal) {
        newVal = this._shallow ? newVal : toRaw(newVal)
        // Object.is() 方法判斷兩個值是否為同一個值。
        if (!Object.is(newVal, this._rawValue)) {
          this._rawValue = newVal
          this._value = this._shallow ? newVal : toReactive(newVal)
          triggerRefValue(this) // ,newValue) 在dev環境下使用了 newValue,這裡忽略
        }
      }
    }
    
    function isRef(r) {
      return Boolean(r && r.__v_isRef === true)
    }
    function createRef(rawValue, _shallow) {
      if (isRef(rawValue)) {
        return rawValue
      }
      return new RefImpl(rawValue, _shallow)
    }
    
    function ref(val) {
      return createRef(val, false)
    }
    
    module.exports = {
      isRef,
      ref
    }
    

    測試程式碼

    /** ** 測試程式碼 ***/
    const testRef = ref(1)
    
    const textComputed = () => {
      const testEffect = new ReactiveEffect(() => { 
        console.log('textComputed', testRef.value) 
      })
    
      testEffect.run()
    
      console.log('testEffect.deps', testEffect.deps)
    }
    const textComputed1 = () => {
      const fn = () => {  console.log('textComputed', testRef.value) }
      const testEffect = new ReactiveEffect(fn)
    
      testEffect.run()
    }
    
    textComputed1()
    textComputed()
    console.log('testRef', testRef)
    // testRef.value ++