Vue.js3.0 響應式系統(tǒng)原理

Vue.js響應式原理回顧
  • Proxy對象實現(xiàn)屬性監(jiān)聽
  • 多層屬性嵌套,在訪問屬性過程中處理下一級屬性
  • 默認監(jiān)聽動態(tài)添加的屬性
  • 默認監(jiān)聽屬性的刪除操作
  • 默認監(jiān)聽數(shù)組索引和 length屬性
  • 可以作為單獨的模塊使用
核心方法
  • reactive/ref/toRefs/computed
  • effect watch/watchEffect是vue3 runtime.core中實現(xiàn)的,內(nèi)部使用effect底層函數(shù)
  • track 收集依賴
  • trigger 觸發(fā)更新
響應式系統(tǒng)原理——Proxy

ProxyReflect是ES6 為了操作對象而提供的新 API

proxy中有兩個需要注意的地方:

  • set 和 deleteProperty 中需要返回布爾類型的值

    <script>
          'use strict'
          // set 和 deleteProperty 中需要返回布爾類型的值
          // 在嚴格模式下,如果返回 false 的話會出現(xiàn) Type Error 的異常
          const target = {
            foo: 'xxx',
            bar: 'yyy'
          }
          // Reflect.getPrototypeOf()相當于Object.getPrototypeOf()
          const proxy = new Proxy(target, {
            // receiver代表當前的的Proxy對象或者繼承Proxy的對象
            get (target, key, receiver) {
              // return target[key]
              // Reflect反射,代碼運行期間獲取對象中的成員
              return Reflect.get(target, key, receiver)
            },
            set (target, key, value, receiver) {
              // target[key] = value
              // Reflect.set設置成功返回true 設置失敗返回false
              return Reflect.set(target, key, value, receiver)
            },
            deleteProperty (target, key) {
              // delete target[key]
              return Reflect.deleteProperty(target, key)
            }
          })
    
          proxy.foo = 'zzz'
          // delete proxy.foo
    </script>
    

    如果set和deleteProperty返回false時,頁面會報錯

    image-20210414080553080.png
  • Proxy 和 Reflect 中使用的 receiver指向

    // Proxy 中 receiver:Proxy 或者繼承 Proxy 的對象
    // Reflect 中 receiver:如果 target 對象中設置了 getter,getter 中的 this 指向 receiver
    
    const obj = {
        get foo() {
            console.log(this)
            return this.bar
        },
    }
    
    const proxy = new Proxy(obj, {
        get(target, key, receiver) {
            if (key === 'bar') {
                return 'value - bar'
            }
            return Reflect.get(target, key, receiver)
        },
    })
    console.log(proxy.foo)
    

    不傳遞receiver時,可以看到this返回的是obj對象,proxy.foo返回undefined

    image-20210414080743227.png

    當傳遞了receiver時,this指向Proxy對象

    image-20210414080825068.png
響應式系統(tǒng)原理——reactive
  • 接收一個參數(shù),判斷這參數(shù)是否是對象,不是直接返回,只能轉(zhuǎn)換對象為響應式對象

  • 創(chuàng)建攔截器對象handler,設置get/set/deleteProperty

  • 返回Proxy 對象

    // reactivily/index.js
    const isObject = (val) => val !== null && typeof val === 'object'
    export function reactive(target) {
      if (!isObject(target)) return
    
      const handler = {
        get(target, key, receiver) {
          console.log('get', key, target)
        },
        set(target, key, value, receiver) {
          console.log('set', key, value)
          return value
        },
        deleteProperty(target, key) {
          console.log('delete', key)
          return target
        },
      }
    
      return new Proxy(target, handler)
    }
    

    測試set和delete,結(jié)果如下

    image-20210414082410979.png

