Vue3和Mobx5都使用Proxy了,你更應該瞭解Proxy Vue3和Mobx5都使用Proxy了,你更應該瞭解Proxy

Vue3和Mobx5都使用Proxy了,你更應該瞭解Proxy

 

vue3.0的pre-alpha版代碼已經開源了,就像作者之前放出的消息一樣,其數據響應這一部分已經由ES6的Proxy來代替Object.defineProperty實現,感興趣的同學可以看其實現源碼,同樣Mobx5也使用Proxy來實現數據響應式。vue和mobx都開始使用Proxy來實現數據的響應式了,所以有必要抽點時間瞭解下Proxy。

Object.defineProperty的缺陷

說到Proxy,就不得不提Object.defineProperty,我們都知道,vue3.0之前的版本都是使用該方法來實現數據的響應式,具體是:

通過設定對象屬性getter/setter方法來監聽數據的變化,同時getter也用於依賴收集,而setter在數據變更時通知訂閱者更新視圖。

大概如下代碼所示:

function defineReactive(obj, key, value) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            collectDeps() // 收集依賴
            return value
        },
        set(newVal) {
            observe(newVal); // 若是對象需要遞歸子屬性
            if (newVal !== value) {
                notifyRender() // 通知訂閱者更新
                value = newVal;
            }
        }
    })
}
function observe(obj) {
    if (!obj || typeof obj! === 'object') {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key]);
    })
}

var data = {
    name: 'wonyun',
    sex: 'male'
}
observe(data)

雖然Object.defineProperty通過爲屬性設置getter/setter能夠完成數據的響應式,但是它並不算是實現數據的響應式的完美方案,某些情況下需要對其進行修補或者hack,這也是它的缺陷,主要表現在兩個方面:

  1. 無法檢測到對象屬性的新增或刪除

    由於js的動態性,可以爲對象追加新的屬性或者刪除其中某個屬性,這點對經過Object.defineProperty方法建立的響應式對象來說,只能追蹤對象已有數據是否被修改,無法追蹤新增屬性和刪除屬性,這就需要另外處理。

    目前Vue保證響應式對象新增屬性也是響應式的,有兩種方式:

    • Vue.set(obj, propertName/index, value)

    • 響應式對象的子對象新增屬性,可以給子響應式對象重新賦值

      data.location = {
          x: 100,
          y: 100
      }
      data.location = {...data, z: 100}
      

    響應式對象刪除屬性,可以使用Vue.delete(obj, propertyName/index)或者vue.$delete(obj, propertyName/index); 類似於刪除響應式對象子對象的某個屬性,也可以重新給子對象賦值來解決。

  2. 不能監聽數組的變化

    vue在實現數組的響應式時,它使用了一些hack,把無法監聽數組的情況通過重寫數組的部分方法來實現響應式,這也只限制在數組的push/pop/shift/unshift/splice/sort/reverse七個方法,其他數組方法及數組的使用則無法檢測到,例如如下兩種使用方式:

    • vm.items[index] = newValue

    • vm.items.length--

    那麼vue怎麼實現數組的響應式呢,並不是重寫數組的Array.prototype對應的方法,具體來說就是重新指定要操作數組的prototype,並重新該prototype中對應上面的7個數組方法,通過下面代碼簡單瞭解下實現原理:

    const methods = ['pop','shift','unshift','sort','reverse','splice', 'push'];
     // 複製Array.prototype,並將其prototype指向Array.prototype
    let proto = Object.create(Array.prototype);
    methods.forEach(method => {
        proto[method] = function () { // 重寫proto中的數組方法
            Array.prototype[method].call(this, ...arguments);
            viewRender() // 視圖更新
        }
    })
    
    function observe(obj) {
        if (Array.isArray(obj)) { // 數組實現響應式
            obj.__proto__ = proto; // 改變傳入數組的prototype
            return;
        }
        if (typeof obj === 'object') {
            ... // 對象的響應式實現
        }
    }
    

Proxy的使用

Proxy,字面意思是代理,是ES6提供的一個新的API,用於修改某些操作的默認行爲,可以理解爲在目標對象之前做一層攔截,外部所有的訪問都必須通過這層攔截,通過這層攔截可以做很多事情,比如對數據進行過濾、修改或者收集信息之類。借用proxy的巧用的一幅圖,它很形象的表達了Proxy的作用。

