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'
复制代码
相关文章
相关标签/搜索