vue源码解读(二):变化侦测相关API的实现原理

vue源码解读(二):变化侦测相关API的实现原理

vm.$watch

用法

vm.$watch(expOrFn,callback,options)
参数:

  • { string | Function } expOrFn
  • { Function | Object } callback
  • { Object } options

返回值:{ Function } unwatch
用法:用于观察一个表达式或者computed函数在vue实例上的变化。回调函数调用时,会从参数中回去新数据和老数据。表达式直接受以.为分隔符的路径。
例如:

vm.$watch('a.b.c',function(newVal,oldVal){
    //todo
})

vm.$watch返回一个取消观察函数unwatch,用于停止触发回调。
参数options

  • deep:为了可以发现对象内部的属性变化,可以添加deep:true进行深度监听。
  • immediate:在参数中指定immediate:true,将立即以表达式的当前值触发回调函数。

原理

vm.$watch其实内部也是对Watcher的封装。我们来看一下vue内部是如何实现deepimmediate的。

src/core/instance/state.js

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

在这段代码中,可以看出,在初始化组件的时候,如果用户使用了immddiate参数,那么就会立即执行一次cb,返回一个新的函数unwatchFn(),在返回函数中调用了watcher的teardown()方法,我们来看一下该方法做了什么:

src/core/observer/watcher.js

/**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }

当执行该方法后,就会将Watcher实例从当前观察的依赖列表中移除。
因此,当前我们需要在Watcher中记录自己都订阅了谁,也就是Watcher实例都被收集进哪些Dep中了。然后当Watcher不想继续订阅这些依赖时,循环遍历这些记录,通知Dep,将自己从它们的依赖列表中移除。
现在Watcher中添加addDep方法,该方法的作用是让Watcher记录自己订阅过哪些Dep

src/core/observer/watcher.js

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set() 
/**
   * Add a dependency to this directive.
   */
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

在上述代码中,通过depIds来判断当前Watcher是否订阅过该Dep,就可以避免重复订阅。
执行this.newDepIds.add(id)来记录当前Watcher一定订阅来该Dep
执行this.newDeps.push(dep)来记录自己订阅了哪些Dep
执行dep.addSub(this)来将自己订阅到Dep中。
现在我们已经知道了vue.$watcher是如何记录Dep的,上面也提到过它是通过循环遍历依赖列表,使用this.deps[i].removeSub(this)来移除依赖的,接下来我们来看看removeSub是如何实现移除依赖列表的:

src/core/observer/dep.js

removeSub (sub: Watcher) {
  remove(this.subs, sub)
}


/**
 * Remove an item from an array.
 */
export function remove (arr: Array<any>, item: any): Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

其实就是使用数组的splice方法将其删除,然后当数据发生变化后,将不再通知该Watcher

deep的原理

在之前的一篇文章中讲过,Vue中的依赖收集和依赖触发。当Watcher想监听某个数据的时候,就会触发该数据的依赖收集逻辑,将自己收集进去,当数据发生变化的时候,通知Watcher。而实现deep功能,除了将当前数据进行依赖收集之外,还有递归的将其子数据也进行依赖收集,这样当子数据发生变化的时候,就会通知当前Watcher了。
具体实现:

src/core/observer/watcher.js

if (this.deep) {
  traverse(value)
}

src/core/observer/traverse.js

/* @flow */

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  //如果当前数据不是数组、对象、或者被冻结、或者是VNode,直接返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    //数据已经是响应式
    const depId = val.__ob__.dep.id
    //拿到depId,保证不会重复收集依赖
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  //如果是数组 循环遍历 将数组中的每一项都调用_traverse 收集依赖
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    //如果是对象 
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

通过_traverse方法,就实现了对数组或者对象的深度监听。

vm.$set

用法

vm.$set(target,key,value)
参数:

  • { Object | Array } target
  • { string | number } key
  • { any } value

返回值:{ Function } unwatch
用法:在object上设置一个属性,如果object是响应式的,那么属性被创建之后也会是响应式的,被触发视图更新。该方法主要用来避开Vue不能侦测属性被添加的限制。

原理

src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
  //如果是数组 则改变数组的length,使用splice方法在key后面添加一个value
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    //使用splice方法,触发数组拦截器,触发依赖收集逻辑
    target.splice(key, 1, val);
    return val;
  }
  //如果是对象且key已经存在于对象中,则直接更改对象属性值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  //获取Observe实例 如果target已是响应式的 则ob存在
  const ob = (target: any).__ob__;
  //target不能是vue实例或者vue实例的根数据对象
  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不是响应式对象 则不需要做特殊处理
  if (!ob) {
    target[key] = val;
    return val;
  }
  //否则 重新触发defineReactive,进行依赖收集
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}

这就是vm.$set()的原理。

vm.$delete

用法

vm.$delete(target,key)
参数:

  • { Object | Array } target
  • { string | number } key

用法:删除对象的属性如果对象是响应式的,则可以保证能触发更新视图。

原理

src/core/instance/state.js

import { del } from '../observer/index'
Vue.prototype.$delete = del;

src/core/observer/index.js

/**
 * Delete a property and trigger change if necessary.
 */
export function del(target: Array<any> | Object, key: any) {
  //target如果是数组 则使用splice触发依赖
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;
  //如果target是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;
  }
  //如果target上不存在key属性
  if (!hasOwn(target, key)) {
    return;
  }
  //如果是对象 直接删除 
  delete target[key];
  //如果target不是响应式数据 不做任何处理
  if (!ob) {
    return;
  }
  //否则 触发依赖
  ob.dep.notify();
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!