Vue3 為什么要用 Proxy 代替 Object.defineProperty 實現響應式


Object.defineProperty 劫持數據

只是對對象的屬性進行劫持

  • 無法監聽新增屬性和刪除屬性
    需要使用 vue.set, vue.delete
  • 深層對象的劫持需要一次性遞歸
var obj = { a: 1, o: { b: 2, o1: {} } }

 

  • 無法監聽原生數組,需要特殊處理,重寫覆蓋部分 Array.prototype 原生方法。

Proxy 劫持數據

真正的對對象本身進行劫持

  • 可以監聽到對象新增,刪除屬性
  • 只在 getter 時才對對象的下一層進行劫持(優化了性能),如訪問obj.o,屬性o才是響應式, 訪問obj.o.b, b才是響應式,訪問到那個屬性,那個屬性才是響應式,不需一次性遍歷
  • 能正確監聽原生數組方法
  • 無法 polyfill 存在瀏覽器兼容問題

Object.defineProperty 實現響應式

function defineReactive(target, key, value) {
    observer(value) // 對 value 深層監聽
    
    Object.defineProperties(target, key, {
        get() {
            // dep.addSubs(watcher) // 添加到監聽隊列
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                observer(newValue) // 再次劫持新 value
                
                value = newValue
                // dep.notify() // 通知依賴觸發監聽隊列的更新
            }
        }
    })
}

function observer(target) {
  if (typeof target !== 'object' || !target) {
    return target
  }
  
  if (Array.isArray(target)) {
    target.__proto__ = newArrProto
  }
  //遍歷遞歸 for (let key of target) {
    defineReactive(target, key, target[key])
  }
}
//創建原生數組原型 const oldArrProto = Array.prototype

const newArrProto = Object.create(oldArrProto)
//重寫數組方法 [
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(methodName => { newArrProto[methodName] = function(...args) { // dep.notify() oldArrProto[methodName].apply(this, args) } })

 

Dep 和 Watcher 具體實現可以參考之前的文章 實現響應式原理

Object.defineProperty 缺點

  • 無法監聽新增屬性和刪除屬性
    需要使用 vue.set, vue.delete
  • 深層對象的劫持需要一次性遞歸
function observer(target) {
  ...
  for (let key of target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  observer(target) // 首次監聽時就對 value 的屬性進行遞歸
  ...
}
監聽原生數組的部分方法需要重寫覆蓋 Array.prototype
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] 會改變原數組的原生方法不會被 Object.defineProperty 劫持,需要重新寫數組的原生方法添加更新觸發

 

Proxy 實現響應式

reflect,規范化,標准化,函數式,mdn描述;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
替代掉Object上的工具函數,

 

 

 

// 創建響應式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        return target
    }
    
    const proxyConfig = {
        get(target, key, receiver) {
        // 只處理本身(非原型的)屬性
            // target是對象,ownKeys是屬性的數組 // target是數組,ownKeys是索引數組,最后一個是length
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
              // dep.subs(watcher) // 添加監聽
            }
            const result = Reflect.get(target, key, receiver)
          // 深度監聽
            // 性能如何提升的?
            return reactive(result) // 只在 getter 時才再次劫持
        },
        set(target, key, val, receiver) {
        // 重復的數據,不處理
          if (val === target[key]) {
            return
          }
            
            const ownKeys = Reflect.ownKeys(target)
        // 判斷是否是新增屬性
            if (ownKeys.includes(key)) { // 已有值
            } else {
              // 新增值
            }
            
            const result = Reflect.set(target, key, val, receiver)
            // dep.noitfy() // 通知監聽隊列進行更新
        // 是否設置成功
            return result
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            // 是否刪除成功
            return result
        }
    }
    
    const observed = new Proxy(target, proxyConfig)
    return observed
}                            

 

Proxy 解決的問題

  • 可以監聽到對象新增刪除屬性
  • 只在 getter 時才對對象的下一層進行劫持(優化了性能)
    get(target, key, receiver) {
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
              // dep.subs(watcher) // 添加監聽
            }
            const result = Reflect.get(target, key, receiver)
            return reactive(result) // 只在 getter 時才再次劫持
        },

 

  • 能正確監聽原生數組方法

總結

Object.defineProperty 是對所有對象屬性的劫持,如訪問obj.o, 所有屬性,包括深層的都是響應式,浪費性能
Proxy 是對訪問到哪個屬性,那個屬性就是響應式,如訪問obj.o,屬性o才是響應式, 訪問obj.o.b, b才是響應式,訪問到那個屬性,那個屬性才是響應式,不需一次性遍歷

Object.defineProperty 無法監聽新增和刪除
Object.defineProperty 無法監聽數組部分方法需要重寫
Object.defineProperty 性能不好要對深層對象劫持要一次性遞歸

Proxy 能正確監聽數組方法
Proxy 能正確監聽對象新增刪除屬性
Proxy 只在 getter 時才進行對象下一層屬性的劫持 性能優化
Proxy 兼容性不好

Object.defineProperty 和 Proxy
在 getter 時進行添加依賴 dep.addSub(watcher) 比如 綁定 view 層,在函數中使用
在 setter 時觸發依賴通知 dep.notify() 比如 修改屬性



作者:coolheadedY
原文鏈接:https://www.jianshu.com/p/8cde476238f0


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM