Vue源码解析系列——响应式原理篇:watch

本文详细解析了Vue.js中Watcher机制的工作原理,包括watch属性的初始化过程、watcher的构造函数、get方法以及update方法等核心内容。通过本文,读者可以深入了解Vue的数据响应式机制。
摘要由CSDN通过智能技术生成

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

写在前面

相信记过前几章节对源码的学习,我们都对DepWatcher的运作有了深入的了解。特别是上一章节《源码解析:computed》,对这两个类的理解更加的深入了。这次我们要学习的是watch属性的运作过程,理解了computed属性的运作过程后,watch属性的运作原理理解起来就非常简单了。

watch初始化

watch初始化在_init中:initState(vm)initState中又有这样一段代码:

if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
}

进入initWatch

initWatch

function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

遍历vm.$options.watch,如果watch[key]是个数组,就遍历数组,调用createWatcher;否则就直接调用createWatcher

createWatcher

function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

这个方法主要是对watch的一个规范化。毕竟watch在定义的时候既可以是一个函数,也可以是一个对象optionoption.handler才是真正的回调。
经过一些标准化处理后最后调用的是原型上的一个API:$watch

$watch

  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();
    };
  };

由于$watch是一个Vue的API,所以既可以是Vue使用者调用的,也可能是Vue内部调用的,所以这里又对cboption做了一次规范化。
然后option.user标注为true,表示这是一个用户定义的watch
实例化了一个Watcher,传入expOrFn(这里是要监控的变量名称,如:要对data中的foo监控,这里的expOrFn就是"foo"),cb(监控值变化后要执行的回调函数),optionsWatcher的配置)。
先不急进入Watcher的实例化查看逻辑,先继续往下看。
如果options.immediatetrue,也就是立即执行这个watch,直接就调用一次回调,并传入最新值。
最后返回了一个函数,主要是用于销毁watch

好了,现在我们进入user watcher的实例化。

watcher.contructor

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    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; // 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 =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        process.env.NODE_ENV !== "production" &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    //computed不会立刻求值
    this.value = this.lazy ? undefined : this.get();
  }

这边要注意的有两行代码:

this.getter = parsePath(expOrFn);

parsePath这个方法可以用于解析对象的.语法,如:expOrFn传入的是"a.b.c"这样一个字符串,parsePath就返回一个函数赋值给this.getter,用于寻找this.a.b.c的值。

以及:

this.value = this.lazy ? undefined : this.get();

直接调用this.get()

进入this.get

watcher.get

  get() {
    //改变Dep.target为当前的Wathcer实例
    pushTarget(this);
    let value;
    const 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);
      }
      //归还上一次的Watcher实例
      popTarget();
      //清除无用的watcher
      this.cleanupDeps();
    }
    return value;
  }

首先一上来直接调用pushTarget改变了Dep.target的值为当前的user watcher
之后调用了this.getter,也就是刚刚使用parsePath解析出来的函数去寻找this上的值,此时这个值有可能是computed也有可能是data

还记得在defineReactive中设置了数据的响应式吗?
这时的user watcher去获取实例上的值,就会触发defineReactive中设置的get函数,实例上那个值的依赖收集器——Dep实例立刻就会将当前的Dep.target(也就是user watcher)收集到他的列表中!当这个值一发生改变,立刻调用defineReactive中的set,立刻通知user watcher去触发this.update
我们等等再看this.update,我们先继续向下看this.get

如果option.deeptrue,使用traverseoption.deep这个配置是在监控对象、数组是使用的,可以深度监听对象、数组中所有值的变化。
进入traverse

traverse

const seenObjects = new Set()

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

创建了一个集合seenObjects,调用_traverse

_traverse

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const 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的操作。(这里我有一个疑问,明明Set就是一个集合,本身就可以防止重复,为什么这里还需要判断if (seen.has(depId)) return?)最后是一个分支:

  • 如果是一个数组,就遍历数组,递归调用_traverse,传入数组中的每个元素(触发一次get)。
  • 如果是个对象,就遍历对象的键,递归调用_traverse传如对象的每个键的值(触发一次get)。

每次递归都需要访问对象、数组中的数据,所以每次递归都会触发对象、数组中的元素的get,所以都会触发收集Dep.target,也就是user watcher

到这里,user watcher的初始化说完了,但是还少了user watcher触发更新的过程。触发user watcher的更新是在依赖调用了自身的set方法时,会调用dep.notify去遍历自身依赖列表,逐个触发上面的update方法。

watcher.update

  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      //如果是个computed watcher
      //将数据设置为 脏 状态,需要进行更新
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步watcher,直接就执行回调
      this.run();
    } else {
      //其他情况的watcher就放在watcher队列,在下一个Tick去行回调
      queueWatcher(this);
    }
  }

这边有三个分支:

  • 如果是一个懒watcher,也就是computed watcher,就设置当前watcher状态。
  • 如果是一个sync watcher,直接就调用run,也就是调用回调函数
  • 除了以上两种情况,都会调用queueWatcher,将当前watcher放入watcher待更新队列,在下一个Tick中执行,详情请看:响应式原理篇:nextTick

总结

watch属性的使用方法有两种,一种是函数体,一种是配置体,都可以创建user watcher去监控响应的属性。其实理解了computed的运作过程,watch理解起来就非常简单了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值