<深入浅出Vuejs>变化侦测_Array

        上一篇介绍了关于Object的变化侦测,这篇介绍关于Array.

        我们在修改Array时,使用的是Array 的原型上的方法(Push,Pop,Shift等),而这些操作不会触发getter/setter.所以对于Object的侦测方法对Array行不通.而ES6之前,js并未提供元编程能力,故我们无法拦截原型方法,但我们可以用自定义方法去覆盖原型方法.

因此我们可以用一个拦截器去覆盖Array.prototype.拦截器原理如图:

 

1.Array拦截器:首先自定义arrayMethods,让其原型指向Array.prototype,并修改其对应methods中的方法.当调用arrayMethods的同名方法时,我们实际执行的函数时mutator方法,而其最后调用Array.prototype的原生方法,而在此之前,我们可以加入自己的逻辑,比如说发送变化通知等等.

// 会改变数组的方法
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

const arrayProto = Array.prototype   // 复制一个Array.prototype对象
const arrayMethods = Object.create(arrayProto) // 这个是我们自己定义的原型对象

// 循环arr,重写arr中的相关方法
methods.forEach((method) => {
  // 先保存原始方法
  const original = arrayProto[method]

  // 在新的原型对象中重写自己的方法
  Object.defineProperty(arrayMethods, method, {
    value: function mutator (...args) {
      // 在执行原始方法前可以加入我们自己的逻辑
      
      // 返回原始方法的调用
      return original.apply(this, args)
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})

        有了拦截器,我们不能直接覆盖Array.prototype,因为这样会污染全局的Array,也就是说我们只希望这个拦截器去覆盖那些响应式数组的原型.而我们是通过Observer类来将数据转换为响应式数据(详见Object变化侦测),所以我们只需要在Observer扫描到数组时用拦截器覆盖其原型即可.

export default class Observer {
	// 参数就是需要侦测的数据。首次进来参数肯定是vue的data对象无疑
	constructor (data) {
		this.value = data

		if (Array.isArray(this.value)) {
			// 我们这次新增的代码,改变原型指向
			this.value.__proto__ = arrayMethods
		} else {
			// 如果不是数组,必然就是对象
			this.walk(this.value) // vue内部用来递归劫持数据的方法
		}
	}

	walk (obj) {
		const keys = Object.keys(obj)
		for (let i = 0; i < keys.length; i++) {
			defineReactive(obj, keys[i], obj[keys[i]])
		}
	}
}

        但是现在问题是,.__proto__并不是所有浏览器都支持的,如果不支持的情况下怎么办。很简单,如果不知道,我们就直接把相关方法写入到该数组中去。根据原型链原理,数组调用某个方法时,优先调用自己的方法,如果不存在此方法,才会去原型对象中找。

        

  // 判断是否支持.__proto__属性
  const hasProto = '__proto__' in {}
  // 获取原型对象下所有方法名的集合
  const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

 /**
  * @description 定义数组自己的方法
  * @Params obj: 需要劫持的数组对象,key:需要设置的属性名,如push。 val: obj.push的值,是一个function
  * @Author chen 
  */
  function def (obj, key, val) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: false,
      writable: true,
      configurable: true
    })
  }

  /** 
   * @description 浏览器支持__proto__属性时的执行函数
   * @Params target: 需要劫持的数组, src: 复制的原型对象arrayMethods keys: arrayMethods中的方法名集合
   * @Author chen
   */
  function protoAugment (target, src, keys) {
    target.__proto__ = src
  }

  /** 
   * @description 浏览器不支持__proto__属性时的执行函数
   * @Params target: 需要劫持的数组, src: 复制的原型对象arrayMethods keys: arrayMethods中的方法名集合
   * @Author chen
   */
  function copyAugment (target, src, keys) {
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      def(target, key, src[key])
    }
  }

  // 将需要被劫持的数组的.__proto__属性指向我们自己定义的原型对象arrayMethods
  export default class Observer {
    // 参数就是需要被劫持(侦测)的数据。首次进来参数肯定是vue的data对象无疑
    constructor (data) {
      this.value = data

      if (Array.isArray(this.value)) {
      	// 更改的地方
        const runFun = hasProto ? protoAugment : copyAugment
        // 执行具体函数,目的反正都是重写数组方法
        runFun(value, arrayMethods, arrayKeys)
      } else {
        // 如果不是数组,必然就是对象
        this.walk(this.value) // vue内部用来递归劫持数据的方法
      }
    }

    walk (obj) {
      const keys = Object.keys(obj)
      for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i], obj[keys[i]])
      }
    }
  }


