饥人谷技术博客

JavaScript 之实现响应式数据

2019-11-08  本文已影响0人  临安linan

更多个人博客:(https://github.com/zenglinan/blog)

如果对你有帮助,欢迎star。

数据响应式:

顾名思义,数据响应式就是当我们修改数据时,可以监听到这个修改,并且作出相应的响应。

一. 监测 Object 对象

需求:当我们修改 obj 对象时,触发 update 方法。

思路:使用 Object.defineProperty 对数据进行劫持,每次修改的时候都会执行 set 方法,在 set 内部可以进行响应更新

编写第一版代码:

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 方法,触发更新。

第一版编写完成,已经实现了基础功能,但是有两个问题:

  1. 对于形如 {a: {b: 1}} 嵌套的对象,无法进行任意深度的监测,因为无法知道对象嵌套了几层,只能用递归进行监测。

  2. 修改的后值如果是一个对象,需要对这个对象也进行监测

obj.a = {c: 1}
obj.a.c = 3 // expected: updated!

我们对 defineReactive 进行一点修改即可:

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'

感谢你看到了这里,更多个人博文戳这

本文正在参与“写编程博客瓜分千元现金”活动,关注公众号“饥人谷”回复“编程博客”参与活动。

上一篇下一篇

猜你喜欢

热点阅读