vue2响应式原理(4)-- 新增 vm.$set、删除 vm.$delete

248 阅读2分钟

先看一段vue官网中的描述:

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:

var vm = new Vue({  
  data:{  
    a:1  
  }  
})    
// `vm.a` 是响应式的   
vm.b = 2  
// `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:

Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`  
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

vm.$set

src/core/instance/state.ts

Vue.prototype.$set = set

src/core/global-api/index.ts

Vue.set = set

src/core/observer/index.ts

export function set(
  target: any[] | Record<string, any>,
  key: any,
  val: any
): any {
  if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${target}`
    )
  }
  const ob = (target as any).__ob__
  if (isArray(target) && isValidArrayIndex(key)) {
   // ...数组暂时不看
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  if ((target as any)._isVue || (ob && ob.vmCount)) {
    __DEV__ &&
      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, undefined, ob.shallow, ob.mock)
  if (__DEV__) {
    // ...
  } else {
    ob.dep.notify()
  }
  return val
}

Vue.set和vm.$set调用的都是同一个方法;

首先traget不能是null,undefined,基本数据类型,否则抛出警告;

1. key属性已经是响应式

如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true,所以如果key不存在于target中,并且不存在于Object原型链中,说明这个key属性已经是响应式了,可以直接观察到了,直接赋值就行了target[key] = val

2. 对象不能是Vue 实例,或者 Vue 实例的根数据对象

vue官网描述:

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

if ((target as any)._isVue || (ob && ob.vmCount)) ,抛出警告;

3. 如果对象不是响应式

const ob = (target as any).__ob__if (!ob) ,如果target中不存在_ob_属性,说明target不是响应式对象,直接赋值,就不用做特殊处理了

export class Observer {
  constructor(public value: any, public shallow = false, public mock = false) {
    def(value, '__ob__', this)
}

可以看到,对象做观测时,会将Observer实例赋值给对象的_ob_属性;

defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock),把新添加的属性key变为响应式,在Observer构造器函数中,第一个参数public valueobj.value拿到的就是target,如果val为对象,val内部的属性也会被变成响应式。

4. 手动通知更新ob.dep.notify()

先看gettter中的一段逻辑:

export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val, false, mock)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {    
      if (Dep.target) {
       if (__DEV__) {
         
        } else {
          dep.depend()
        }
        if (childOb) {
          childOb.dep.depend()
        }
      }
    },
    set: function reactiveSetter(newVal) {
    
  })
  return dep
}

childOb.dep.depend()收集了依赖,这个逻辑专门为vm.$set准备的,所以在set函数中手动执行ob.dep.notify(),这样新添加属性也能触发依赖watcher,例如:

 <body>
    <div id="app">
      <div ref="name">{{obj}}</div>
      <button @click="change">change</button>
    </div>
    <script src="./vue.js"></script>
    <script>
      let vm = new Vue({
        el: '#app',
        data() {
          return {
            obj: {
              foo: 'abc'
            }
          }
        },
        methods: {
          change() {
            this.$set(this.obj, 'bar', 123)
          }
        }
      })
    </script>
  </body>

在这个例子中,childOb返回的是obj的Observer实例ob,在读取obj的get函数中childOb.dep.depend()添加了obj的依赖渲染watcher,在set函数中,给obj添加新属性bar,执行defineReactive(obj, 'bar', '123'),将bar属性转换成响应式,同时手动触发更新,ob.dep.notify(),收集的obj的依赖watcher会被触发

vm.$delete

export function del(target: any[] | object, key: any) {
  if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
    warn(
      `Cannot delete reactive property on undefined, null, or primitive value: ${target}`
    )
  }
  if (isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target as any).__ob__
  if ((target as any)._isVue || (ob && ob.vmCount)) {
    __DEV__ &&
      warn(
        'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
      )
    return
  }
  if (isReadonly(target)) {
    __DEV__ &&
      warn(`Delete operation on key "${key}" failed: target is readonly.`)
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  if (__DEV__) {
 
  } else {
    ob.dep.notify()
  }
}

vm,$deletevm.$set逻辑类似, delete target[key]删除属性, if (!ob) { return },在手动触发更新前,先判断target是不是响应式,如果不是直接返回,什么都不用做,是响应式,再去手动触发更新 ob.dep.notify()