[关闭]
@frank-shaw 2019-11-30T02:18:38.000000Z 字数 2837 阅读 901

vue源码阅读(三):数组的响应式处理

vue 数据响应式 数组响应式

数组的响应式处理有何特殊之处

回答这个问题,实际上回答的是:Vue的数据响应式原理有什么限制?理解了Vue的数据响应式原理的限制,就可以很好回答这个问题。

在实际使用Vue的过程中,对于响应式Data的声明,必须将需要响应的各个属性都逐一在声明阶段写清楚。如果对于一个响应式的Object,动态增加了某一个属性,那么Vue是无法为该新增属性做响应式处理的。举个栗子:

  1. export default {
  2. data(){
  3. return {
  4. foo : 1,
  5. bar : {tau : "test"}
  6. }
  7. }
  8. //给data动态增加属性newFoo, 页面无法响应式
  9. this.data.newFoo = "123"
  10. //给bar动态增加属性tau2, 页面无法响应
  11. this.data.bar.tau2 = "tell me"

理解了这个限制,我们再来看看数组的情况。我们会经常对数组进行的操作是怎样的呢?

  1. var array1 = ['t1','t2','t3']
  2. //动态修改元素
  3. array1[0] = 'tt1'
  4. //动态增加元素
  5. array1[4] = 't5'
  6. //push方法增加数组元素
  7. array1.push("1234")
  8. //动态改变元素的长度
  9. array1.length = 100

这些常规操作,如果只是通过常规的拦截key、value的方式来进行数据响应式,那么明显无法完全覆盖。并且成本太高,设想一下:如果用户直接写一个array1[1000] = 'haha',Vue是否要为其1000个子元素(极有可能是都为null)都响应化处理呢?

另外,值得注意的是,之所以对象可以在data声明的时候写死,是因为页面操作中的对象属性基本上可以在编写的时候确定。但页面中数组的变化就不一样,其变化基本上无法预测。而JS语言中的数组对其子元素的属性不限制,于是更加凌乱。

那么,Vue是采用了什么方式来进行数组的数据响应化实现呢?

  1. 对于数组子元素,确定了可以触发数组子元素数据响应化的七个方法,其他数组操作无法触发数组子元素的数据响应化。(具体方法,见源码解析)
  2. 针对数组子元素内部,循环赋予新的Observer,进行新一轮的判断。

我们来接着上节课的内容,看源码。

源码解析

查看/src/observer/array.js,可以知道Vue对数组的七个方法进行了拦截处理:

  1. const arrayProto = Array.prototype
  2. //先克隆一份数组原型
  3. export const arrayMethods = Object.create(arrayProto)
  4. //七个变异方法
  5. const methodsToPatch = [
  6. 'push',
  7. 'pop',
  8. 'shift',
  9. 'unshift',
  10. 'splice',
  11. 'sort',
  12. 'reverse'
  13. ]
  14. /**
  15. * Intercept mutating methods and emit events
  16. */
  17. methodsToPatch.forEach(function (method) {
  18. // cache original method
  19. const original = arrayProto[method]
  20. def(arrayMethods, method, function mutator (...args) {
  21. //执行原始方法
  22. const result = original.apply(this, args)
  23. //额外通知变更,当然,只有这7个方法才会有这个待遇
  24. const ob = this.__ob__
  25. let inserted
  26. switch (method) {
  27. case 'push':
  28. case 'unshift':
  29. inserted = args
  30. break
  31. case 'splice':
  32. inserted = args.slice(2)
  33. break
  34. }
  35. //对新加入对象进行响应化处理
  36. if (inserted) ob.observeArray(inserted)
  37. // notify change
  38. //此处通知,可以知道数组更新行为
  39. ob.dep.notify()
  40. return result
  41. })
  42. })

我们来看,在定义Observer的时候,如何处理数组:

  1. import { arrayMethods } from './array'
  2. const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
  3. export class Observer {
  4. value: any;
  5. dep: Dep;
  6. vmCount: number; // number of vms that have this object as root $data
  7. //值得留意的是:Observer对象在一个Vue实例中是存在多个的,取决于data数据中嵌套了几个Object对象或数组对象
  8. constructor (value: any) {
  9. this.value = value
  10. this.dep = new Dep()
  11. this.vmCount = 0
  12. def(value, '__ob__', this)
  13. //如果是数组
  14. if (Array.isArray(value)) {
  15. //如果能够使用原型特性,直接将变异方法赋予响应化数组的原型链上
  16. if (hasProto) {
  17. protoAugment(value, arrayMethods)
  18. } else {
  19. //如果无法使用原型,那么通过defineProperty的方式将变异方法赋予响应化数组
  20. copyAugment(value, arrayMethods, arrayKeys)
  21. }
  22. //接着,对数组子元素,进行新一轮的observe数据响应化的过程
  23. this.observeArray(value)
  24. } else {
  25. //如果是对象
  26. this.walk(value)
  27. }
  28. observeArray (items: Array<any>) {
  29. for (let i = 0, l = items.length; i < l; i++) {
  30. observe(items[i])
  31. }
  32. }
  33. }
  34. function protoAugment (target, src: Object) {
  35. target.__proto__ = src
  36. }
  37. function copyAugment (target: Object, src: Object, keys: Array<string>) {
  38. for (let i = 0, l = keys.length; i < l; i++) {
  39. const key = keys[i]
  40. def(target, key, src[key])
  41. }
  42. }

这个过程并不难看懂。至此,我们可以回答这个问题:

  1. data: {
  2. obj: {foo: 'foo'}
  3. bar: ['tua', 'tea']
  4. }
  5. //下面的这个操作,是否会触发数据响应化过程呢?
  6. this.bar[0] = 'testNew';

号外

我们了解了Vue数据响应化的这些限制,但我们如果又想要动态更新数组的属性,或者动态增加已有对象的新属性,该怎么办呢? Vue给我们提供了可用的api: Vue.set()Vue.delete()方法。问题不是如何使用该api,而是想要了解其内部源码是怎样的?

想了解更多,可以通过

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注