Vue中vm.$watch的实现原理

API用法

/**
* watch
* @params expOrFn {Strng | Function}	监听项
* @params callback {Function | Object}	回调函数
* @params options ? {					配置项
* 	{boolean} deep			深度监听
* 	{boolean} immediate  	立即执行
* @returns {Function} unwatch			此函数调用后会取消该监听
*/
vm.$watch(expOrFn, callback, [options])

实现思路

  1. 通过实例化一个Watcher对象来实现基本的功能:监听项变化时执行回调函数。(该部分细节点击此处:Vue中对象的响应式原理解析
  2. 处理监听项为函数的情况(在Watcher中)
  3. 返回一个取消监听的函数(在Watcher中:watcher.teardown()
  4. 处理options.immediate: true的情况:直接执行一遍callback
  5. 处理options.deep: true的情况:遍历监听项的所有子项,触发子项的收集依赖逻辑,将该callback放入遍历的所有子项中。

内部原理

当expOrFn是函数时,它不仅仅可以动态返回数据,其中读取的所有数据也都会被观察。

vm.$watch:

Vue.prototype.$watch = function (expOrFn, cb, options) {
    const vm = this;
    options = options || {};
    const tacher = new Watcher(vm, expOrFn, cb, options);
    // 立即执行
    if (options.immediate) {
        cb.call(vm, watcher.value);
    }

    return function unwatchFn() {
        watcher.teardown();
    }
}

Watcher:

class Watcher {
	constructor(vm, expOrFn, cb, options) {
      this.vm = vm;
      this.deep = (options) ? !!options.deep : false;
      this.deps = []; // 用于存储Dep,列表项表示这个Watcher依赖都被谁订阅了
      this.depIds = new Set(); // 用于存储Dep的ID,来防止重复添加
      if (typeof expOrFn === 'function') {
          this.getter = expOrFn;
      } else {
          this.getter = parsePath(expOrFn); // 解析简单路径的值
      }

      this.cb = cb;
      this.value = this.get();
  }

  /**
   * 增加Dep
   * @params dep Object Dep
  */
  addDep(dep) {
      const id = dep.id;
      // 防止重复增加Dep
      if (!this.depIds.has(id)) {
          this.depIds.add(id);
          this.deps.push(dep);
          dep.addSub(this);
      }
  }

  get() {
      window.target = this;
      let value = this.getter.call(this.vm, this.vm);
      this.deep && traverse(value); // 深度监听的处理,原理是将value所有的子集收集当前的Walker
      window.target = undefined;
      return value;
  }

  update() {
      const oldValue = this.value;
      this.value = this.get();
      this.cb.call(this.vm, this.value, oldValue); // 核心操作
  }
	
	/**
     * 从所有依赖项的Dep列表中将自己移除
    */
    teardown() {
        let i = this.deps.length;
        while (i--) {
            this.deps[i].removeSub(this);
        }
    }
}

function parsePath(path) {
  if (bailRE.test(path)) return; // 过滤掉特殊字符
  const segments = path.split('.');

  return function (obj) {
      for (let i = 0, len = segments.length; i < len; i++) {
          if (!obj) return;
          obj = obj[segments[i]];
      }
      return obj;
  }
}

teardown()的实现思路
在该Watcher实例被添加到对象Depsubs中时,将Dep存到Watcherthis.deps中,teardown()方法会遍历this.deps中的每一个Dep,调用该Dep对象上的removeSub()方法,将该Watcher实例在Dep对象的subs列表中去除,这样Dep对象发生变化后就不会通知到该Watcher了。

这里非常强调的一点:
一定要在window.target = undefined之前去触发子值的收集依赖逻辑,这样才能保证子集收集的依赖是当前这个Watcher。如果在window.target = undefined之后去触发收集依赖的逻辑,那么其实当前的Watcher并不会被收集到子值的以来列表中,也就无法实现deep的功能。
Dep

class Dep {
    constructor() {
        this.id = uid++;
        this.subs = [];
    }

    depend() {
        if (window.target) {
            // this.addSub(window.target); // 废弃
            window.target.addDep(this); // 新增
        }
    }

    removeSub(sub) {
        const index = this.subs.indexOf(sub);
        if(index > -1) {
            return this.subs.splice(index, 1);
        }
    }

    // ... 详情可了解本专栏的其他文章
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值