爲何Vue3.0使用Proxy實現數據監聽?defineProperty表示不背這個鍋

導 讀

vue3.0中,響應式數據部分棄用了 Object.defineProperty,使用 Proxy 來代替它。本文將主要經過如下方面來分析爲何vue選擇棄用 Object.definePropertyjavascript

  1. Object.defineProperty 真的沒法監測數組下標的變化嗎?
  2. 分析vue2.x中對數組 Observe 部分源碼
  3. 對比 Object.definePropertyProxy

1、沒法監控到數組下標的變化?

在一些技術博客上看到過這樣一種說法,認爲 Object.defineProperty 有一個缺陷是沒法監聽數組變化:前端

沒法監控到數組下標的變化,致使直接經過數組的下標給數組設置值,不能實時響應。因此vue才設置了7個變異數組(pushpopshiftunshiftsplicesortreverse)的 hack 方法來解決問題。vue

Object.defineProperty 的第一個缺陷,沒法監聽數組變化。 然而Vue的文檔提到了Vue是能夠檢測到數組變化的,可是隻有如下八種方法, vm.items[indexOfItem] = newValue 這種是沒法檢測的。java

這種說法是有問題的,事實上,Object.defineProperty 自己是能夠監控到數組下標的變化的,只是在 Vue 的實現中,從性能/體驗的性價比考慮,放棄了這個特性。git

