【深入浅出Vue.js篇】之Array的变化侦测

【深入浅出Vue.js篇】之Array的变化侦测



前言

在上一篇文章中我们使用Object.defineProperty实现了Object的变化侦测,但是Array在触发push方法的时候就不能通过getter/setter来实现侦测了,那么Array的变化侦测要怎么实现呢,看完这篇文章你就会知道啦。


提示:以下是本篇文章正文内容,下面案例可供参考

一、如何追踪变化?

Object的变化是靠setter来追踪的,只要数据发生了变化就可以触发setter方法。

那么Array的值变化的时候要怎么追踪呢?我们在用户调用操作数组方法(push,pop…)的时候做点什么,比如拦截一下是不是就可以实现数据侦测了呢?接下来我们就浅试一下。

二、拦截器

Array 原型中可以改变数组自身内容的方法有7个,分别是push 、pop 、shift 、unshift 、splice 、sort 和reverse 。

代码如下(示例):

// 拦截器
const arrayProto = Array.prototype
// 创建 arrayMethods 继承array 的原型
const arrayMethods = Object.create(arrayProto)
['push', 'shift', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => {
  const original = arrayProto[method]
   // Object.defineProperty(arrayMethods, method, {
  //   configurable: true,
  //   enumerable: false,
  //   writable: true,
  //   // 在操作数组时,调用的是mutator函数,里面可以做一些其他事,例如来发送变化通知等等。
  //   value: function metator (...args) {
  //     return original.apply(this, args) 
  //   }
  // })
  //工具函数
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__

    // 侦测新元素变化
    // 1.如果method 是push 、unshift 、splice 这种可以新增数组元素的方法
    //那么从args 中将新增元素取出来,暂存在inserted 中。
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'unshift':
        inserted = args.slice(2)
        break;
    }

    // 如果有新增元素,则使用ob.observeArray 来侦测这些新增元素的变化。
    if(inserted)ob.observeArray(inserted)

    ob.dep.notify() //向依赖(Watcher )发送消息
    return result
  })
})

在上面的代码中,我们创建了变量arrayMethods ,它继承自Array.prototype ,具备其所有功能。未来,我们要使用arrayMethods 去覆盖Array.prototype 。

接下来,在arrayMethods 上使用Object.defineProperty 方法将那些可以改变数组自身内容的方法(push 、pop 、shift 、unshift 、splice 、sort 和reverse )进行封装

所以,当使用push 方法的时候,其实调用的是arrayMethods.push ,而arrayMethods.push 是函数mutator ,也就是说,实际上执行的是mutator 函数。

最后,在mutator 中执行original (它是原生Array.prototype 上的方法,例如Array.prototype.push )来做它应该做的事,比如push 的功能。

三.使用拦截器覆盖Array原型

有了拦截器,想要让他生效,那就需要他覆盖掉Arraay.prototype,但这样显然是不可行的,因为直接覆盖会影响到全局的Array。

之前我们要将一个数据变成响应式数据,是通过Observer,同理 我们只需要在Observer中将Array的原型覆盖掉就好啦。

同时我们还要注意一下 不是所有的浏览器都支持 ‘__ proto __’,因此当浏览器不支持的时候我们不能直接覆盖,而是通过将拦截器挂载到vlaue上

代码如下(示例):

// __proto__是否可用
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer{
  constructor(value) {
    this.value = value
	this.dep = new Dep()
    // 新增一个不可枚举的属性 __ob__ 
    // 所有被侦测了变化的数据身上都会有一个 __ob__ 属性来表示它们是响应式的
    def(value,'__ob__',this)
    
    if (Array.isArray(value)) {
      // 覆盖原型的功能
      // 判断浏览器是否支持__proto__
      //const augment = hasProto ? protoAugment : copyAugment;
      //augment(value,arrayMethods,arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}

/**
 * 循环Array 中的每一项,执行observe 函数来侦测变化
 * 将数组中的每个元素都执行一遍new Observer
 */
function observeArray (items) {
  for (let i = 0; i < items.length; i++){
    observe(items[i])
  }
}
// 覆盖原型
function protoAugment (target, src, keys) {
  target.__proto__ = src
}
// 将拦截器挂载到vlaue上
function copyAugment (target, src, keys) {
  for (let i = 0; i < keys.length; i++){
    const key = keys[i]
    def(target,key,src[key])
  }
}

工具函数

function def (obj,key,val,enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable:true
    })
}

当value 身上被标记了 ob 之后,就可以通过value.ob 来访问Observer 实例。如果是Array 拦截器,因为拦截器是原型方法,所以可以直接通过this.ob 来访问Observer 实例

四.收集依赖

看过上一篇的人都知道,在实现Object数据侦测时,我们将依赖收集在Dep中,同理Array的依赖也同样是存放在Dep中的。

也就是说我们接下来要实现的就是在getter中收集依赖 并将依赖统一存放在Dep中进行管理。

function defineReactive (data, key, val) {
  // Observer 实例
  let childOb = observe(val)
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: TreeWalker,
    get: function () {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
      }
      return val
    },
    set: function (newValue) {
      if (val === newValue) {
        return
      }
      // 通知
      dep.notify()
      val == newValue
    }
  })
}

/**
 * 尝试为value创建一个Observer实例
 * 如果创建成功,直接返回创建的Observe实例
 * 如果value已经存在一个Observer实例,则直接返回它
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

总结

Array 追踪变化的方式和Object 不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化。

为了不污染全局Array.prototype ,我们在Observer 中只针对那些需要侦测变化的数组使用 __ proto __ 来覆盖原型方法,但 __ proto __ 在ES6之前并不是标准属性,不是所有浏览器都支持它。因此,针对不支持 __ proto __ 属性的浏览器,我们直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype 上的原生方法。

Array 收集依赖的方式和Object 一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object 那样保存在defineReactive 中,而是把依赖保存在了Observer 实例上。

在Observer 中,我们对每个侦测了变化的数据都标上印记 ob ,并把this (Observer 实例)保存在 ob 上。这主要有两个作用,一方面是为了标记数据是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以很方便地通过数据取到 ob ,从而拿到Observer 实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。

除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。我们在Observer 中判断如果当前被侦测的数据是数组,则调用observeArray 方法将数组中的每一个元素都转换成响应式的并侦测变化。

除了侦测已有数据外,当用户使用push 等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push 、unshift 和splice 方法,则从参数中将新增数据提取出来,然后使用observeArray 对新增数据进行变化侦测。

由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用length 清空数组的操作就无法拦截。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值