reactive實現(xiàn)思路:

  1. 定義handler對象,用于Proxy的第二個參數(shù)(攔截器對象)
  2. get方法實現(xiàn)
    • 收集依賴
    • 返回target中對于key的value
    • 如果value為對象,需要再次轉(zhuǎn)為響應式對象
  3. set方法中實現(xiàn)
    • 獲取key屬性的值,判斷新舊值是否相同,相同時返回true
    • 不同時,先將target中的key對應的value修改為新值
    • 最后觸發(fā)更新
  4. deleteProperty方法實現(xiàn)
    • 首先判斷target本身是否存在key
    • 刪除target中的key,并返回成功或失敗
    • 刪除成功,觸發(fā)更新

代碼示例:

const isObject = (val) => val !== null && typeof val === 'object'
const convert = (val) => (isObject(val) ? reactive(val) : val)
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

export function reactive(target) {
  if (!isObject(target)) return

  const handler = {
    get(target, key, receiver) {
      // 收集依賴
      const value = Reflect.get(target, key, receiver)
      return convert(value)
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        let result = Reflect.set(target, key, value, receiver)
        // 觸發(fā)更新
      }
      return result
    },
    deleteProperty(target, key) {
      const hasKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hasKey && result) {
        // 觸發(fā)更新
      }
      return result
    },
  }

  return new Proxy(target, handler)
}

測試,創(chuàng)建html文件進行測試:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { reactive } from './reactivity/index.js'
    const obj = reactive({
      name: 'zs',
      age: 18
    })
    obj.name = 'lisi'
    delete obj.age
    console.log(obj)
  </script>
</body>
</html>
響應式系統(tǒng)原理——收集依賴
image-20210415080133182.png

image-20210414082624298.png
  • 依賴收集過程中會創(chuàng)建3個集合,分別是targetMap、depsMap和dep
  • targetMap作用是記錄目標對象和一個字典(depsMap),使用WeakMap弱引用,當目標對象失去引用之后,可以銷毀
  • targetMap的值是depsMap,depsMap的key是目標對象的屬性名稱,值是一個set集合dep
  • dep中存儲的是effect函數(shù),因為可以多次調(diào)用一個effect,在effect中訪問同一個屬性,這時該屬性會收集多次依賴,對應多個effect函數(shù)
  • 通過這種結(jié)構(gòu),可以存儲目標對象,目標對象屬性,以及屬性對應的effect函數(shù)
  • 一個屬性可能對應多個函數(shù),當觸發(fā)更新時,在這個結(jié)構(gòu)中根據(jù)目標對象屬性找到effect函數(shù)然后執(zhí)行
  • 收集依賴的track函數(shù)內(nèi)部,首先根據(jù)當前targetMap對象找到depsMap,如果沒找到要給當前對象創(chuàng)建一個depsMap,并添加到targetMap中,如果找到了再根據(jù)當前使用的屬性在depsMap找到對應的dep,dep中存儲的是effect函數(shù),如果沒有找到時,為當前屬性創(chuàng)建對應的dep集合,并且存儲到depsMap中,如果找到當前屬性對應的dep集合,就把當前的effect函數(shù)存儲到集合中

effect方法實現(xiàn)

實現(xiàn)思路:

  1. effect接收函數(shù)作為參數(shù)
  2. 執(zhí)行函數(shù)并返回響應式對象去收集依賴,收集依賴過程中將callback存儲起來,需要在后面的track函數(shù)中能夠訪問到這里的callback
  3. 依賴收集完畢設置activeEffect為null

代碼實現(xiàn):

let activeEffect = null
export function effect (callback) {
  activeEffect = callback
  callback() // 訪問響應式對象屬性,去收集依賴
  activeEffect = null
}

track方法實現(xiàn)

實現(xiàn)思路:

  1. track接收兩個參數(shù),目標對象target和需要跟蹤的屬性key
  2. 內(nèi)部需要將target存儲到targetMap中,targetMap定義在外面,除了track使用外,trigger函數(shù)也要使用
  3. activeEffect不存在直接返回,否則需要在targetMap中根據(jù)當前target找depsMap
  4. 判斷是否找到depsMap,因為target可能還沒有收集依賴
  5. 未找到,為當前target創(chuàng)建depsMap去存儲對應的鍵和dep對象,并添加到targetMap中
  6. 根據(jù)屬性查找對應的dep對象,dep是個集合,存儲effect函數(shù)
  7. 判斷是否存在,未找到時創(chuàng)建新的dep集合并添加到depsMap中
  8. 將effect函數(shù)添加到dep集合中
  9. 在收集依賴的get中調(diào)用這個函數(shù)

