1. 程式人生 > 其它 >你想知道 vue3 中響應式的成長曆程嗎?

你想知道 vue3 中響應式的成長曆程嗎?

技術標籤:Vuevuevue.js

vue 的響應性

當我第一次使用 vue 進行專案開發時,響應式資料渲染 是讓我感到最驚奇的一個功能,我們來看下面這段程式碼:

<body>
  <div id="app">
    <div>
      修改商品的數量: <input type="number" v-model="product.quantity">
    </div>
    <div>
      修改商品的價格: <input type=
"number"
v-model="product.price">
</div> <p> 總價格:{{ total }} </p> </div> </body> <script src="https://unpkg.com/[email protected]"></script> <script> const component = { data() { return { // 定義一個商品物件,包含價格和數量
product: { price: 10, quantity: 2 }, } }, computed: { // 計算總價格 total() { return this.product.price * this.product.quantity } } } const app = Vue.createApp(component) app.mount('#app')
</script>

這是一段標準的 vue3 的程式碼,當你在輸入框中輸入內容的時候,totla

永遠會被重新計算,就像下面這樣。

這樣的功能我們稱它為 響應式

響應式的資料渲染 是現在前端非常重要的機制。但是這種機制它究竟是 怎麼被一步一步的構建出來的呢? 這就是這篇部落格想要說的內容。

如果你想要了解 vue3的 響應系統 及 構建歷程 ,那麼你就應該看下去。

js 的程式性

想要了解 響應性,那麼你需要先了解 程式性。我們來看下面這段普通的 js程式碼:

  // 定義一個商品物件,包含價格和數量
  let product = {
    price: 10,
    quantity: 2
  }
  // 總價格
  let total = product.price * product.quantity;
  // 第一次列印
  console.log(`總價格:${total}`); 
  // 修改了商品的數量
  product.quantity = 5;
  // 第二次列印
  console.log(`總價格:${total}`); 

想一下,上面的程式碼第一次應該列印什麼內容?第二次應該列印什麼內容?

恭喜你!答對了,因為它們只是普通的 js 程式碼,所以兩次的列印結果應該都是:

總價格:20

但是你有沒有想過,當我們去進行第二次列印的時候,你真的希望它還是 20 嗎?

你有沒有過冒出來這麼一個想法:商品數量發生變化了,如果總價格能夠自己跟隨變化,那就太好了! 這是 人性,從 的角度考慮,確實應該這個樣子。但是 程式 並不會如此 ”智慧“。 那麼怎麼能夠讓程式變得更加 “聰明” 呢?

這個 讓程式變 ”聰明“ 的過程,就是響應式構建的過程

你希望:當資料變化時,重新執行運算

你為了讓你的程式變得更加 “聰明” , 所以你開始想:”如果資料變化了,重新執行運算就好了“。

想到就去做,為了達到這個目的,你開始對運算函式進行了封裝。

你定義了一個匿名函式 effect,用來計算 商品總價格。並且 當列印總價格前,讓 effect 執行 。所以你得到了下面的程式碼:

  // 定義一個商品物件,包含價格和數量
  let product = {
    price: 10,
    quantity: 2
  }
  // 總價格
  let total = 0;
  // 計算總價格的匿名函式
  let effect = () => {
    total = product.price * product.quantity;
  };
  // 第一次列印
  effect();
  console.log(`總價格:${total}`); // 總價格:20
  // 修改了商品的數量
  product.quantity = 5;
  // 第二次列印
  effect();
  console.log(`總價格:${total}`); // 總價格:50

在這樣的一個程式碼中,你得到了一個想要的結果:資料變化了,運算也重新執行了

但是,你很快發現了一個新的問題:這樣的程式碼只能維護單一的總價格運算 。 你希望讓它可以支援更多的運算,那怎麼辦呢?

你希望:當資料變化時,重新執行多個運算

你的程式碼只支援單一運算,你希望讓它支援更多。

為了達到這個目的,你開始對程式碼進行了簡單的封裝,你做了以下三件事情:

  1. 建立 Set 陣列(點選瞭解 Set) ,用來存放多個運算函式
  2. 建立 track 函式,用來向 Set 中存放運算函式
  3. 建立 trigger 函式,用來執行所有的運算函式

這樣,你得到了下面的程式碼,並且把這樣的一套程式碼稱之為 響應式

  // -------------建立響應式-------------
  // set 陣列,用作儲存所有的運算函式
  let deps = new Set();
  // 儲存運算函式
  function track() {
    deps.add(effect);
  }
  // 觸發器,執行所有的運算函式
  function trigger() {
    deps.forEach((effect) => effect());
  }

  // -------------建立資料來源-------------
  // 宣告商品物件,為資料來源
  let product = {
    price: 10,
    quantity: 2
  };
  // 宣告總價格
  let total = 0;
  // 運算總價格的匿名函式
  let effect = () => {
    total = product.price * product.quantity;
  };

  // -------------執行響應式-------------
  // 儲存運算函式
  track();
  // 運算 總價格
  effect();
  console.log(`總價格:${total}`); // 總價格:20
	
  // 修改資料來源
  product.quantity = 5;
  // 資料來源被修改,執行觸發器,重新運算所有的 total
  trigger();
  console.log(`總價格:${total}`); // 總價格:50

你對你的 創造 非常驕傲,並且開始把它推薦給周邊的朋友進行使用。但是很快,就有人提出了問題:我 希望把響應式作用到物件的具體屬性中 ,而不是 一個屬性改變,全部計算重新執行

你希望:使每個屬性具備單獨的響應性

響應性繫結物件,導致 一個屬性改變,全部計算重新執行。所以你希望把響應式作用到物件的具體屬性中,只 重新運算該屬性相關的內容

為了實現這個功能,你需要藉助 Map 物件

Mapkey:val 的形式儲存資料,你希望以 屬性為 key,以該屬性相關的運算方法集合為 val。以此你構建了一個 depsMap 物件,用來達到你的目的:

// -------------建立響應式-------------
  // Key:Val 結構的集合
  let depsMap = new Map();
  // 為每個屬性單獨儲存運算函式,從而讓每個屬性具備自己獨立的響應式
  function track(key, eff) {
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(eff)
  }
  // 觸發器,執行指定屬性的運算函式
  function trigger(key) {
    // 獲取指定函式的 dep 陣列
    const dep = depsMap.get(key);
    // 遍歷 dep,執行指定函式的運算函式
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // -------------建立資料來源-------------
  // 宣告商品物件,為資料來源
  let product = {
    price: 10,
    quantity: 2
  };
  // 宣告總價格
  let total = 0;
  // 運算總價格的匿名函式
  let effect = () => {
    total = product.price * product.quantity;
  };

  // -------------執行響應式-------------
  // 儲存運算函式
  track('quantity', effect);
  // 運算 總價格
  effect();
  console.log(`總價格:${total}`); // 總價格:20

  // 修改資料來源
  product.quantity = 5;
  // quantity 被修改,僅僅觸發 quantity 的響應式
  trigger('quantity');
  console.log(`總價格:${total}`); // 總價格:50
</script>

你的客戶總是非常挑剔的,很快他們丟擲了新的問題:我的程式不可能只有一個物件!你需要讓所有的物件都具備響應式!

你希望:使不同物件的不同屬性具備單獨的響應性

你的響應式需要覆蓋程式中的所有物件,否則你的程式碼將毫無意義!

為了達到這個目的,你需要將 物件、屬性、運算方法 進行分別的快取,現有的 depsMap 已經沒有辦法滿足你了。你需要更加強大的 Map ,讓 每個物件 都有一個 Map 。它就是 WeakMap

WeakMap 物件是一組鍵/值對的集合。其鍵必須是物件,而值可以是任意的。

藉助 WeakMap 你讓每個物件都擁有了一個 depsMap

  // -------------建立響應式-------------
  // weakMap:key 必須為物件,val 可以為任意值
  const targetMap = new WeakMap()
  // 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
  function track(target, key, eff) {
    // 獲取物件所對應的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 獲取 depsMap 對應的屬性
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    // 儲存不同物件,不同屬性的 運算函式
    dep.add(eff)
  }
  // 觸發器,執行指定物件的指定屬性的運算函式
  function trigger(target, key) {
    // 獲取物件所對應的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 獲取指定函式的 dep 陣列
    const dep = depsMap.get(key);
    // 遍歷 dep,執行指定函式的運算函式
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // -------------建立資料來源-------------
  // 宣告商品物件,為資料來源
  let product = {
    price: 10,
    quantity: 2
  };
  // 宣告總價格
  let total = 0;
  // 運算總價格的匿名函式
  let effect = () => {
    total = product.price * product.quantity;
  };


  // -------------執行響應式-------------
  // 儲存運算函式
  track(product, 'quantity', effect);
  // 運算 總價格
  effect();
  console.log(`總價格:${total}`); // 總價格:20

  // 修改資料來源
  product.quantity = 5;
  // quantity 被修改,僅僅觸發 quantity 的響應式
  trigger(product, 'quantity');
  console.log(`總價格:${total}`); // 總價格:50

每次資料改變,我都需要重新執行 trigger , 這樣太麻煩了!萬一我忘了怎麼辦? 。客戶總是會提出一些 改(wu)進(li) 的要求,沒辦法,誰讓人家是客戶呢?

你希望:使不同物件的不同屬性具備自動的響應性

每次資料改變,我都需要重新執行 trigger ,你的客戶發出了這樣的抱怨。

如果想要達到這樣的目的,那麼你需要了解 “資料的行為” , 即:你需要知道,資料在什麼時候被賦值,在什麼時候被輸出

此時你需要藉助兩個新的物件:

藉助 Proxy + Reflect 你成功實現了對資料的監聽:

  // -------------建立響應式-------------
  // weakMap:key 必須為物件,val 可以為任意值
  const targetMap = new WeakMap()
  // 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
  function track(target, key, eff) {
    // 獲取物件所對應的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 獲取 depsMap 對應的屬性
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    // 儲存不同物件,不同屬性的 運算函式
    dep.add(eff)
  }
  // 觸發器,執行指定物件的指定屬性的運算函式
  function trigger(target, key) {
    // 獲取物件所對應的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 獲取指定函式的 dep 陣列
    const dep = depsMap.get(key);
    // 遍歷 dep,執行指定函式的運算函式
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // 使用 proxy 代理資料來源,以達到監聽的目的
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key, effect)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
  }

  // -------------建立資料來源-------------

  // 宣告商品物件,為資料來源
  let product = reactive({ price: 10, quantity: 2 })
  // 宣告總價格
  let total = 0;
  // 運算總價格的匿名函式
  let effect = () => {
    total = product.price * product.quantity;
  };


  // -------------執行響應式-------------
  effect()
  console.log(`總價格:${total}`); // 總價格:20
  // 修改資料來源
  product.quantity = 5;
  console.log(`總價格:${total}`); // 總價格:50

你心滿意足,覺得你的程式碼無懈可擊。突然耳邊響起客戶 賞(bu)心(he)悅(shi)目(yi) 的聲音:你不覺得每次執行 effect 很反人類嗎?

你希望:讓運算自動執行

自動化!自動化!所有的操作都應該自動執行!

為了可以讓運算自動執行,你專門設計了一個 effect 函式,它可以 接收運算函式,並自動執行

  // -------------建立響應式-------------
  // weakMap:key 必須為物件,val 可以為任意值
  const targetMap = new WeakMap()
  // 運算函式的物件
  let activeEffect = null;
  // 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
  function track(target, key) {
    if (activeEffect) {
      // 獲取物件所對應的 depsMap
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      // 獲取 depsMap 對應的屬性
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      // 儲存不同物件,不同屬性的 運算函式
      dep.add(activeEffect)
    }

  }
  // 觸發器,執行指定物件的指定屬性的運算函式
  function trigger(target, key) {
    // 獲取物件所對應的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 獲取指定函式的 dep 陣列
    const dep = depsMap.get(key);
    // 遍歷 dep,執行指定函式的運算函式
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // 使用 proxy 代理資料來源,以達到監聽的目的
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
  }

  // 接收運算函式,執行運算函式
  function effect(eff) {
    activeEffect = eff;
    activeEffect();
    activeEffect = null;
  }

  // -------------建立資料來源-------------
  // 宣告商品物件,為資料來源
  let product = reactive({ price: 10, quantity: 2 })
  // 宣告總價格
  let total = 0;
  // 通過 effect 運算總價格
  effect(() => {
    total = product.price * product.quantity;
  })


  // -------------執行響應式-------------
  console.log(`總價格:${total}`); // 總價格:20
  // 修改資料來源
  product.quantity = 5;
  console.log(`總價格:${total}`); // 總價格:50

總結

vue 的響應性讓人驚奇,我們希望瞭解它,更希望知道它的發展歷程。

我們從 JS 的程式性 開始,站在 人性 開始思考,程式應該是什麼樣子的?

我們經歷了 6 個大的階段,最終得到了我們想要的 響應式 系統,而這個也正是 vue3 的響應式在構建時,所經歷的 ”過程“。