Vue3.0裏爲何要用 Proxy API 替代 defineProperty API ?

1、Object.defineProperty

定義:Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象
爲何能實現響應式
經過defineProperty 兩個屬性,get及setjavascript

  • get
    屬性的 getter 函數,當訪問該屬性時,會調用此函數。執行時不傳入任何參數,可是會傳入 this 對象(因爲繼承關係,這裏的this並不必定是定義該屬性的對象)。該函數的返回值會被用做屬性的值
  • set
    屬性的 setter 函數,當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的 this 對象。默認爲 undefined
    下面經過代碼展現:
    定義一個響應式函數defineReactive
function update() {
  app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        update()
      }
    }
  })
}

調用defineReactive,數據發生變化觸發update方法,實現數據響應式java

const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
  obj.foo = new Date().toLocaleTimeString()
},1000)
// 在對象存在多個key狀況下,須要進行遍歷
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

若是存在嵌套對象的狀況,還須要在defineReactive中進行遞歸react

function defineReactive(obj, key, val) {
  observe(val)
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        update()
      }
    }
  })
}

當給key賦值爲對象的時候,還須要在set屬性中進行遞歸api

set(newVal) {
  if (newVal !== val) {
    observe(newVal) // 新值是對象的狀況
    notifyUpdate()
  }
}

上述例子可以實現對一個對象的基本響應式,但仍然存在諸多問題
如今對一個對象進行刪除與添加屬性操做,沒法劫持到數組

const obj = {
  foo: "foo",
  bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

當咱們對一個數組進行監聽的時候,並不那麼好使了app

const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
  defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0] = 99 // ok

能夠看到數據的api沒法劫持到,從而沒法實現數據響應式,
因此在Vue2中,增長了set、delete API,而且對數組api方法進行一個重寫
還有一個問題則是,若是存在深層的嵌套對象關係,須要深層的進行監聽,形成了性能的極大問題
小結
檢測不到對象屬性的添加和刪除
數組API方法沒法監聽到
須要對每一個屬性進行遍歷監聽,若是嵌套對象,須要深層監聽,形成性能問題ide

2、proxy

Proxy的監聽是針對一個對象的,那麼對這個對象的全部操做會進入監聽操做,這就徹底能夠代理全部屬性了
在ES6系列中,咱們詳細講解過Proxy的使用,就再也不述說了
下面經過代碼進行展現:
定義一個響應式方法reactive函數

function reactive(obj) {
  if (typeof obj !== 'object' && obj != null) {
      return obj
  }
  // Proxy至關於在對象外層加攔截
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      console.log(`獲取${key}:${res}`)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      console.log(`設置${key}:${value}`)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      console.log(`刪除${key}:${res}`)
      return res
    }
  })
  return observed
}

測試一下簡單數據的操做,發現都能劫持性能

const state = reactive({
  foo: 'foo'
})
// 1.獲取
state.foo // ok
// 2.設置已存在屬性
state.foo = 'fooooooo' // ok
// 3.設置不存在屬性
state.dong = 'dong' // ok
// 4.刪除屬性
delete state.dong // ok

再測試嵌套對象狀況,這時候發現就不那麼 OK 了測試

const state = reactive({
    bar: { a: 1 }
})

// 設置嵌套對象屬性
state.bar.a = 10 // no ok
// 若是要解決,須要在get之上再進行一層代理
function reactive(obj) {
  if (typeof obj !== 'object' && obj != null) {
    return obj
  }
  // Proxy至關於在對象外層加攔截
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      console.log(`獲取${key}:${res}`)
      return isObject(res) ? reactive(res) : res
    }
  })
  return observed
}

3、總結

Object.defineProperty只能遍歷對象屬性進行劫持

function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

Proxy直接能夠劫持整個對象,並返回一個新對象,咱們能夠只操做新的對象達到響應式目的

function reactive(obj) {
  if (typeof obj !== 'object' && obj != null) {
    return obj
  }
  // Proxy至關於在對象外層加攔截
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      console.log(`獲取${key}:${res}`)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      console.log(`設置${key}:${value}`)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      console.log(`刪除${key}:${res}`)
      return res
    }
  })
  return observed
}

Proxy能夠直接監聽數組的變化(push、shift、splice)

const obj = [1,2,3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok
Proxy有多達13種攔截方法,不限於apply、ownKeys、deleteProperty、has等等,這是Object.defineProperty不具有的
正由於defineProperty自身的缺陷,致使Vue2在實現響應式過程須要實現其餘的方法輔助(如重寫數組方法、增長額外set、delete方法)
// 數組重寫
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  arrayProto[method] = function () {
    originalProto[method].apply(this.arguments)
    dep.notice()
  }
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

Proxy 不兼容IE,也沒有 polyfill, defineProperty 能支持到IE9

相關文章
相關標籤/搜索