下面咱們經過一個例子來爲 Object.defineProperty 正名:es6

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} value: ${newVal}`)
      value = newVal
    }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key])
  })
}

let arr = [1, 2, 3]
observe(arr)
複製代碼

上面代碼對數組arr的每一個屬性經過 Object.defineProperty 進行劫持,下面咱們對數組arr進行操做,看看哪些行爲會觸發數組的 gettersetter 方法。github

1. 經過下標獲取某個元素和修改某個元素的值

能夠看到,經過下標獲取某個元素會觸發 getter 方法, 設置某個值會觸發 setter 方法。

接下來,咱們再試一下數組的一些操做方法,看看是否會觸發。segmentfault

2. 數組的 push 方法

push 並未觸發 settergetter 方法,數組的下標能夠看作是對象中的 key ,這裏 push 以後至關於增長了下索引爲3的元素,可是並未對新的下標進行 observe ,因此不會觸發。數組

3. 數組的 unshift 方法

我擦,發生了什麼?瀏覽器

unshift 操做會致使原來索引爲0,1,2,3的值發生變化,這就須要將原來索引爲0,1,2,3的值取出來,而後從新賦值,因此取值的過程觸發了 getter ,賦值時觸發了 setter

下面咱們嘗試經過索引獲取一下對應的元素:

只有索引爲0,1,2的屬性纔會觸發 getter

這裏咱們能夠對比對象來看,arr數組初始值爲[1, 2, 3],即只對索引爲0,1,2執行了 observe 方法,因此不管後來數組的長度發生怎樣的變化,依然只有索引爲0,1,2的元素髮生變化纔會觸發,其餘的新增索引,就至關於對象中新增的屬性,須要再手動 observe 才能夠。

4. 數組的 pop 方法

當移除的元素爲引用爲2的元素時,會觸發 getter

刪除了索引爲2的元素後,再去修改或獲取它的值時,不會再觸發 settergetter

這和對象的處理是一樣的,數組的索引被刪除後,就至關於對象的屬性被刪除同樣,不會再去觸發 observe

到這裏,咱們能夠簡單的總結一下結論。

Object.defineProperty 在數組中的表現和在對象中的表現是一致的,數組的索引就能夠看作是對象中的 key

  1. 經過索引訪問或設置對應元素的值時,能夠觸發 gettersetter 方法
  2. 經過 pushunshift 會增長索引,對於新增長的屬性,須要再手動初始化才能被 observe
  3. 經過 popshift 刪除元素,會刪除並更新索引,也會觸發 settergetter 方法。

因此,Object.defineProperty 是有監控數組下標變化的能力的,只是vue2.x放棄了這個特性。

2、vue對數組的observe作了哪些處理?

vue的 Observer 類定義在 core/observer/index.js 中。

能夠看到,vue的 Observer 對數組作了單獨的處理。

hasProto 是判斷數組的實例是否有 __proto__ 屬性,若是有 __proto__ 屬性就會執行 protoAugment 方法,將 arrayMethods 重寫到原型上。 hasProto 定義以下。

arrayMethods 是對數組的方法進行重寫,定義在 core/observer/array.js 中, 下面是這部分源碼的分析。

/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */

import { def } from '../util/index'

// 複製數組構造函數的原型,Array.prototype也是一個數組。
const arrayProto = Array.prototype
// 建立對象,對象的__proto__指向arrayProto,因此arrayMethods的__proto__包含數組的全部方法。
export const arrayMethods = Object.create(arrayProto)

// 下面的數組是要進行重寫的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/** * Intercept mutating methods and emit events */
// 遍歷methodsToPatch數組,對其中的方法進行重寫
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // def方法定義在lang.js文件中,是經過object.defineProperty對屬性進行從新定義。
  // 即在arrayMethods中找到咱們要重寫的方法,對其進行從新定義
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      // 上面已經分析過,對於push,unshift會新增索引,因此須要手動observe
      case 'push':
      case 'unshift':
        inserted = args
        break
      // splice方法,若是傳入了第三個參數,也會有新增索引,因此也須要手動observe
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // push,unshift,splice三個方法觸發後,在這裏手動observe,其餘方法的變動會在當前的索引上進行更新,因此不須要再執行ob.observeArray
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
複製代碼

三 Object.defineProperty VS Proxy

上面已經知道 Object.defineProperty 對數組和對象的表現是一致的,那麼它和 Proxy 對比存在哪些優缺點呢?

1. Object.defineProperty只能劫持對象的屬性,而Proxy是直接代理對象。

因爲 Object.defineProperty 只能對屬性進行劫持,須要遍歷對象的每一個屬性。而 Proxy 能夠直接代理對象。

2. Object.defineProperty對新增屬性須要手動進行Observe。

因爲 Object.defineProperty 劫持的是對象的屬性,因此新增屬性時,須要從新遍歷對象,對其新增屬性再使用 Object.defineProperty 進行劫持。

也正是由於這個緣由,使用vue給 data 中的數組或對象新增屬性時,須要使用 vm.$set 才能保證新增的屬性也是響應式的。

下面看一下vue的 set 方法是如何實現的,set 方法定義在 core/observer/index.js ,下面是核心代碼。

/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 若是target是數組,且key是有效的數組索引,會調用數組的splice方法,
  // 咱們上面說過,數組的splice方法會被重寫,重寫的方法中會手動Observe
  // 因此vue的set方法,對於數組,就是直接調用重寫splice方法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 對於對象,若是key原本就是對象中的屬性,直接修改值就能夠觸發更新
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // vue的響應式對象中都會添加了__ob__屬性,因此能夠根據是否有__ob__屬性判斷是否爲響應式對象
  const ob = (target: any).__ob__
  // 若是不是響應式對象,直接賦值
  if (!ob) {
    target[key] = val
    return val
  }
  // 調用defineReactive給數據添加了 getter 和 setter,
  // 因此vue的set方法,對於響應式的對象,就會調用defineReactive從新定義響應式對象,defineReactive 函數
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

複製代碼

set 方法中,對 target 是數組和對象作了分別的處理,target 是數組時,會調用重寫過的 splice 方法進行手動 Observe

對於對象,若是 key 原本就是對象的屬性,則直接修改值觸發更新,不然調用 defineReactive 方法從新定義響應式對象。

若是採用 proxy 實現,Proxy 經過 set(target, propKey, value, receiver) 攔截對象屬性的設置,是能夠攔截到對象的新增屬性的。

不止如此,Proxy 對數組的方法也能夠監測到,不須要像上面vue2.x源碼中那樣進行 hack

完美!!!

3. Proxy支持13種攔截操做,這是defineProperty所不具備的

  • get(target, propKey, receiver):攔截對象屬性的讀取,好比 proxy.fooproxy['foo']

  • set(target, propKey, value, receiver):攔截對象屬性的設置,好比 proxy.foo = vproxy['foo'] = v ,返回一個布爾值。

  • has(target, propKey):攔截 propKey in proxy 的操做,返回一個布爾值。

  • deleteProperty(target, propKey):攔截 delete proxy[propKey] 的操做,返回一個布爾值。

  • ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in 循環,返回一個數組。該方法返回目標對象全部自身的屬性的屬性名,而 Object.keys() 的返回結果僅包括目標對象自身的可遍歷屬性。

  • getOwnPropertyDescriptor(target, propKey):攔截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回屬性的描述對象。

  • defineProperty(target, propKey, propDesc):攔截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs) ,返回一個布爾值。

  • preventExtensions(target):攔截 Object.preventExtensions(proxy) ,返回一個布爾值。

  • getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy) ,返回一個對象。

  • isExtensible(target):攔截 Object.isExtensible(proxy) ,返回一個布爾值。

  • setPrototypeOf(target, proto):攔截 Object.setPrototypeOf(proxy, proto) ,返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截。

  • apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比 proxy(...args)proxy.call(object, ...args)proxy.apply(...)

  • construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做,好比 new proxy(...args)

4. 新標準性能紅利

Proxy 做爲新標準,長遠來看,JS引擎會繼續優化 Proxy ,但 gettersetter 基本不會再有針對性優化。

5. Proxy兼容性差

能夠看到,Proxy 對於IE瀏覽器來講簡直是災難。

而且目前並無一個完整支持 Proxy 全部攔截方法的Polyfill方案,有一個google編寫的 proxy-polyfill 也只支持了 get,set,apply,construct 四種攔截,能夠支持到IE9+和Safari 6+。

四 總結

  1. Object.defineProperty 對數組和對象的表現一直,並不是不能監控數組下標的變化,vue2.x中沒法經過數組索引來實現響應式數據的自動更新是vue自己的設計致使的,不是 defineProperty 的鍋。

  2. Object.definePropertyProxy 本質差異是,defineProperty 只能對屬性進行劫持,新增屬性須要手動 Observe 的問題。

  3. Proxy 做爲新標準,瀏覽器廠商勢必會對其進行持續優化,但它的兼容性也是塊硬傷,而且目前尚未完整的polifill方案。

參考

developer.mozilla.org/zh-CN/docs/…

segmentfault.com/a/119000001…

zhuanlan.zhihu.com/p/35080324

es6.ruanyifeng.com/#docs/proxy

歡迎關注個人公衆號「前端小苑」,我會按期在上面更新原創文章。

相關文章
相關標籤/搜索