Vue2源码学习笔记之Set和Del

下面看下 set 函数,我们在扩展 Vue 的构造函数的原型对象时创建 Vue.prototype.$set 方法,然后在扩展 Vue 的构造函数的静态方法时创建 Vue.set 方法,这两个方法所赋值的函数都是 set 函数。 在 Vue

目录
  1. set函数
  2. del
  3. 参考资料

set函数#

下面看下set函数,我们在扩展 Vue 的构造函数的原型对象时创建Vue.prototype.$set方法,然后在扩展 Vue 的构造函数的静态方法时创建Vue.set方法,这两个方法所赋值的函数都是set函数。

在 Vue 的官方文档中,提供了这两个 API 供用户使用,用来向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性。另外,注意设置的对象不能是 Vue 实例或者 Vue 实例的根数据对象。

下面查看set函数的代码

export function set(target: Array<any> | Object, key: any, val: any): any {
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
    );
  }
  // ...
}

首先在非生产环境中,判断target的类型是不是未定义的值或者是一个原始类型的值,如果满足其中之一的话会报错,并提示不能设置nullundefined或者原始类型中的响应式属性。

继续看下面的代码。

// ! target 是数组且 key 有效时
if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key); // ! 修改数组长度 key, 否则大于原数组长度时 splice 无效
  target.splice(key, 1, val); // ! 使用重写的 splice 增加或者替换元素,并触发响应
  return val;
}

如果定义的响应式对象是数组时且它里面的key值是有效值时,先获取数组长度和key的中最大值,把它设置为target的长度。为什么要这样做呢?因为如果设置的key大于原来target的长度,使用splice是插入元素时,插入后的元素在数组中的位置会不正确,即索引不正确。

示例代码:

const arr = [1, 2, 3]; // length = 3
const key = 5;
arr.splice(key, 1, 4);
console.log(arr); // [ 1, 2, 3, 4 ]

上面代码中 key 是5,它比数组arr的长度3要大,把数字4传入到key的位置后,它的位置是错误的。

如果修改了target的长度为key和原来长度的最大值后,插入后的位置是不一样的。

示例代码:

const arr = [1, 2, 3];
const key = 5;
arr.length = Math.max(arr.length, key);
arr.splice(key, 1, 4);
console.log(arr); // [ 1, 2, 3, <2 empty items>, 4 ]

继续看下面的代码。

// ! key 已经存在对象 target 且不在它的原型中时
if (key in target && !(key in Object.prototype)) {
  target[key] = val; // ! 直接修改原来的值,会自动触发 setter
  return val;
}

设置的key已经存在于响应式对象中但不在它的原型对象上时,直接赋值即可,会自动触发setter,最后返回设置的新值。

继续看下面的代码。

// ! 获取原对象的 __ob__ 属性
const ob = (target: any).__ob__;
// ! 不允许设置 Vue 实例 和 根实例数据对象(根 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;
}

声明常量ob存储target__ob__属性,判断target是否有_isVue属性或者ob.vmCount的值是否大于零。如果同时满足其中的一个条件且在非生产环境中会报错,提示不能设置 Vue 实例对象的属性或者根组件的data选项的值,最后返回设置的值val

继续看剩下的代码。

// ! 原对象不是响应式对象,新属性直接赋值并返回
if (!ob) {
  target[key] = val;
  return val;
}
defineReactive(ob.value, key, val); // ! 新属性变成响应式
ob.dep.notify(); // ! 手动触发依赖
return val;

判断ob是否存在,即判断target是否是一个响应式对象,如果不是的话,直接给它的key赋值为val,并返回val

如果target是一个响应式对象,把它新增的值也变成响应式数据,然后手动触发依赖,并返回设置的值val

小结,虽然 Vue 的官方文档中说明只能是为响应式数据设置值,但是它的代码中也可以为非响应式的对象设置值,不过我们在实际开发中没有必要这样,因为可以直接在这个对象上操作,而不需要额外使用这个 API。

del#

下面看下del函数,我们在扩展 Vue 的构造函数的原型对象时创建Vue.prototype.$delete方法,然后在扩展 Vue 的构造函数的静态方法时创建Vue.delete方法,这两个方法所赋值的函数都是del函数。

在 Vue 的文档中,提供了这两个 API 供用户使用,用来删除对象的属性。如果对象是响应式对象,确保删除值能触发依赖,更新视图。如果不是响应式对象,直接删除它即可。

这个方法是为了避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。

下面查看del函数的代码,它的结构和set函数很相似,下面结合set来对比说明。

export function del(target: Array<any> | Object, key: any) {
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(
      `Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`
    );
  }
  // ...
}

首先和set函数一样,判断target的类型是不是未定义的值或者是一个原始类型的值,如果满足其中之一的话会报错,并提示不能删除nullundefined或者原始类型中的响应式属性。

继续看下面的代码。

if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.splice(key, 1); // ! splice 删除元素,并触发响应
  return;
}

判断target的类型是不是数组且它的索引是否有效的,如果都满足条件的话,直接调用splice删除索引key位置的值即可。

继续看下面的代码。

const ob = (target: any).__ob__;
 
// ! 不能删除 Vue 实例 和 根实例数据对象(根 data 不是响应式的)的属性
if (target._isVue || (ob && ob.vmCount)) {
  process.env.NODE_ENV !== "production" &&
    warn(
      "Avoid deleting properties on a Vue instance or its root $data " +
        "- just set it to null."
    );
  return;
}

set函数中逻辑的一样,声明常量ob存储target__ob__属性,判断target是否有_isVue属性或者ob.vmCount的值是否大于零。如果同时满足其中的一个条件且在开发中会报错,提示不能删除 Vue 实例对象的属性或者根组件的data选项的值,最后直接返回。

// ! 没有要删除的属性
if (!hasOwn(target, key)) {
  return;
}
delete target[key]; // ! 删除目标的字段

如果target对象上没有要删除的key值,直接返回。如果有的话,调用delete运算符删除target中的key字段或者索引。

继续看剩下最后的代码。

// ! 目标不是响应式,不处理
if (!ob) {
  return;
}
ob.dep.notify(); // ! 手动触发依赖

如果ob不存在,即target不是响应式对象,那么直接返回,不做处理。如果是响应式对象,手动触发依赖,更新视图。

注意del不需要返回值,也不需要返回一个布尔值来表示删除属性是否成功,这也从侧面说明这个函数关联的Vue.deletevm.$delete的 API 使用的场景非常少。

参考资料#