之前有写了 一篇博客 ,给出了,通过数组下标给数组添加元素、修改数组长度或者直接对对象的属性的添加或删除,但是数据没变成响应式解决方法,并且讲解了为什么 vue3.0 不再使用 Object.defineProperty 而使用 Proxy 的实现依赖收集和派发更新原因 ,那么就从源码角度分析下,为什么上述操作对象或者数组,数据没有变成响应式的,但可以通过 Vue.set 或者 this.$set…

之前有写了一篇博客,给出了,通过数组下标给数组添加元素、修改数组长度或者直接对对象的属性的添加或删除,但是数据没变成响应式解决方法,并且讲解了为什么vue3.0 不再使用 Object.defineProperty 而使用 Proxy 的实现依赖收集和派发更新原因,那么就从源码角度分析下,为什么上述操作对象或者数组,数据没有变成响应式的,但可以通过 Vue.set 或者 this.$set 变成响应式。

Vue.set 都做了什么

  1. Vue.set(target, key, val),target 是数组或者对象,key 是数组下标或者这对象的键值,val 是添加的值或修改的值
  2. 如果 target 是数组,并且 key 是一个合法的下标,那么就触发 vue 自己封装的 splice 方法,给 target 添加这个值,然后内部获取到这个值,把这个值变成响应式对象,然后通过 ob.dep.notify()手动触发依赖更新
  3. 如果 target 是对象,如果 key 已经在 target 中,直接赋值返回,因为此时的变化是可以检测到的
  4. 如果 target 是对象,如果检测到 target 的__ob__不存在,就证明 target 不是响应式对象,直接赋值返回,
  5. 如果上述条件都不满足,通过 defineReactive 把新添加的属性变成响应式对象,再通过 ob.dep.notify()手动触发依赖更新
  6. 如果 target 是对象,并且 key 是一个数组,就对数组的每一个元素进行依赖收集
  7. defineReactive 执行了 observe(),observe()方法会实例化 Observer,Observer 判断如果新插入的值是数组,就会把插入的值原型指向 arrayMethods,arrayMethods 是内置方法,会对能增加数组长度的 3 个方法 push、unshift、splice 做了封装,最终对插入的数组进行 ob.dep.notify()手动触发依赖更新
  8. 可以看到 Vue.set 操作数组,实际上触发的 splice 方法,只不过这个 splice 方法会最终还是通过 ob.dep.notify()手动触发依赖更新;如果 Vue.set 操作对象,先检测对象是否响应式,如果是响应式直接赋值,不是响应式并且 target.__ob__存在,就通过 vue 内置的 defineReactive 把新增的属性和变成响应式对象,通过 ob.dep.notify()手动触发依赖更新,如果新增的属性是一个数组,那么就对数组的每一个元素进行依赖收集
// src/core/observer/index.js
// set接收三个参数,target是数组或者对象,key是数组下标或者这对象的键值,val是添加的值或修改的值
function set(target, key, val) {
  // 首先判断数组,并且是一个合法的下标
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    // 则通过splice方法添加数组
    target.splice(key, 1, val);
    // 然后返回
    return val;
  }
  // 如果key已经在target中了,则直接赋值返回,因为这样的变化是可以检测到的
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 把target.__ob__赋给ob,__ob__是在Observer构造函数执行的时候初始化的,表示Observer是一个实例
  const ob = (target: any).__ob__;

  // 如果target是一个vue实例或者ob有vmCount,有vmCount代表是一个根data
  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;
  }
  // 如果target.__ob__不存在,就说明target不是一个响应式对象,直接赋值并返回
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 通过defineReactivev把新添加的属性变成响应式对象
  defineReactive(ob.value, key, val);
  // 然后通过ob.dep.notify()手动的触发依赖更新
  ob.dep.notify();
  return val;
}

defineReactive

把属性变成响应式

// src/core/observer/index.js
export function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  // 值是对象或者数组,值是一个observer
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          // 在getter中判断了childOb,调用了childOb.dep.depend() 收集了依赖,这就是为什么执行Vue.set的时候可以通过
          // ob.dep.notify() 能够通知到 watcher,从而新增的对象也可以检测到变化,如果对象新增的值为一个数组,那对数组的每一个元素也进行依赖收集
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    }
    // ...
  });
}

vue 中自定义的数组方法:splice

  1. Observer

    Observer 是一个类,作用是给对象属性添加 getter 和 setter

// src/core/observer/index.js
export class Observer {
  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    // 新插入的值如果是数组
    if (Array.isArray(value)) {
      // protoAugment方法直接把target.__proto__ = src,原型直接修改src
      // copyAugment方法先遍历keys,通过 Object.defineProperty 去定义它自身的属性值
      // 现代浏览器大部分执行protoAugment,所以就把value原型指向arrayMethods
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value);
      // 对于数组则会调用observeArray 方法
      // observeArray是遍历数组再次调用observe方法
      // observe 的作用是用来检测数据变化
    } else {
      // ...
    }
  }
}
  1. protoAugment 和 copyAugment
// src/core/observer/index.js
// protoAugment方法直接把target.__proto__ = src,在原型上直接修改src
function protoAugment(target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src;
  /* eslint-enable no-proto */
}

// copyAugment方法先遍历keys,通过 Object.defineProperty 去定义它自身的属性值
function copyAugment(target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}
  1. arrayMethods
//  src/core/observer/array.js
// arrayMethods先继承了Array,然后对能改变数组的本身的数组内置方法重写
// 对能增加数组长度的 3 个方法 push、unshift、splice做了判断
// 获取到插入的值,把新添加的值变成了一个响应式对象, ob.dep.notify()手动触发依赖更新,
import { def } from '../util/index';
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
/**
 * Intercept mutating methods and emit events
 */
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);
    // notify change
    ob.dep.notify();
    return result;
  });
});

暂无评论