Vue中$set和$delete的原理

29 阅读1分钟

1. 为什么需要$set$delete

1.1 Object.defineProperty无法检测对象的属性动增删,无法检测数组索引的直接修改。

1.2 对数组:通过调用重写的变异方法触发更新

1.3 对对象:

  • 将新属性转为响应式 ($set 会调用 defineReactive 方法,将新属性转换为响应式属性。)
  • 手动触发依赖通知
  • 通过Observer实例的dep对象收集依赖

2.应用场景

2.1 动态添加/删除对象属性

2.2 修改数组指定索引元素

2.3 嵌套对象深层属性更新

2.4 初始复杂数据结构

3. 源码

$set的源码

/**
 * vm.$set 的底层实现
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
    // 如果 target 是一个数组,并且 key 也是一个有效的数组索引值的话
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 设置数组的 length 属性,设置的属性值是 "数组原长度" 和 "key" 中的最大值
        target.length = Math.max(target.length, key)
        // 然后通过数组原型上的方法,将 val 添加到数组中
        // 在前面数组响应式源码的阅读中可以知道,通过数组原型的方法添加的元素,其是会被转换成响应式的
        target.splice(key, 1, val)
        return val
    }
    // 这里用于处理 key 已经存在于 target 中的情况
    if (hasOwn(target, key)) {
        // 由于这个 key 已经存在于对象中了,也就是说这个 key 已经被侦测了变化,在这里,只不过是修改下属性而已
        // 所以在这里,直接修改属性,并返回 val 即可
        target[key] = val
        return val
    }

    const ob = (target: any).__ob__

    // 如果 target 没有 __ob__ 属性的话,说明 target 并不是一个响应式的对象
    // 所以在这里也不需要做什么额外的处理,将 val 设到 target 上,并且返回这个 val 即可
    if (!ob) {
        target[key] = val
        return val
    }
    // 如果上面所有的判断条件都不满足的话,说明用户是在响应式数据上新增了一个数据,这种情况需要跟踪这个新增属性的变化
    // 在这里使用 defineReactive 将 val 变成 getter/setter 的形式
    defineReactive(ob.value, key, val)
    // 因为新增了一个属性,所以 ob.value 变化了,所以在这里需要出发依赖的更新
    ob.dep.notify()
    return val
}

$delete的源码

/**
 * Delete a property and trigger change if necessary.
 */
// Vue 对数据的监控是通过 Object.defineProperty() 实现的,所以当用户通过 delete 关键字删除某个字段时,Vue 是检测不到的,
// 为了解决这个问题,Vue 提供了 vm.$delete 来解决这个问题
export function del(target: Array<any> | Object, key: any) {
    // 如果 target 是一个数组,并且 key 是一个下标值的话
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 执行数组原型上的 splice 方法,该方法会执行删除的操作,并且会出发依赖的更新
        target.splice(key, 1)
        return
    }
    const ob = (target: any).__ob__
    // 如果 target 上面没有 key 属性的话,直接 return 即可,什么都不用干
    if (!hasOwn(target, key)) {
        return
    }
    // 使用 js 中原生的 delete 关键字删除指定的 key
    delete target[key]
    // 在这里判断 target 是不是响应式的,如果不是的话,就不用出发依赖的更新操作了。在这里,直接 return
    if (!ob) {
        return
    }
    // 出发依赖的更新操作
    ob.dep.notify()
}