2.收集依赖 :

        数组收集依赖的方法与Objcet一样,都是在getter中收集的.所以,Array在getter中收集依赖,在拦截器中触发依赖.

        但依赖应该存储在哪呢?vue把数组的Dep存储在Observer实例中.因为这样可以保证无论是getter还是拦截器都可以访问到这个Dep.

  // 将需要被侦测的数组的.__proto__属性指向我们自己定义的原型对象arrayMethods
  export default class Observer {
    // 参数就是需要侦测的数据。首次进来参数肯定是vue的data对象无疑
    constructor (data) {
      this.value = data
	  // 新增dep
	  this.dep = new Dep()
	  // 此函数得作用是在data(需要劫持得数组)下新增一个__ob__属性, 属性值是this(当前this就是new Observer出来得oberver实例)。同时在data下新增一个这样得属性,还可以作为当前数据是否已经是响应式数据得一个标识,如果是响应式数据,那么肯定就会有这个属性
	  def(data, '__ob__', this)
      if (Array.isArray(this.value)) {
        const runFun = hasProto ? protoAugment : copyAugment
        // 执行具体函数,目的反正都是重写数组方法
        runFun(value, arrayMethods, arrayKeys)
      } else {
        // 如果不是数组,必然就是对象
        this.walk(this.value) // vue内部用来递归劫持数据的方法
      }
    }

    walk (obj) {
      const keys = Object.keys(obj)
      for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i], obj[keys[i]])
      }
    }
  }
 
 /**
   * @description 用于返回当前observer实例,如果当前val存在__ob__属性,说明是响应式数	  据,直接返回val的__ob__的值
   * 否则说明是非响应式数据,也就是还没有被劫持过。那么直接返回new Observer()
   * @Author chen  
   */
  function observe (val) {
    let ob = ''
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof of Observer) {
      ob = value.__ob__
    } else {
      ob = new Observer()
    }
    return ob
  }

  function defineReactive(data, key, val) {
    //如果是数组返回数组的observer实例,是对象则递归侦测
	if (val.toString() === '[Object Array]') {
		let childObserver = observe(val)
	} else if (val.toString() === '[Object Object]') {
		new Observer(val)
	}
	let dep = new Dep()
	Object.defineProperty(data, key, function() {
		enumerable: true, // 该属性是否可被枚举
		configurable: true, // 该属性的配置后续是否可进行更改
		get: function () { // 用于获取属性值
			// 收集依赖
			if (window.target) {
				dep.addSub(window.target)
				
				// 新增数组的依赖
				if (childObserver) {
					childObserver.dep.addSub(window.target)
				}
			}
			return val
		},
		set: function (newVal) { // 设置属性值
			if (newVal === val) return
			// 用新值替换旧值
			val = newVal
			// 数据发生变化时,通知依赖者
			dep.notify()
		}
	})
}

        此时,如此一来,我们可以通过被侦测数组的__ob__属性来获取对应于它的Observer实例,再通过observer.dep来获取对应于它的Dep.对该dep进行添加或循环通知信息.

        修改后的拦截器方法如下:

// 循环arr,重写arr中的相关方法
methods.forEach((method) => {
  // 先缓存原始方法
  const original = arrayProto[method]

  // 在新的原型对象中重写自己的方法
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      //发送更新通知,这里的this指代调用方法的array
      this.__ob__.dep.notify()
      
      // 返回原始方法的调用结果
      return original.apply(this, args)
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})

3.侦测数组中元素变化 :无论是Object还是Array,响应式数据的子数据都要被侦测.

function observeArray (items) {
	for (let item of items) {
		observe(item)
	}
}

//然后在Observer构造器关于数组的逻辑中编写
if (Array.isArray(this.value)) {
        // 对每一项都执行new Observer()
        this.observeArray(this.value)
      	// 更改的地方
        const runFun = hasProto ? protoAugment : copyAugment
        // 执行具体函数,目的反正都是重写数组方法
        runFun(value, arrayMethods, arrayKeys)
      } else {
        // 如果不是数组,必然就是对象
        this.walk(this.value) // vue内部用来递归劫持数据的方法
      }

而对于数组来说,有三个新增操作push,unshift,splice,我们只需要在拦截器中对这三种情况进行单独处理即可:

 Object.defineProperty(arrayMethods, method, {
   value: function myMethod (...args) {
      let inserted // 用来存放新增的元素
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
        default:
          break
      }
      if (inserted) {
        // this.__ob__获取到当前observer实例,然后用这个实例调用observerArray方法,对这个对象以及他的子级进行数据侦测
        this.__ob__.observeArray(inserted)
      }
     // 这里可以发送更新通知
     this.__ob__.dep.notify()
     // 返回原始方法的调用结果
     return original.apply(this, args)
   },
   enumerable: false,
   writable: true,
   configurable: true
 })
})

PS 关于Array的一些问题 

        对Array的数据侦测是通过拦截原型方法实现的,而正是因为这种方式,我们无法侦测到一些数据操作所造成的数组变化,如 this.arr[0] = 2,this.arr.length = 0 等.

        本文章仅为本人学习总结,如果有不足还请各位指出!

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值