JavaScript 之實現響應式數據

數據響應式:

顧名思義,數據響應式就是當咱們修改數據時,能夠監聽到這個修改,而且做出相應的響應。javascript

一. 監測 Object 對象

需求:當咱們修改 obj 對象時,觸發 update 方法。java

思路:使用 Object.defineProperty 對數據進行劫持,每次修改的時候都會執行 set 方法,在 set 內部能夠進行響應更新api

編寫初版代碼:數組

function isObject(obj){
  return obj.constructor === Object
}

function update(){  // 更新響應
  console.log('updated!')
}

function observer(obj){ // 監測對象
  if(!isObject(obj)) return
  for(let key in obj){  // 對每一個屬性進行 Object.defineProperty 定義
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value){ // 數據劫持
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){  // 修改時,觸發 update 方法
      update()
      value = newValue
    }
  })
}

let obj = {a: 1}
observer(obj)
obj.a = 3 // updated! 
複製代碼

當咱們修改 obj 中經過 Object.defineProperty 定義的屬性時,會觸發 set 方法,觸發更新。app

初版編寫完成,已經實現了基礎功能,可是有兩個問題:ui

  1. 對於形如 {a: {b: 1}} 嵌套的對象,沒法進行任意深度的監測,由於沒法知道對象嵌套了幾層,只能用遞歸進行監測。this

  2. 修改的後值若是是一個對象,須要對這個對象也進行監測spa

obj.a = {c: 1}
obj.a.c = 3 // expected: updated!
複製代碼

咱們對 defineReactive 進行一點修改便可:prototype

function defineReactive(obj, key, value){
  observer(value) // 利用遞歸深度劫持:若是 value 仍是對象,繼續定義,直到 isObject 返回 false
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){
      if(isObject(newValue)){ // 若是新值爲對象,對新值進行進行數據監測
        observer(newValue)
      }
      update()
      value = newValue
    }
  })
}
複製代碼

至此,咱們實現了對對象數據的監測,當修改對象上的屬性時,能夠觸發響應,而且這個對象能夠是任意嵌套深度的,修改的新值也能夠是任意深度嵌套的對象。代理

不足之處:給對象新增一個不存在的屬性時,沒法觸發響應。

二. 監測數組

需求:當咱們使用 push pop shift unshift reverse sort splice 方法修改數組時,會觸發更新。

數組不能像對象那樣用 Object.defineProperty 劫持修改,因此咱們只能在上面說的這些方法上面下手,咱們能夠對這些方法進行重寫。

可是要注意的是:重寫不能夠對使用這些 api 的其餘地方產生影響

這裏咱們建立一個新的 Array 原型,而後改變須要監測的數組的原型,指向新的原型 ResponsiveArray

const ResponsiveArray = Object.create(Array.prototype);  // 建立新的 Array 原型
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  // 對每一個方法進行重寫,掛載到 ResponsiveArray 上 
  ResponsiveArray[method] = function() {
    update()
    Array.prototype[method].apply(this, arguments)
  }
})

function observer(obj){
  if(Array.isArray(obj)){
    return Object.setPrototypeOf(obj, ResponsiveArray) // 改變原型
  }
}

function update(){
  console.log('updated!')
}

let arr = [1,2,3,4]
observer(arr)
arr.push(1,2,3) // updated!
複製代碼

以上,就實現了對普通對象和數組的監測。完整代碼以下:

// 建立新的 Array 原型
const ResponsiveArray = Object.create(Array.prototype);

// 在新原型上重寫數組方法
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  ResponsiveArray[method] = function() {
    update()
    Array.prototype[method].apply(this, arguments)
  }
})

function update(){
  console.log('updated!')
}

function isObject(obj){
  return obj.constructor === Object
}

function observer(obj){
  if(Array.isArray(obj)){
    return Object.setPrototypeOf(obj, ResponsiveArray)  // 改變數組的原型
  }
  if(!isObject(obj)) return
  for(let key in obj){ // 對普通對象的每一個屬性進行監測
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value){// 數據劫持
  observer(value) // 遞歸調用,使得任意深度的對象能夠被監測到
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){
      if(isObject(newValue)){ // 對修改後爲對象的新值進行監測
        observer(newValue)
      }
      update()
      value = newValue
    }
  })
}
複製代碼

三. 利用 proxy 進行代理

function update(){
  console.log('updated')
}

let obj = [1,2,3]

const proxyObj = new Proxy(obj, {
  set(target, key, value){
    if(key === 'length') return true  // ①
    update()
    return Reflect.set(target, key, value)
  },
  get(target, key){
    return Reflect.get(target, key)
  }
})

proxyObj.push(12)
proxyObj[1] = 'xxx'
複製代碼

與 defineProperty 的區別:

  1. 能夠對添加新屬性進行代理
  2. 無需額外操做便可對數組進行代理,包括 push pop 等方法,以及修改指定索引的元素

須要注意的點是:修改數組元素時,除了插入元素以外,還會修改 length 屬性,觸發兩次更新,若是想避免修改 length 觸發更新,能夠加上上面的①,對 length 的修改進行過濾。

但不足的是:此時不能實現任意嵌套深度的對象的代理。

由於對於形如 proxyObj.a.b = 1 的語句,首先會返回 proxyObj.a對返回值上的 b 進行修改,沒有通過代理,因此也不會觸發更新

因此咱們只須要在返回的時候,返回通過 proxy 代理的值便可。

const handler = {
  set(target, key, value){
    if(key === 'length') return true
    update()
    return Reflect.set(target, key, value)
  },
  get(target, key){
    if(typeof target[key] === 'object'){
      return new Proxy(target[key], handler)  // 只要獲取的是對象,就返回通過代理後的對象。
    }
    return Reflect.get(target, key)
  }
}

let proxyObj = new Proxy(obj, handler)
proxyObj.b.c = 'xxx'
複製代碼
相關文章
相關標籤/搜索