代碼實現(xiàn):

let targetMap = new WeakMap()
export function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

此時,整個依賴收集過程已經(jīng)完成

trigger方法實現(xiàn)

依賴收集完成后需要觸發(fā)更新

實現(xiàn)思路:

  1. 參數(shù)target和key
  2. 根據(jù)target在targetMap中找到depsMap
  3. 未找到時,直接返回
  4. 再根據(jù)key找對應的dep集合,effect函數(shù)
  5. 如果dep有值,遍歷dep集合執(zhí)行每一個effect函數(shù)
  6. 在set和deleteProperty中觸發(fā)更新

代碼實現(xiàn):

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach((effect) => {
      effect()
    })
  }
}

依賴收集和觸發(fā)更新代碼完成,創(chuàng)建html文件進行測試

<body>
  <script type="module">
    import { reactive, effect } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = 0 
    effect(() => {
      total = product.price * product.count
    })
    console.log(total)

    product.price = 4000
    console.log(total)

    product.count = 1
    console.log(total)

  </script>
</body>

打開瀏覽器控制臺,可以看到輸出結(jié)果如下

image-20210416084313137.png
響應式系統(tǒng)原理——ref

ref vs reactive

  • ref可以把基本數(shù)據(jù)類型數(shù)據(jù),轉(zhuǎn)成響應式對象
  • ref返回的對象,重新賦值成對象也是響應式的
  • reactive返回的對象,重新賦值丟失響應式
  • reactive返回的對象不可以解構(gòu)

實現(xiàn)原理:

  1. 判斷 raw 是否是ref 創(chuàng)建的對象,如果是的話直接返回
  2. 判斷 raw是否是對象,如果是對象調(diào)用reactive創(chuàng)建響應式對象,否則返回原始值
  3. 創(chuàng)建ref對象并返回,標識是否是ref對象,這個對象只有value屬性,并且這個value屬性具有set和get
  4. get中調(diào)用track收集依賴,收集依賴的對象是剛創(chuàng)建的r對象,屬性是value,也就是當訪問對象中的值,返回的是內(nèi)部的變量value
  5. set中判斷新舊值是否相等,不相等時將新值存儲到raw中,并調(diào)用convert處理raw,最終把結(jié)果存儲到value中,如果給value重新賦值為一個對象依然是響應式的,當raw是對象時,convert里調(diào)用reactive轉(zhuǎn)換為響應式對象
  6. 最后觸發(fā)更新

代碼實現(xiàn):

export function ref(raw) {
  // 判斷 raw 是否是ref 創(chuàng)建的對象,如果是的話直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  let value = convert(raw)
  const r = {
    __v_isRef: true,
    get value() {
      track(r, 'value')
      return value
    },
    set value(newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    },
  }
  return r
}

創(chuàng)建html文件進行測試:

<body>
  <script type="module">
    import { reactive, effect, ref } from './reactivity/index.js'

    const price = ref(5000)
    const count = ref(3)
   
    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)

  </script>
</body>

打開控制臺可以看到輸出結(jié)果和上面的相同

響應式系統(tǒng)原理——toRefs

