4

Vue 响应式原理剖析 - 数据更新常见问题

 2 years ago
source link: https://www.fly63.com/article/detial/11267
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对象数据的某些修改无法被检听?

如下的一个场景, obj.message 赋值时能否被监听响应呢?

var vm = new vue({
    data: {
        obj: {
            a: 1
        },
    },
    template: '<div>{{ obj.message }}</div>'
});

vm.obj.message = 'modified';

答案是不能被监听的。原因:对象属性的添加和删除无法被 Object.defineProperty 监听,正如前文所述,Vue 的数据响应式基于 Object.defineProperty 实现,因此也受限。

解决办法:Vue 提供了特定的方法 vm.$set(obj, propertyName, newValue) 来处理这种情况,至于该方法的具体逻辑,后面会详细展开说明。

数组数据的某些修改无法被监听?

如下的一个场景,三个赋值语句里面,哪个能被监听响应呢?

const vm = new Vue({
    data: {
        items: [1, 2, 3, 4, 5],
    },
});
vm.items[1] = 8;
vm.items[5] = 6;
vm.items.length = 2;

答案是三个操作都不能被监听到。原因:

  • 第二个操作 vm.items[5] = 6 应该是比较明显的,与上面对象添加和删除属性类似,数组新添加的元素和删除元素无法被 Object.defineProperty 监听。
  • 第三个操作 vm.items.length = 2 也是由于 Object.defineProperty 的限制,数组的长度直接修改也无法被监听。
  • 最容易误判的可能是第一个操作 vm.items[1] = 8 ,一种较为常规的说法是 Object.defineProperty 不支持监听数组元素的变化,要验证这个说法可以直接用一个例子说明真实的情况。

如下的一个例子:

function definereactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('读取 index ' + key, '当前值是 ' + val)
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal
            console.log('修改 index ' + key, '新值是 ' + val)
        }
    })
}

const testArray = [1, 2, 3, 4, 5]

testArray.forEach((c, index) => {
    defineReactive(testArray, index, c)
});

testArray[0] = 100;
testArray[5] = 600;
62318b7adf00e.png

可以看到, 超出范围的数组元素操作 Object.defineProperty 确实无法监听,但范围内的元素重新赋值是可以被监听的。 那么为什么在 Vue 中对数组类型的 data ,直接使用下标赋值无法被监听呢?

答案是出于性能考虑,从上面的基础例子中可以看到, 对象和数组如果需要监听每个属性和元素,实际上是对每个属性或者元素进行 Object.defineProperty 劫持,对象是监听 key 而数组则是以数字下标作为 key ,数组的数据量可能会很大,因此 Vue 出于性能考虑,并没有对元素下标进行响应式处理。 作为补充,Vue 对数组原型链上的几个方法进行劫持,对于会导致元素新增的3个方法 push 、 pop 、 unshift 会在内部获取新增的元素,执行响应式的处理:

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  })
})

与对象类似,如果需要为数字元素重新赋值,可以使用 vm.$set(arr, indexOfItem, newValue) 方法,这里展示一下 $set 的具体实现:

// 精简了非 production 逻辑
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

主要的逻辑包括以下操作:

  1. 这个方法同时用于对象和数组,因此第一步会检验传入的 target 是否为数组,并且传入的 key (即数组的数字下标)是否符合数组的长度范围(如上面所述, Object.defineProperty 不支持劫持新添加的元素),符合的元素会调用 splice 插入到数组中,由于 splice 已经被劫持,新增加的元素会进行「响应式」处理。
  2. 判断如果 key 原先已存在,则无需再监听。
  3. 判断是根节点 Vue,即最外层的 Vue,或者已经有 __ob__ 属性(表示已经进行响应式处理,详情可以浏览前文),则无需再进行监听。
  4. 如果不符合前面的条件,则表明该属性需要执行「响应式」处理,会调用 defineReactive 方法(响应式数据封装的入口方法,详情可以浏览前文)。

原文 https://kayosite.com/vue-reactivity-common-problems.html

链接: https://www.fly63.com/article/detial/11267


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK