美文网首页饥人谷技术博客
JavaScript 之实现响应式数据

JavaScript 之实现响应式数据

作者: 临安linan | 来源:发表于2019-11-08 20:03 被阅读0次

    更多个人博客:(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'
    

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

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

    相关文章

      网友评论

        本文标题:JavaScript 之实现响应式数据

        本文链接:https://www.haomeiwen.com/subject/gliwbctx.html