如何監聽數組變化?

起源:在 Vue 的數據綁定中會對一個對象屬性的變化進行監聽,而且經過依賴收集作出相應的視圖更新等等。javascript

問題:一個對象全部類型的屬性變化都能被監聽到嗎?vue

以前用 Object.defineProperty經過對象的 getter/setter簡單的實現了對象屬性變化的監聽,而且去經過依賴關係去作相應的依賴處理。java

可是,這是存在問題的,尤爲是當對象中某個屬性的值是數組的時候。正如 Vue 文檔所說:git

因爲 JavaScript 的限制,Vue 沒法檢測到如下數組變更:github

  1. 當你使用索引直接設置一項時,例如 vm.items[indexOfItem] = newValue
  2. 當你修改數組長度時,例如 vm.items.length = newLength

Vue 源碼中也能夠看到確實是對數組作了特殊處理的。緣由就是 ES5 及如下的版本沒法作到對數組的完美繼承數組

實驗一下?

用以前寫好的 observe作了一個簡單的實驗,以下:app

import { observe } from './mvvm'
const data = {
  name: 'Jiang',
  userInfo: {
    gender: 0
  },
  list: []
}
// 此處直接使用了前面寫好的 getter/setter
observe(data)
data.name = 'Solo'
data.userInfo.gender = 1
data.list.push(1)
console.log(data)
複製代碼

結果是這樣的:mvvm

從結果能夠看出問題所在,data中 name、userInfo、list 屬性的值均發生了變化,可是數組 list 的變化並無被 observe監聽到。緣由是什麼呢?簡單來講,操做數組的方法,也就是 Array.prototype上掛載的方法並不能觸發該屬性的 setter,由於這個屬性並無作賦值操做。函數

如何解決這個問題?

Vue 中解決這個問題的方法,是將數組的經常使用方法進行重寫,經過包裝以後的數組方法就可以去在調用的時候被監聽到。測試

在這裏,我想的一種方法與它相似,大概就是經過原型鏈去攔截對數組的操做,從而實現對操做數組這個行爲的監聽。

實現以下:

// 讓 arrExtend 先繼承 Array 自己的全部屬性
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
/** * arrExtend 做爲一個攔截對象, 對其中的方法進行重寫 */
arrMethods.forEach(method => {
  const oldMethod = Array.prototype[method]
  const newMethod = function(...args) {
    oldMethod.apply(this, args)
    console.log(`${method}方法被執行了`)
  }
  arrExtend[method] = newMethod
})

export default {
  arrExtend
}
複製代碼

須要在 defineReactive 函數中添加的代碼爲:

if (Array.isArray(value)) {
    value.__proto__ = arrExtend
 }
複製代碼

測試一下:data.list.push(1)

咱們看看結果:

上面代碼的邏輯一目瞭然,也是 Vue 中實現思路的簡化。將 arrExtend 這個對象做爲攔截器。首先讓這個對象繼承 Array 自己的全部屬性,這樣就不會影響到數組自己其餘屬性的使用,後面對相應的函數進行改寫,也就是在原方法調用後去通知其它相關依賴這個屬性發生了變化,這點和 Object.definePropertysetter所作的事情幾乎徹底同樣,惟一的區別是能夠細化到用戶到底作的是哪種操做,以及數組的長度是否變化等等。

還有什麼別的辦法嗎?

ES6 中咱們看到了一個讓人耳目一新的屬性——Proxy。咱們先看一下概念:

經過調用 new Proxy() ,你能夠建立一個代理用來替代另外一個對象(被稱爲目標),這個代理對目標對象進行了虛擬,所以該代理與該目標對象表面上能夠被看成同一個對象來對待。

代理容許你攔截在目標對象上的底層操做,而這本來是 JS 引擎的內部能力。攔截行爲使用了一個可以響應特定操做的函數(被稱爲陷阱)。

Proxy顧名思義,就是代理的意思,這是一個能讓咱們隨意玩弄對象的特性。當咱們,經過Proxy去對一個對象進行代理以後,咱們將獲得一個和被代理對象幾乎徹底同樣的對象,而且能夠對這個對象進行徹底的監控。

什麼叫徹底監控?Proxy所帶來的,是對底層操做的攔截。前面咱們在實現對對象監聽時使用了Object.defineProperty,這個實際上是 JS 提供給咱們的高級操做,也就是經過底層封裝以後暴露出來的方法。Proxy的強大之處在於,咱們能夠直接攔截對代理對象的底層操做。這樣咱們至關於從一個對象的底層操做開始實現對它的監聽。

改進一下咱們的代碼?

const createProxy = data => {
  if (typeof data === 'object' && data.toString() === '[object Object]') {
    for (let k in data) {
      if (typeof data[k] === 'object') {
        defineObjectReactive(data, k, data[k])
      } else {
        defineBasicReactive(data, k, data[k])
      }
    }
  }
}

function defineObjectReactive(obj, key, value) {
  // 遞歸
  createProxy(value)
  obj[key] = new Proxy(value, {
    set(target, property, val, receiver) {
      if (property !== 'length') {
        console.log('Set %s to %o', property, val)
      }
      return Reflect.set(target, property, val, receiver)
    }
  })
}

function defineBasicReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: false,
    get() {
      return value
    },
    set(newValue) {
      if (value === newValue) return
      console.log(`發現 ${key} 屬性 ${value} -> ${newValue}`)
      value = newValue
    }
  })
}

export default {
  createProxy
}
複製代碼

對於一個對象中的基礎類型的屬性,咱們仍是經過Object.defineProperty來實現響應式的屬性,由於這裏並不存在痛點,可是在實現對Object類型的屬性進行監聽的時候,我採用的是建立代理,由於咱們以前的痛點在於沒法去有效監聽數組的變化。當咱們使用這種改進方法以後,咱們不用像以前經過重寫數組的方法來實現對數組操做的監聽了,由於以前這種方法存在不少的侷限性,咱們不能覆蓋全部的數組操做,同時,咱們也不能響應到相似於data.array.length = 0這種操做。經過代理實現以後,一切都不同了。咱們能夠從底層就實現對數組的變化進行監聽。甚至能watch到數組長度的變化等等各類更加細節的東西。這無疑解決了很大的問題。

咱們調用一下剛纔的方法,試試看?

let data = {
  name: 'Jiang',
  userInfo: {
    gender: 0,
    movies: []
  },
  list: []
}
createProxy(data)

data.name = 'Solo'
data.userInfo.gender = 0
data.userInfo.movies.push('星際穿越')
data.list.push(1)
複製代碼

輸出爲:

結果很是完美~咱們實現了對對象全部屬性變化的監聽Proxy的騷操做還有不少不少,好比說將代理看成原型放到原型鏈上,這樣一來就能夠只對子類不含有的屬性進行監聽,很是的強大。Proxy能夠獲得更加普遍的應用,並且場景不少。這也是我第一次去使用,還須要多加鞏固( ;´Д`)

相關文章
相關標籤/搜索