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

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

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

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

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

    <script>
          'use strict'
          // set 和 deleteProperty 中需要返回布爾類型的值
          // 在嚴(yán)格模式下兵多,如果返回 false 的話會出現(xiàn) Type Error 的異常
          const target = {
            foo: 'xxx',
            bar: 'yyy'
          }
          // Reflect.getPrototypeOf()相當(dāng)于Object.getPrototypeOf()
          const proxy = new Proxy(target, {
            // receiver代表當(dāng)前的的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設(shè)置成功返回true 設(shè)置失敗返回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 對象中設(shè)置了 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

    當(dāng)傳遞了receiver時辫继,this指向Proxy對象

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

  • 創(chuàng)建攔截器對象handler,設(shè)置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)為響應(yīng)式對象
  3. set方法中實現(xiàn)
    • 獲取key屬性的值记焊,判斷新舊值是否相同逸月,相同時返回true
    • 不同時,先將target中的key對應(yīng)的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文件進(jìn)行測試:

<!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>
響應(yīng)式系統(tǒng)原理——收集依賴
image-20210415080133182.png

image-20210414082624298.png
  • 依賴收集過程中會創(chuàng)建3個集合瓢颅,分別是targetMap恩尾、depsMap和dep
  • targetMap作用是記錄目標(biāo)對象和一個字典(depsMap),使用WeakMap弱引用挽懦,當(dāng)目標(biāo)對象失去引用之后翰意,可以銷毀
  • targetMap的值是depsMap,depsMap的key是目標(biāo)對象的屬性名稱信柿,值是一個set集合dep
  • dep中存儲的是effect函數(shù)冀偶,因為可以多次調(diào)用一個effect,在effect中訪問同一個屬性渔嚷,這時該屬性會收集多次依賴进鸠,對應(yīng)多個effect函數(shù)
  • 通過這種結(jié)構(gòu),可以存儲目標(biāo)對象形病,目標(biāo)對象屬性客年,以及屬性對應(yīng)的effect函數(shù)
  • 一個屬性可能對應(yīng)多個函數(shù),當(dāng)觸發(fā)更新時窒朋,在這個結(jié)構(gòu)中根據(jù)目標(biāo)對象屬性找到effect函數(shù)然后執(zhí)行
  • 收集依賴的track函數(shù)內(nèi)部搀罢,首先根據(jù)當(dāng)前targetMap對象找到depsMap,如果沒找到要給當(dāng)前對象創(chuàng)建一個depsMap侥猩,并添加到targetMap中榔至,如果找到了再根據(jù)當(dāng)前使用的屬性在depsMap找到對應(yīng)的dep,dep中存儲的是effect函數(shù)欺劳,如果沒有找到時唧取,為當(dāng)前屬性創(chuàng)建對應(yīng)的dep集合,并且存儲到depsMap中划提,如果找到當(dāng)前屬性對應(yīng)的dep集合枫弟,就把當(dāng)前的effect函數(shù)存儲到集合中

effect方法實現(xiàn)

實現(xiàn)思路:

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

代碼實現(xiàn):

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

track方法實現(xiàn)

實現(xiàn)思路:

  1. track接收兩個參數(shù),目標(biāo)對象target和需要跟蹤的屬性key
  2. 內(nèi)部需要將target存儲到targetMap中,targetMap定義在外面韩容,除了track使用外款违,trigger函數(shù)也要使用
  3. activeEffect不存在直接返回,否則需要在targetMap中根據(jù)當(dāng)前target找depsMap
  4. 判斷是否找到depsMap群凶,因為target可能還沒有收集依賴
  5. 未找到插爹,為當(dāng)前target創(chuàng)建depsMap去存儲對應(yīng)的鍵和dep對象,并添加到targetMap中
  6. 根據(jù)屬性查找對應(yīng)的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找對應(yīng)的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文件進(jìn)行測試

<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
響應(yīng)式系統(tǒng)原理——ref

ref vs reactive

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

實現(xiàn)原理:

  1. 判斷 raw 是否是ref 創(chuàng)建的對象蛾坯,如果是的話直接返回
  2. 判斷 raw是否是對象光酣,如果是對象調(diào)用reactive創(chuàng)建響應(yīng)式對象,否則返回原始值
  3. 創(chuàng)建ref對象并返回脉课,標(biāo)識是否是ref對象救军,這個對象只有value屬性,并且這個value屬性具有set和get
  4. get中調(diào)用track收集依賴倘零,收集依賴的對象是剛創(chuàng)建的r對象唱遭,屬性是value,也就是當(dāng)訪問對象中的值呈驶,返回的是內(nèi)部的變量value
  5. set中判斷新舊值是否相等拷泽,不相等時將新值存儲到raw中,并調(diào)用convert處理raw袖瞻,最終把結(jié)果存儲到value中司致,如果給value重新賦值為一個對象依然是響應(yīng)式的,當(dāng)raw是對象時聋迎,convert里調(diào)用reactive轉(zhuǎn)換為響應(yīng)式對象
  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文件進(jìn)行測試:

<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é)果和上面的相同

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

實現(xiàn)思路:

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

代碼實現(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進(jìn)行測試:

<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é)果和上面的相同

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

實現(xiàn)原理:

  1. 接收一個有返回值的函數(shù)作為參數(shù)废膘,函數(shù)的返回值就是計算屬性的值
  2. 監(jiān)聽這個函數(shù)內(nèi)部的響應(yīng)式數(shù)據(jù)變化,最后將函數(shù)執(zhí)行結(jié)果返回
  3. computed內(nèi)部會通過effect監(jiān)聽getter內(nèi)部的響應(yīng)式數(shù)據(jù)變化慕蔚,因為在effect中執(zhí)行g(shù)etter訪問響應(yīng)式數(shù)據(jù)的getter會去收集依賴丐黄,當(dāng)數(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文件進(jìn)行測試:

<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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末孔飒,一起剝皮案震驚了整個濱河市灌闺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坏瞄,老刑警劉巖桂对,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鸠匀,居然都是意外死亡蕉斜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門狮崩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛛勉,“玉大人,你說我怎么就攤上這事睦柴》塘瑁” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵坦敌,是天一觀的道長侣诵。 經(jīng)常有香客問我痢法,道長,這世上最難降的妖魔是什么杜顺? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任财搁,我火速辦了婚禮,結(jié)果婚禮上躬络,老公的妹妹穿的比我還像新娘尖奔。我一直安慰自己,他們只是感情好穷当,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布提茁。 她就那樣靜靜地躺著,像睡著了一般馁菜。 火紅的嫁衣襯著肌膚如雪茴扁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天汪疮,我揣著相機與錄音峭火,去河邊找鬼。 笑死智嚷,一個胖子當(dāng)著我的面吹牛卖丸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纤勒,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼坯苹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了摇天?” 一聲冷哼從身側(cè)響起粹湃,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泉坐,沒想到半個月后为鳄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡腕让,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年孤钦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纯丸。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡偏形,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出觉鼻,到底是詐尸還是另有隱情俊扭,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布坠陈,位于F島的核電站萨惑,受9級特大地震影響捐康,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜庸蔼,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一解总、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧姐仅,春花似錦花枫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至壤追,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間供屉,已是汗流浹背行冰。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伶丐,地道東北人悼做。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像哗魂,于是被迫代替她去往敵國和親肛走。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

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