如何监听数组变化?

起源:在 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能够获得更加普遍的应用,并且场景不少。这也是我第一次去使用,还须要多加巩固( ;´Д`)

相关文章
相关标签/搜索