Watch

Watch 的使用例子:

watch: {
  c: {
    handler: function (val, oldVal) { /* ... */ },
  },
}

回顾一下响应式原理:Vue 中的响应式是指数据层的变化能自动触发视图的更新。其原理是每个 data 属性都有一个容器 dep 去收集一个 Watcher, 这个 Watcher 是一个包含了渲染组件的方法的实例。每次数据的变化都能触发这些收集到的 Watcher, 进而实现视图的更新

这里的 watch 也是同理,watch 监听的每个属性都有对应的一个 handler 方法,这个 handler 方法跟上面的响应式原理一样也会被一个 Watcher 包装并被监听的属性所收集。接下从源码角度分析一下实现过程

源码分析

源码中处理 watch 属性的地方:initState(vn) => initWatch(vm, opts.watch)

// initWatch
function initWatch (vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}
 // createWatcher
function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}

initWatch 会遍历所有的 watch, 并执行 createWatcher(vm, key, handler), 然后再执行 vm.$watch(expOrFn, handler, options),这里参数表示如下:

  • expOrFn: 监测的属性名

  • handler: 监测的属性名对应的 handler

重点看下 vm.$watch 的定义:

Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var 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();
    }
  };
}

跟响应式原理一样,在 vm.$watch 中也实例化了一个 Watcher ,分析下此时的这个 Watcher 构造函数做了啥:

  var Watcher = function Watcher (
    vm,
    expOrFn, // 这个参数是 watch 要监听的属性名
    cb,     // 对应 watch 的 handler
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // 如果 expOrFn 是函数,render 时这里是函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      // watch时 expOrFn 是属性名,所以会走到这里
      // parsePath 方法就是获取属性名对应值的方法
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    // this.lazy 为false 所以会执行 this.get()
    this.value = this.lazy
      ? undefined
      : this.get();
  };

要注意此时传进来的参数:

  • expOrFn: watch 要监听的属性名

  • cb: watch 的属名性名的 handler

在执行 Watcher 构造方法时重点代码是以下几行

this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
  this.getter = expOrFn;
} else {
  this.getter = parsePath(expOrFn);
}
this.value = this.lazy
  ? undefined
  : this.get();

expOrFn 此时是字符串所以走的代码是 this.getter = parsePath(expOrFn), 这里先简单介绍下 parsePath 方法就是获取属性名对应值的方法

function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  var segments = path.split('.'); // 处理监测属性是一个对象属性的情况,比如 `obj.a: { handler: function(){}}`
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

在响应原理中实例的 Watcher 时,expOrFn 是一个渲染组件的 updateComponents() 函数

之后执行 get() 方法:

 /**
   * Evaluate the getter, and re-collect dependencies.
   */
Watcher.prototype.get = function get () {
  pushTarget(this); // 将当前的 Watcher 保存到 `Dep.target` 全局属性中
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm); 
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

上面的 get 主要做的事情有:

  1. 执行 pushTarget(this): 将当前的 Watcher 实例保存到 Dep.target 全局属性中, 方便被属性的 dep 容器收集

  2. 执行 value = this.getter.call(vm, vm) : 此时的 this.getter 是上文中 parsePath(expOrFn) 的返回值, 这里执行完之后就是得到属性对应的值

data(){
  message: 007,
},
watch: {
  message: {
    handler: function(){
      console.log('message变了!')
    }
  }
}
// value = this.getter.call(vm, vm) 执行之后,value就是 007

以上面这个例子为例, this.getter 是获取 message 对应的值,就是意味着要读取 data 上的 message 属性, 此时就是会被 Object.definedProperty()get 方法劫持,然后就是把当前保存在全局属性 Dep.target 中的当前的 Watcher 实例保存到容器 dep 中, 至此就完成当前属性(message)对这个 Watcher 的收集

上面就完成了对 watch 监听属性机制的分析

触发handle

