变化侦测API实现

在上一节中,我们分析了变化侦测一些问题,在这一节中我们来分析一下为了解决这些问题,Vue.js是如何实现相关API的。

Vue.set实现

Vue.setvm.$set引用的是用一个set方法,其中set方法被定义在observer/index.js文件中:

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)}`)
  }
  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
}

在代码分析之前,我们来回顾一下Vue.set或者vm.$set的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: []
    }
  },
  created () {
    // 添加对象新属性
    this.$set(this.obj, 'b', 'b')
    console.log(this.obj.b) // b

    // 往数组中添加新元素
    this.$set(this.arr, 0, 'AAA')
    console.log(this.arr[0]) // AAA

    // 通过索引修改数组元素
    this.$set(this.arr, 0, 'BBB')
    console.log(this.arr[0]) // BBB
  }
}

代码分析:

  • set方法首先对传入的target参数进行了校验,其中isUndef判断是否为undefinedisPrimitive判断是否为JavaScript原始值,如果满足其中一个条件则在开发环境下提示错误信息。
export default {
  created () {
    // 提示错误
    this.$set(undefined, 'a', 'a')
    this.$set(1, 'a', 'a')
    this.$set('1', 'a', 'a')
    this.$set(true, 'a', 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法往数组中指定位置设置值。其中,还重新设置了数组的length属性,这样做是因为我们传入的索引可能比现有数组的length还要大。

  • 接着判断是否为对象,并且当前key是否已经在这个对象上,如果已经存在,则我们只需要进行重新复制即可。

  • 最后,通过defineReactive方法在响应式对象上面新增一个属性,defineReactive方法已经在之前介绍过,这里不再累述。在defineReactive执行完毕后,马上进行派发更新,通知响应式数据的依赖立即更新,可以说以下两段代码是set方法核心中的核心:

defineReactive(ob.value, key, val)
ob.dep.notify()

Vue.delete实现

解决完新增属性的问题后,我们来解决以下删除属性的情况,Vue.deletevm.$delete使用的是同一个delete方法,它被定义在observer/index.js文件中:

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)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  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
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

在代码分析之前,我们来回顾以下Vue.delete或者vm.$delete的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: [1, 2, 3]
    }
  },
  created () {
    // 删除对象属性
    this.$delete(this.obj, 'a')
    console.log(this.obj.a) // undefined
    // 删除数组元素
    this.$delete(this.arr, 1)
    console.log(this.arr)   // [1, 3]
  }
}

代码分析:

  • 首先判断了待删除的target不能为undefined或者原始值,如果是则在开发环境下提示错误。
export default {
  created () {
    // 提示错误
    this.$delete(undefined, 'a')
    this.$delete(1, 'a')
    this.$delete('1', 'a')
    this.$delete(true, 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法删除指定位置的元素。
  • 接着判断当前要删除的属性是否在target对象中,如果不在则直接返回,什么都不做。
  • 最后,通过delete操作符删除对象上的属性,然后ob.dep.notify()进行派发更新,通知响应式对象上的依赖进行更新。

Vue.observable实现

Vue.observable是在Vue2.6+版本才会有的一个全局方法,它的作用是让一个对象变成响应式:

const obj = {
  a: 1,
  b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) // 触发getter

observeObj.b = 22 // 触发setter

这个全局方法是在initGlobalAPI的过程中被定义的,initGlobalAPI我们在之前已经介绍过,这里不在累述:

export default function initGlobalAPI (Vue) {
  // ...
  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  // ...
}

我们可以看到observable的实现很简单,在方法内部仅仅只是调用了observe方法,然后返回这个obj。关于observe的代码实现,我们在之前的章节中已经介绍过了,这里不再过多的进行说明:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
最后更新时间:
贡献者: wangtunan