<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 一種JavaScript響應式系統設計與實現

    一種JavaScript響應式系統實現

    根據VueJs核心團隊成員霍春陽《Vue.js設計與實現》第四章前三節整理而成

    1. 響應式數據與副作用函數

    1.1 副作用函數

    會產生副作用的函數。

    如下示例所示:

    function effect () {
        document.body.innerText = 'hello vue3!'
    }
    

    當effect函數執行時,會設置body的文本內容。但是,除了effect函數之外的任何函數都可以讀取或者設置body的文本內容。也就是說,effect函數的執行,會直接或間接影響其他函數的執行,這時,我們說effect函數產生了副作用。

    // 此方法的結果就受effect函數的影響
    function getInnerText () {
        return document.body.innerText
    }
    

    副作用很容易產生,例如一個函數修改了全局變量,這其實也是一個副作用,如下面代碼所示:

    let globalValue = 1
    function effect () {
        globalValue = 3 // 修改全局變量,產生副作用
    }
    

    1.2 響應式數據

    理解了什么是副作用函數,再來說一說什么是響應式數據。

    假設在一個副作用函數中讀取了某個對象的屬性:

    const obj = {
        text: 'hello vue2'
    }
    function effect () {
        document.body.innerText = obj.text // effect 函數的執行會讀取obj.text
    }
    

    如上面代碼所示,副作用函數effect會設置body元素的innerText屬性,其值為obj.text。當obj.text的值改變時,我們希望副作用函數effect()會重新執行:

    obj.text = 'hello vue3' // 修改obj.text的值,同時希望副作用函數重新執行
    

    這句代碼修改了字段 obj.text 的值,我們希望當值變化后,副作用函數會重新執行,如果能實現這個目標,那么對象 obj 就是響應式數據。(某數據改變時,依賴該數據的副作用函數會重新執行,該數據即為響應式數據

    但是,從上面代碼來看,我們還做不到這一點,因為obj是一個普通的對象,當我們修改它的值時,除了值本身發生變化外,不會有任何其他反應。

    2. 響應式數據的基本實現

    2.1 如何讓 obj 變為響應式數據?

    通過觀察,我們可以發現 2 點線索:

    • 當副作用函數effect()執行時,會觸發字段 obj.text 的讀取操作;
    • 當修改 obj.text 的值時,會觸發字段 obj.text設置操作;

    ? 如果我們能攔截一個對象的讀取設置操作,事情就變得簡單了。

    • 當讀取obj.text 時,我們可以把副作用函數effect()存儲到一個“桶”里;

    image-20220627151018125

    • 當設置obj.text時,再把effect()副作用函數從 “桶” 里取出,并執行;
    image-20220627151637370

    2.2 如何攔截對象屬性的讀取和設置操作

    在 es5 及之前,只能通過 Obj.defineProperty 來實現,這也是vue2中所采用的方式。

    es6以后,可以使用代理對象 Proxy 來實現,這是vue3中的所采用的方式。

    實現步驟:

    • 創建一個用于存儲副作用函數的容器(或者通俗點說,一個桶,vue3里用了bucket這個詞,以后都稱為桶);
    • 定義一個普通對象obj作為原始數據;
    • 創建 obj 對象的代理作為響應式數據,分別設置 getset 攔截函數,用于攔截讀和寫操作;
    • 當在effect副作用函數中執行響應式數據讀操作時,將effect()副作用函數存到桶里;
    • 當對響應式數據進行寫操作時,先更新原始數據,再從桶中取出依賴了響應式數據的函數,進行執行;

    接下來,根據以上思路,使用Proxy 實現一下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p id="title"></p>
        <button onclick="changeObj()">修改響應式數據</button>
      </div>
    </body>
    </html>
    <script>
      // 1. 創建一個存儲副作用函數的桶      
      const bucket = new Set()
      // 2. 一個普通的對象
      const obj = {
        text: 'hello vue2'
      }
      // 3. 普通對象的代理,一個簡單的響應式數據
      const objProxy = new Proxy(obj, {
        get (target, key) {
          bucket.add(effect)
          return target[key]
        },
        set (target, key, newValue) {
          target[key] = newValue
          bucket.forEach((fn) => {
            fn()
          })
          return true
        }
      })
    
      // 4. 副作用函數 effect(), 給p標簽設置值
      const effect = function () {
        document.getElementById('title').innerText = objProxy.text
      }
    
      // 5. 改變代理對象的元素值
      const changeObj = function () {
        objProxy.text = 'hello vue3!!!!!!'
      }
    
      // 首次進入時,給p標簽設置值
      effect()
    </script>
    

    響應式-show-1

    2.3 缺陷

    • 副作用函數名字叫effect ,是硬編碼的,假如叫其他的名字,就無法正確執行了;

    3. 完善的響應式系統

    3.1 如何存儲一個任意命名(甚至匿名)的副作用函數?

    需要提供一個用來注冊副作用函數的機制,如以下代碼所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p id="title"></p>
        <button onclick="changeObj()">修改響應式數據</button>
      </div>
    </body>
    </html>
    <script>
    let activeEffect
    function effectRegister (fn) {
      activeEffect = fn
      fn()
    }
    
    const bucket = new Set()
    const obj = {
      text: 'hello vue2'
    }
    const objProxy = new Proxy(obj, {
      get (target, key) {
        if (activeEffect) {
          bucket.add(activeEffect)
        }
        return target[key]
      },
      set (target, key, newValue) {
        target[key] = newValue
        bucket.forEach((fn) => {
          fn()
        })
        return true
      }
    })
    const changeObj = function () {
      objProxy.text = 'hello vue3!!!!!!'
    }
    
    effectRegister(() => {
      document.getElementById('title').innerText = objProxy.text
    })
    </script>
    
    

    以上方式,通過定義一個全局變量activeEffect,用來存儲(匿名)副作用函數。并提供一個注冊副作用函數的注冊函數 effectRegister,該函數是一個高階函數,接受一個函數作為參數,保存并執行該函數。將(匿名)副作用函數保存到activeEffect 中,當(匿名)副作用函數執行時,觸發響應式數據的讀操作,此時將activeEffect 存入副作用函數桶中。

    缺陷:

    以上方案成功解決了匿名的函數的保存問題,但仍存在一個嚴重問題:

    響應式對象內的不同屬性和不同副作用函數的對應問題

    下面給響應式數據添加一個屬性,以上代碼微調為如下:

    // ... 省略未改變代碼
    
    const setObj = function (key, value) {
      objProxy[key] = value
    }
    
    const changeObj = function () {
      setObj('text', 'hello vue3!!!!!!')
      setObj('notExist', 'this key is not exist')
      console.log(1, bucket)
    }
    
    effectRegister(() => {
      console.log('執行')
      document.getElementById('title').innerText = objProxy.text
    })
    

    執行結果為:

    image-20220627190632726

    可以看到,此時副作用函數與obj.notExist 屬性并未建立響應關系,但當給 notExist 賦值時,副作用函數也執行了,這顯然不對了。

    理想情況應該是,a屬性與aFunc建立響應式關系,b屬性與bFunc建立響應式聯系,則 a 改變時,僅 aFunc函數觸發執行,b改變時,僅bFunc觸發執行。

    3.2 如何解決響應式對象內的不同屬性和不同副作用函數的對應問題?

    問題分析:

    導致該問題的根本原因是,我們沒有在副作用函數與被操作的目標字段之間建立明確的聯系

    例如,但讀取屬性值時,無論讀取到哪一個屬性,其實都一樣,都會把副作用函數收集到“桶”里;無論設置的是哪一個屬性,也會把“桶”里面的副作用函數取出并執行。響應數據屬性和副作用函數之間沒有明確的聯系。

    解決方案很簡單,只需要在副作用函數與被操作的字段之間建立聯系即可。

    要實現不同屬性值與副作用函數對應,Set類型的數據作為桶已經明顯不合適了。

    科普下es6幾大數據類型:

    Set:ES6 提供了新的數據結構 Set。它類似于數組,但是成員的值都是唯一的,沒有重復的值。

    WeakSet:WeakSet 結構與 Set 類似,也是不重復的值的集合。但是,它與 Set 有兩個區別。首先,WeakSet 的成員只能是對象,而不能是其他類型的值。其次,WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,如果其他對象都不再引用該對象,那么垃圾回收機制會自動回收該對象所占用的內存,不考慮該對象還存在于 WeakSet 之中。

    Map:它類似于對象,也是鍵值對的集合,但是“鍵”的范圍不限于字符串,各種類型的值(包括對象)都可以當作鍵。也就是說,Object 結構提供了“字符串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。

    WeakMapWeakMap結構與Map結構類似,也是用于生成鍵值對的集合。WeakMapMap的區別有兩點。

    首先,WeakMap只接受對象作為鍵名(null除外),不接受其他類型的值作為鍵名。其次,WeakMap的鍵名所指向的對象,不計入垃圾回收機制。

    新的方案:

    首先,來看下如下代碼:

      effectRegister(function effectFn () {
        document.getElementById('title').innerText = objProxy.text
      })
    

    這段代碼存在三個角色:

    • 被讀取的響應式數據 objProxy;
    • 被讀取的響應式數據的屬性名 text;
    • 使用effectRegister 函數注冊的副作用函數 effectFn;

    如果使用 target 來表示一個代理對象所代理的原始對象,用key 來表示被操作的字段名,用effectFn 來表示被注冊的副作用函數,那么可以為這三個角色建立如下關系:

    image-20220627213637491

    這是一種樹型結構,下面舉幾個例子進行補充說明:

    1. 如果有2個副作用函數同時讀取同一個對象的屬性值:

      effectRegister(function effectFn1 () {
          obj.text
      })
      
      effectRegister(function effectFn2 () {
          obj.text
      })
      

    ? 那么這三者關系如下:

    image-20220627214317647

    1. 如果一個副作用函數中讀取了同一個對象的 2 個不同屬性:

      effectRegister(function effectFn () {
          obj.text1
          obj.text2
      }
      

    那么這三者關系如下:

    image-20220627214620629

    1. 如果 2 個不同的副作用函數中讀取了 2 個不同對象的不同屬性:

      effectRegister(function effectFn1 () {
          obj1.text1
      })
      
      effectRegister(function effectFn2 () {
          obj2.text2
      })
      

      那么這三者關系如下:

      image-20220627215041694

    總之,這其實就是一個樹形數據結構。這個聯系建立起來后,就可以解決前文提到的問題。上文中,如果我們設置了obj2.text2的值,就只會導致 effectFn2 函數重新執行,并不會導致 effectFn1 函數重新執行。

    接下來,需要重新設計這個桶。根據對以上幾種情況的分析,我們可以總結一下這種數據結構的模型:

    image-20220628103144724

    首先,使用WeakMap 代替 Set 來作為桶,將 原始對象 target 作為WeakMap 的key,使用 Map 作為value。在Map 中又以屬性值 作為 key值,使用 Set 存儲key 對應的副作用函數 effectFn,然后修改 get/set 攔截器代碼。

    以下,我們將桶命名為 bucket, 每個對象的 屬性-副作用函數 存儲塊 命名為 depsMap,將depsMap中的value部分(Set類型,存儲副作用函數)命名為 deps(與vue3響應式實現源碼命名保持一致),根據數據結構圖,重新編寫get/set 攔截器代碼:

    js:

    // WeakMap 類型的桶
    const bucket = new WeakMap()
    // 初始值為undefined
    let activeEffect
    // 定義2個對象
    const obj_1 = {
        text1: 'hello vue',
        text2: 'hello jquery'
    }
    
    const obj_2 = {
        text1: 'hello react',
        text2: 'hello angular'
    }
    
    const objProxy1 = new Proxy(obj_1, {
        get (target, key) {
            // 如果沒有注冊的副作用函數,直接返回key對應的value值
            if (!activeEffect) {
                return target[key]
            }
            
            // 獲取桶內 target 對象對應的 depsMap
            let depsMap = bucket.get(target)
            // 如果該對象還沒有depsMap,這新建一個
            if (!depsMap) {
                depsMap = new Map()
                bucket.set(target, depsMap)
            }
            
            // 從depsMap中取出屬性對應的副作用函數集合
            let deps = depsMap.get(key)
            // 同理,若不存在,則創建
            if (!deps) {
                deps = new Set()
                depsMap.set(key, deps)
            }
            deps.add(activeEffect)
            return target[key]
        },
        set (target,key, newValue) {
            // 給target的key設置新的value
            target[key] = newValue
            
            // 取出該target對應的depsMap
            let depsMap = bucket.get(target)
            // depsMap 不存在或為空直接返回
            if (!depsMap) {
                return
            }
            
            // 取出deps
            let deps = depsMap.get(key)
            // deps 不存在或為空直接返回
            if (!deps) {
                return
            }
            // 依次執行對應副作用函數
            deps.forEach((fn) => {
                fn()
            })
            return true
        }
    })
    
    const objProxy2 = new Proxy(obj_2, {
        ...
    })
    
    const effectRegister = function (fn) {
        activeEffect = fn
        fn()
    }
    
    

    演示:

    響應式-show-3

    3.3 思考:為什么桶的結構使用WeakMap而不是Map?

    看一段代碼:

    const map = new Map()
    const weakmap = new WeakMap()
    ;(function () {
        const foo = { foo: 1}
        const bar = { bar: 2}
        map.set(foo, 1)
        weakmap.set(bar, 2)
    })();
    console.log(map)
    console.log(weakmap)
    

    思考,打印結果分別是啥?

    注意:在瀏覽器環境下,console打印結果表現出異步行為。

    image-20220628140308855

    原因分析:

    Map對key是強引用,當立即執行函數結束后,foo仍被map引用,因此map.keys()可以成功打印出key值,而WeakMap對key是弱引用,立即執行函數結束后,bar 失去引用,被垃圾回收器回收掉,因此weakmap.keys()無法取出key值。

    結論:

    WeakMap經常存儲那些只有當key值所引用的對象存在(沒有被垃圾回收)時才有價值的信息,例如示例中的target,如果target對象沒有任何引用了,說明用戶測不需要它了,這是垃圾回收器會完成回收任務。

    如果使用Map來代替WeakMap,那即使用戶側沒有任何對target的引用,這個target也不會被回收,最終可能導致內存溢出。

    4. 總結

    以上,我們實現了一個簡單的響應系統,核心思路是通過代理來攔截響應數據的讀和寫的操作,將與對象屬性相關的副作用函數進行存儲,當對象屬性變化時,同步執行相關聯的副作用函數,達到響應式的效果。

    其實現中混合使用了代理模式和觀察者模式。

    5. 參考資料

    附:程序源碼

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p id="title_1"></p>
        <p id="title_2"></p>
        <button onclick="setObj_1()">修改響應式數據1</button>
        <button onclick="setObj_2()">修改響應式數據2</button>
      </div>
    </body>
    </html>
    <script>
      let activeEffect
      let count = 3
      let version = 3
      const bucket = new WeakMap()
      const obj_1 = {
        text1: 'hello vue2',
        text2: 'hello jq2'
      }
    
      const obj_2 = {
        text2: 'hello react16',
        text1: 'hello ng2'
      }
    
      const getProxy = function (obj) {
        return new Proxy(obj, {
          get (target, key) {
            track(target, key)
            return target[key]
          },
          set (target, key, newValue) {
            trigger(target, key, newValue)
            return true
          }
        })
      }
    
      const track = function (target, key) {
        // 如果無副作用函數,則直接返回原始對象值
        if (!activeEffect) {
          return
        }
    
        // 以該代理對象的原始對象作為key值,獲取depsMap  (屬性和副作用函數之間的對應關系 key --> effectFn)
        let depsMap = bucket.get(target)
        // 如果不存在depsMap,則新建一個Map與target關聯起來
        if (!depsMap) {
          depsMap = new Map()
          bucket.set(target, depsMap)
        }
        // 根據key從depsMap中獲取對應的deps,deps是一個Set
        let deps = depsMap.get(key)
        // 如果不存在,則新建Set,并與key關聯
        if (!deps) {
          deps = new Set()
          depsMap.set(key, deps)
        }
        // 最后將當前活躍的副作用函數添加到桶里
        deps.add(activeEffect)
      }
    
      const trigger = function (target, key, newValue) {
        target[key] = newValue
        let depsMap = bucket.get(target)
        if (!depsMap) {
          return
        }
        let deps = depsMap.get(key)
        if (!deps) {
          return
        }
    
        deps.forEach(fn => {
          fn()
        })
      }
    
      const objProxy1 = getProxy(obj_1)
      const objProxy2 = getProxy(obj_2)
    
      const effectRegister = function (fn) {
        activeEffect = fn
        fn()
      }
    
      const setObj_1 = function () {
        objProxy1.text1 = `hello, vue${count++}`
        console.log('bucket-1', bucket)
      }
    
      const setObj_2 = function () {
        objProxy2.text2 = `hello, react${version++}`
        console.log('bucket-2', bucket)
      }
    
      console.log('bucket-init', bucket)
      effectRegister(function effectFn1 () {
        document.getElementById('title_1').innerText = objProxy1.text1
      })
    
      effectRegister(function effectFn2 () {
        document.getElementById('title_2').innerText = objProxy2.text2
      })
    </script>
    
    posted @ 2022-07-04 19:03  CherishTheYouth  閱讀(80)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看