当要监听属性更新时,这会派发收集到的 Watcher ,执行 Watcher.update => Watcher.run => this.cb

  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };

this.cb 就是 watch handle 方法

deep

使用 watch 的时候可以配置一个 deep 属性来配置是否深度监听属性,那么 Vue 中是如何处理的呢? 以下在例子为例:

data(){
  return {
    tree: {
      child: {
        name: 'lan'
      }
    }
  }
},
watch: {
  tree: {
    handler: function(){},
    deep: true
  }
}

首先 Vue 也可以劫持到 object 类型的数据的, 因为在初始化 data 数据时,会遍历对象对子元素使用 Object.definedProperry() 处理,也就是意味着每他对象的子元素也会有自己的一个收集容器的 dep

回到 Vue 对 watch 的处理中,上文有提到在 new Watcher 的构造方法时,结尾会执行 Watcher.property.get() 方法:

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

可以看到当前 this.deeptrue 时, 会执行 traverse(value) ,它的定义如下:

function traverse (val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}
// _traverse
function _traverse (val, seen) {
  var i, keys;
  var isA = Array.isArray(val);
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }

  // 获取当前 val 所对应 Observer 实例
  if (val.__ob__) {
    var depId = val.__ob__.dep.id;
    if (seen.has(depId)) {
      return
    }
    seen.add(depId);
  }
  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 方法,传进去的参数为:

  • val: 以上面的例子为例,此时的值为: { child: { name: 'lan' }}

  • seenObjects: 一个 Set 结构

进入到 _traverse 后, 逐条分析一下代码:

  1. 只有当前 val 的值是对象类型才继续往下处理

  2. 通过 depIdseen 配合判断当前的 val 是否是已经处理过的, 这个 val.__ob__ 属性在初化 data 数添加的, 具体位置在 new Observer(value) 中有一行代码 def(value, '__ob__', this)

 function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

参数为:

  • value: 当前监听属性对应的值

  • __ob__

  • this: 当前 Observer 实例

initData (vm) => observe (value, asRootData) => new Observer(value)

  1. 无论是数组还是对象都通过遍历, 继续 _traverse(val[keys[i]], seen)

然后整个函数就完了~,怎么感觉什么都做?

真正发挥作用 _traverse(val[keys[i]], seen) 这行代码,调用 _traverse 方法时一方面是遍历子元素递归调用自己,还有一个关键的语句是 val[keys[i]], val[keys[i]] 的作用是获取当前子属性的值,上文有提到,对于 object 类型的数据, Vue 会通过遍历对子属性也添加 Object.defindProperty 劫持,也就意味着当这里访问 val[keys[i]] 时,就会触发劫持的 get 方法,触发 get 方法时,此时的 Dep.target 还是这个 watch 对应的 Watcher !所以子元素的 dep 也收集了这个 Watcher,以此完成了对所谓 deep 的深度监听

immediate

immediatewatch 的另一个属性,表示立即执行一个 handler 的意思,在 Vue.prototype.$watch 定义

Vue.prototype.$watch = function (
  expOrFn,
  cb,
  options
) {
  var vm = this;
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {};
  options.user = true;
  var 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();
  }
};

如果存在 options.immediate 则执行 cb.call(vm, watcher.value) , 这个 cb 就是对应的 handler 方法

总结

watch 的监听原理其实也是把当前的 watch handler 包装在 watcher 中,然后这个 watch Watcher 被监听的属性所收集,之后当属性被更新时,这会派发收集的 Watcher,执行对应的 handler 方法

deep 的实现本质是因为 Vue 对于对象类型的数据会遍历其子元素,并对这些子元素进行监听劫持,然后当前如果有 deep 属性时, watch Watcher 中会去遍历监听元素的子元素,注意此时全局 Dep.target 仍为当前这个 watch Watcher,通过访问这些子属性,让子属性收集 watch Watcher

Last Updated:
Contributors: 156081289@qq.com