ES6原生提供的Proxy構造函數,用法如下:

var proxy = new Proxy(obj, handler)

其中obj爲Proxy要攔截的對象,handler用來定製攔截的操作,返回一個新的代理對象proxy;Proxy代理特點:

  • Proxy直接代理整個對象而非對象屬性

    Proxy的代理針對的是整個對象,而不是像Object.defineProperty針對某個屬性。只需做一層代理就可以監聽同級結構下的所有屬性變化,包括新增屬性和刪除屬性

  • Proxy也可以監聽數組的變化

    例如上面vue使用的Object.defineProperty實現響應式方式用Proxy來實現則相對比較簡單:

    let handler = {
     get(target, key){
       if (target[key] === 'object' && target[key]!== null) {
         // 嵌套子對象也需要進行數據代理
         return new Proxy(target[key], hanlder)
       }
       collectDeps() // 收集依賴
       return Reflect.get(target, key)
     },
     set(target, key, value) {
       if (key === 'length') return true
       notifyRender() // 通知訂閱者更新
       return Reflect.set(target, key, value);
     }
    }
    let proxy = new Proxy(data, handler);
    proxy.age = 18 // 支持新增屬性
    let proxy1 = new Proxy({arr: []}, handler);
    proxy1.arr[0] = 'proxy' // 支持數組內容變化
    

    上面的Proxy的構造函數中的 get/set爲Proxy定義的13種的trap中的其中兩種,它共有13種代理操作方法:

    trap描述
    handler.get 獲取對象的屬性時攔截
    handler.set 設置對象的屬性時攔截
    handler.has 攔截propName in proxy的操作,返回boolean
    handler.apply 攔截proxy實例作爲函數調用的操作,proxy(args)proxy.call(...)proxy.apply(..)
    handler.construct 攔截proxy作爲構造函數調用的操作
    handler.ownKeys 攔截獲取proxy實例屬性的操作,包括Object.getOwnPropertyNamesObject.getOwnPropertySymbolsObject.keysfor...in
    handler.deleteProperty 攔截delete proxy[propName]操作
    handler.defineProperty 攔截Objecet.defineProperty
    handler.isExtensible 攔截Object.isExtensible操作
    handler.preventExtensions 攔截Object.preventExtensions操作
    handler.getPrototypeOf 攔截Object.getPrototypeOf操作
    handler.setPrototypeOf 攔截Object.setPrototypeOf操作
    handler.getOwnPropertyDescriptor 攔截Object.getOwnPropertyDescriptor操作

Proxy代理目標對象,是通過操作上面的13種trap來完成的,這與ES6提供的另一個apiReflect的13種靜態方法一一對應。二者一般是配合使用的,在修改proxy代理對象時,一般也需要同步到代理的目標對象上,這個同步就是用Reflect對應方法來完成的。例如上面的Reflect.set(target, key, value)同步目標對象屬性的修改。需要補充一點:

13種trap操作方法中,若初始化時handler沒設置的方法就直接操作目標對象,不會走攔截操作

Proxy的使用場景