實現(xiàn)思路:

  1. 接收參數(shù)proxy,判斷參數(shù)是否為reactive創(chuàng)建的對象,如果不是發(fā)出警告
  2. 判斷傳入?yún)?shù),如果是數(shù)組創(chuàng)建長度是length的數(shù)組,否則返回空對象,因為傳入的proxy可能是響應式數(shù)組或響應式對象
  3. 接著遍歷proxy對象的所有屬性,如果是數(shù)組遍歷索引,將每一個屬性都轉(zhuǎn)換為類似ref返回的對象
  4. 創(chuàng)建toProxyRef函數(shù),接收proxy和key,創(chuàng)建對象并最終返回對象(類似ref返回的對象)
  5. 創(chuàng)建標識屬性__v_isRef,這里的get中不需要收集依賴,因為這里訪問的是響應式對象,當訪問屬性時,內(nèi)部的getter回去收集依賴,set不需要觸發(fā)更新,調(diào)用代理對象內(nèi)部的set觸發(fā)更新
  6. 調(diào)用toProxyRef,將所有屬性轉(zhuǎn)換并存儲到ret中
  7. toRefs將reactive返回的對象的所有屬性都轉(zhuǎn)換成一個對象,所以當對響應式對象進行解構(gòu)的時候,解構(gòu)出的每一個屬性都是對象,而對象是引用傳遞,所以解構(gòu)的屬性依然是響應式的

代碼實現(xiàn):

export function toRefs(proxy) {
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef(proxy, key) {
  const r = {
    __v_isRef: true,
    get value() {
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    },
  }
  return r
}

創(chuàng)建html進行測試:

<body>
  <script type="module">
    import { reactive, effect, toRefs } from './reactivity/index.js'

    function useProduct () {
      const product = reactive({
        name: 'iPhone',
        price: 5000,
        count: 3
      })
      
      return toRefs(product)
    }

    const { price, count } = useProduct()


    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)

  </script>
</body>

打開控制臺可以看到輸出結(jié)果和上面的相同

響應式系統(tǒng)原理——computed

實現(xiàn)原理:

  1. 接收一個有返回值的函數(shù)作為參數(shù),函數(shù)的返回值就是計算屬性的值
  2. 監(jiān)聽這個函數(shù)內(nèi)部的響應式數(shù)據(jù)變化,最后將函數(shù)執(zhí)行結(jié)果返回
  3. computed內(nèi)部會通過effect監(jiān)聽getter內(nèi)部的響應式數(shù)據(jù)變化,因為在effect中執(zhí)行getter訪問響應式數(shù)據(jù)的getter會去收集依賴,當數(shù)據(jù)變化后,回去重新執(zhí)行effect函數(shù)將getter結(jié)果在存儲到result中

代碼實現(xiàn):

export function computed(getter) {
  const result = ref()

  effect(() => (result.value = getter()))

  return result
}

創(chuàng)建html文件進行測試:

<body>
  <script type="module">
    import { reactive, effect, computed } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = computed(() => {
      return product.price * product.count
    })
   
    console.log(total.value)

    product.price = 4000
    console.log(total.value)

    product.count = 1
    console.log(total.value)

  </script>
</body>

打開控制臺可以看到輸出結(jié)果和上面的相同

github地址

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 前言 關于響應式原理想必大家都很清楚了,下面我將會根據(jù)響應式API來具體講解Vue3.0中的實現(xiàn)原理, 另外我只會...
    西施老師閱讀 2,380評論 2 0
  • 響應式原理 響應式是 Vue.js 組件化更新渲染的一個核心機制 Vue2.x響應式實現(xiàn) Object.defin...
    啦啦啦嘍啰閱讀 1,159評論 0 2
  • vue3.0監(jiān)測機制有了很大的改善,彌補了vue2.0的一些局限: 對屬性的添加、刪除動作的監(jiān)測; 對數(shù)組基于下標...
    秘果_li閱讀 8,698評論 1 4
  • VUE2.0 的響應式原理 本篇文章篇幅較長,已經(jīng)對2.0響應式原理熟悉的可直接跳過此部分,各取所需,共同交流 在...
    ElvisYang1993閱讀 1,226評論 0 3
  • 數(shù)據(jù)驅(qū)動 在我們學習Vue.js的過程中,我們經(jīng)??吹饺齻€概念 數(shù)據(jù)驅(qū)動 數(shù)據(jù)響應式 雙向數(shù)據(jù)綁定 核心原理分析 ...
    amanohina閱讀 544評論 0 4

友情鏈接更多精彩內(nèi)容