【Vue2深度学习】变化侦测篇-变化侦测相关的API

258 阅读3分钟

与变化侦测相关的常用API,主要有vm.$watchvm.$setvm.$delete,他们是挂载到Vue原型上的, 下面我们来一一介绍。

vm.$watch

一、介绍

vm.$watch用于观察一个表达式或函数在Vue实例上的变化。回调函数调用时,会从参数得到新数据(new value) 和旧数据(old value).表达式只接受以点分隔的路径,例如a.b.c,如果是一个比较复杂的表达式,可以用函数代替。

二、语法

/**
* 参数
* {String | Function} expOrFn
* {Function | Object} callback 
* {Object} [options]
* 返回值
* {Function} unwatch 
*/
​
vm.$watch( expOrFn, callback, [options] )

三、使用

  • 当监听Object的某个子属性的变化时
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})
  • 当监听Object自身及所有子属性的变化时(注意需要加deep选项,实现深度监听)
vm.$watch('someObject', callback, {
  deep: true
})
​
  • 当监听一个函数的变化时
vm.$watch(
  function () {
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)
  • 当以表达式的当前值立即触发回调时
vm.$watch('a', callback, {
  immediate: true
})
  • 当callback与 options合并使用时(第二个参数为一个对象)
vm.$watch(
    'a.b.c',
    {
        handler: function (val, oldVal{},
        deep: true
    }
)

四、内部原理

了解到vm.$watch的用法后,我们来根据其源码来分析其内部实现原理。

Vue.prototype.$watch = function (expOrFn,cb,options) {
    const vm: Component = this
    //判断传入的回调是否为一个对象。
    if (isPlainObject(cb)) {
        // 如果传入的回调是个对象,那就表明用户是把第二参数回调函数cb和第三个参数选项options合起来传入的,此时调用createWatcher。
       // createWatcher方法其实就是将用户传入的回调(对象)中的回调函数`cb`和参数`options`剥离出来,然后再以常规的方式$watch方法并将剥离出来的参数传进去。
      return createWatcher(vm, expOrFn, cb, options)
    }
    //如果用户传入的回调不是一个对象,而是一个函数,则获取用户传入的options,如果用户没有传入则将其赋值为一个默认空对象
    options = options || {}
    //$watch方法内部会创建一个watcher实例,由于该实例是用户手动调用$watch方法创建而来的,所以给options添加user属性并赋值为true,用于区分用户创建的watcher实例和Vue内部创建的watcher实例
    options.user = true
    //传入参数创建一个watcher实例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    //判断如果用户在选项参数options中指定的immediate为true,则立即用被观察数据当前的值触发回调
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    // 返回一个取消观察函数unwatchFn,用来停止触发回调
    return function unwatchFn () {
      watcher.teardown()
    }
  }

通过上述源码逐行解读,我们知道了vm.$watch是如何实现的,那么还有最后一个问题,当选项参数options中的deep属性为true时,如何实现深度观察呢?

export default class Watcher {
    constructor (/* ... */) {
        // ...
        this.value = this.get()
    }
    get () {
        // 关键代码
        if (this.deep) {
            traverse(value)
        }
        return value
    }
}

可以看到,在get方法中,如果传入的deeptrue,则会调用traverse函数,把被观察数据的内部值都递归遍历读取一遍,从而实现深度观察。

vm.$set

一、介绍

vm.$set用来向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。

它必须用于向响应式对象上添加新属性,因为对于 Vue 来说,当我们向object数据里添加一对新的key/valueVue是无法观测到的;而对于Array型数据,当我们通过数组下标修改数组中的数据时,Vue也是是无法观测到的。

二、语法

/**
* 参数
* {Object | Array} target
* {string | number} propertyName/index 
* {any} value
* 返回值
* 设置的值
*/
​
vm.$set( target, propertyName/index, value )

三、使用

  • 当需要向对象中新增属性时
vm.$set( obj, a, 'hello' )
  • 当需要通过下标的形式修改数组时
vm.$set( Array, 3, 'world' )

四、内部原理

了解到vm.$set的用法后,我们来根据其源码来逐行分析其内部实现原理。

export function set (target, key, val){
    //首先判断在非生产环境下如果传入的target是否为undefined、null或是原始类型,如果是,则抛出警告
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
       ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    //判断如果传入的target是数组并且传入的key是有效索引的话,那么就取当前数组长度与key这两者的最大值作为数组的新长度,然后使用数组的splice方法将传入的索引key对应的val值添加进数组。
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    //如果传入的target不是数组,那就当做对象来处理。
    //判断传入的key是否已经存在于target中,如果存在,表明这次操作不是新增属性,而是对已有的属性进行简单的修改值,那么就只修改属性值即可
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    //获取到traget的__ob__属性,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是 Vue 实例,或者是 Vue 实例的根数据对象,则抛出警告并退出程序,
    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
    }
    //如果ob属性为false,那么表明target不是一个响应式对象,那么我们只需简单给它添加上新的属性,不用将新属性转化成响应式
    if (!ob) {
        target[key] = val
        return val
    }
    //如果target是对象,并且是响应式,那么就调用defineReactive方法将新属性值添加到target上,defineReactive方会将新属性添加完之后并将其转化成响应式
    defineReactive(ob.value, key, val)
    //通知依赖更新
    ob.dep.notify()
    return val
}

以上即是对vm.$set内部原理的分析

vm.$delete

一、介绍

vm.$delete用来删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制

二、语法

/**
* 参数
* {Object | Array} target
* {string | number} propertyName/index 
*/
​
vm.$delete( target, propertyName/index, value )

三、使用

  • 当需要删除对象的某个属性时
vm.$delete( obj, a)
  • 当需要通过下标的形式删除数组的某个值时
vm.$delete( Array, 3)

四、内部原理

了解到$delete的用法后,我们来根据其源码来逐行分析其内部实现原理。

export function del (target, key) {
  //判断在非生产环境下如果传入的target不存在,或者target是原始值,则抛出警告
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  //判断如果传入的target是数组并且传入的key是有效索引的话,就使用数组的splice方法将索引key对应的值删掉
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  //如果传入的target不是数组,那就当做对象来处理。
  const ob = (target: any).__ob__
  // 取到traget的__ob__属性,我们说过,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是 Vue 实例,或者是 Vue 实例的根数据对象,则抛出警告并退出程序,
  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
  }
  //判断传入的key是否存在于target中,如果key本来就不存在于target中,那就不用删除,直接退出程序即可
  if (!hasOwn(target, key)) {
    return
  }
  //如果target是对象,并且传入的key也存在于target中,那么就从target中将该属性删除
  delete target[key]
    
  //判断当前的target是否为响应式对象,如果不是,删除完后直接返回不通知更新
  if (!ob) {
    return
  }
  //如果是响应式对象,则通知依赖更新
  ob.dep.notify()
}

该方法的内部原理与set方法有几分相似,都是根据不同情况作出不同处理。

\