Proxy因爲在目標對象之前架設了一層攔截,外部對該目標對象的訪問都必須經過這次攔截。那麼通過這層攔截,可以做很多事情,例如控制過濾、緩存、數據驗證等等,可以說Proxy的使用場景比較廣,下面簡單列舉幾個使用場景,更多實用場景可以參考Proxy的巧用

  • Vue3的數據響應

    vue3中利用Proxy實現數據讀取和設置時進行攔截,在攔截trap中實現數據的依賴收集以及觸發視圖更新操作,vue3該部分實現的主要僞碼如下:

    function get(target, key, receiver) { // handler.get的攔截實現
        const res = Reflect.get(target, key, receiver)
        if(isSymbol(key) && builtInSymbols.has(key)) return res
        if (isRef(res)) return res.value
        track(target, OperationTypes.GET, key) // 收集依賴
        return isObject(res) ? reactive(res) : res
    }
    // handler.set的攔截操作
    function set(target, key, value, receiver) {
        value = toRaw(value) // 獲取緩存響應數據
        oldValue = target[key]
        if (isRef(oldValue) && !isRef(value)) {
           oldValue.value = value
           return true
        }
        const result = Reflect.set(target, key, value, receiver)
        if (target === toRaw(receiver)) { //set攔截只限對象本身
           ... // 不同環境操作處理,並省略下面trigger方法第二參數獲取邏輯
           trigger(target, OperationTypes.x, key) // 觸發視圖更新
        }
        return result
    }
    
  • 獲取屬性對應的值,無該屬性或者屬性爲空返回默認值

    在項目中經常遇到這樣的需求,在前端拿到後端返回的數據時,獲取某些可選字段時,如果其值爲空或者不存在該屬性時,可以設置一個默認值,類似loadsh庫的get方法_.get(object, path, [defaultValue])。下面就對象形式下_.get用Proxy來實現,代碼如下:

     function getValueByPath(object, path, defaultValue) {
         let proxy = new Proxy(object, {
             get(target, key) {
                 if (key.startsWith('.')) {
                     key = key.slice(1);
                 }
                 if (key.includes('.')) {
                    path = path.split('.');
                    let index = 0, len = path.length;
                    while(target != null && index < len) {
                        target = target[path[index++]]
                    }
                    return target || defaultValue;
                 }
                 if (!(key in target) || !target[key]) {
                   return defaultValue
                 }
                 return Reflect.get(target, key)
             }
         });
         return proxy[path]
     }
    

    需要注意的是,參數path若有類似a.b.c這樣嵌套的路徑時,我們是直接在Proxy的handler.get中處理的,如果在proxy對象實例上調用如proxy.a.b.c則需要在Proxy的handler.get對返回對象的屬性還需要創建其Proxy實例,類似如下:

    function getValueByPath(object, path, defaultValue) {
        return proxy = new Proxy(object, {
          get(target, key) {
              if (isObject(target[key])){
                  return new Proxy(target[key], {get(){}})
              }
              ... // 其他省略
          }
        })
    }
    
  • 實現數組負數索引的訪問

    正常的數組,如果訪問數組的負數索引會得到undefined,現在要實現類似字符串的substr方法,傳遞負數索引index,表示從倒數第index開始讀取,實現攔截如下:

    function getArrItem(arr) {
        return new Proxy(arr, {
            get(target, key, receiver) {
                let index = Number(key);
                if (index < 0) {
                  key = String(target.length + index);
                }
                return Reflect.get(target, key, receiver)
            }
        });
    }
    

Proxy的劣勢

雖然Proxy相對於Object.defineProperty有很有優勢,但是並不是說Proxy就沒有劣勢,這主要表現在以下兩個方面:

  • 兼容性問題,無完全polyfill

    Proxy爲ES6新出的API,瀏覽器的對其支持情況可以在caniuse查到,如下圖所示:

    可以看出雖然大部分瀏覽器支持Proxy特性,但是一些瀏覽器或者其低版本不支持Proxy,其中IE、QQ瀏覽器、百度瀏覽器等完全不支持,因此Proxy有兼容性問題。那能否像ES6其他特性那樣有對應的polyfill解決方案呢,答案並不那麼樂觀。其中作爲ES6轉換的翹楚babel,在其官網明確做了說明:

    Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled.

    也就是說,由於ES5的限制,ES6的Proxy沒辦法被完全polyfill,所以babel沒有提供對應的轉換支持,Proxy的實現是需要JS引擎級別提供支持,目前大部分主要的JS引擎提供了支持,可以查看ES6 Proxy compatibilit

    然而,截止目前2019年10月,Google開發的Proxy polyfill:proxy-polyfill,其實現也是殘缺的,表現在:

    • 只支持Proxy的4個trap:getsetapplyconstruct

    • 部分支持的trap其功能也是殘缺的,如set不支持新增屬性

    • 該polyfill不能代理數組

  • 性能問題

    Proxy的另一個就是性能問題,爲此有人專門做了一個對比實驗,原文在這裏thoughts-on-es6-proxies-performance,對應的中文翻譯可以參考ES6 Proxy性能之我見。Proxy的性能比Promise還差,這就要需要在性能和簡單實用上進行權衡。例如vue3使用Proxy後,其對對象及數組的攔截很容易實現數據的響應式,尤其對數組來說。

    另外,Proxy作爲新標準將受到瀏覽器廠商重點持續的性能優化,性能這塊相信會逐步得到改善。

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章