Vue源码之用户watcher

Posted 天地会珠海分舵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue源码之用户watcher相关的知识,希望对你有一定的参考价值。

通过上一篇文章《Vue源码之渲染watcher》我们学习到了每个组件实例初始化时都会创建一个渲染watcher来监控页面引用到的响应式数据的变动,一旦数据发生变化,就会通知渲染watcher来重新生成虚拟DOM,做diff,update,然后patch来更新页面。

今天我们将会学习下vue框架用到的第三个watcher - 用户watcher。下面回忆下这三个watcher的简介:

  • 计算属性watcher: vue会为我们在computed选项写的每个我们自定义的计算属性创建一个计算属性watcher,该watcher会在依赖的响应式数据变化时将计算属性标志位设置成dirty,使得页面在下次更新时调用计算属性函数进行求值。
  • 渲染watcher: vue会为每个组件创建一个渲染watcher来在依赖的响应式数据状态发生变化时重新渲染页面
  • 用户watcher: vue会为我们在watch选项写的每个要监控的属性创建一个watcher来在其变化时执行提供的回调函数

既然用户watcher是根据我们编写的watch选项而创建的,所以我们下面先简单介绍下watch的用法。

1. watch用法简介

我们在编写组件代码时,如果我们需要监控某个响应式属性的变化,我们会在watch选项下面实现相关的监听函数。

标准的写法应该是下面这个样子的

watch: 
  counter: 
    handler: function (newVal, oldVal) 
      console.log(newVal, oldVal);
    ,
  ,
,

如果不需要提供immediate,deep等选项的话,可以简化写成下面这样

watch: 
   counter: function (newVal, oldVal) 
     console.log(newVal, oldVal);
   ,
 ,

一个完整的vue实例初始化例子如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>counter</div>
      <button @click="increase">Increase</button>
    </div>

    <script>
      const vm = new Vue(
        el: "#app",
        data: 
          counter: 1,
        ,
        methods: 
          increase() 
            this.counter += 1;
          ,
        ,
        watch: 
          counter: 
            handler: function (newVal, oldVal) 
              console.log(newVal, oldVal);
            ,
          ,
        ,
      );
    </script>
  </body>
</html>

2. 用户watcher源码分析

下面开始分析下我们写的watch选项是怎么生成用户watcher的。

我们编写vue实例或者组件实例的时候,比如上面的创建vue实例中的例子,在写好watch选项之后,vue会将这些选项放到options参数并传递给他_init方法来对实例进行初始化。然后会经历一系列的初始化函数调用流程。

// core/instance/index.js文件
function Vue(options) 
  this._init(options);


// core/instances/init.js文件
Vue.prototype._init = function (options?: Object) 
    const vm: Component = this;
    ...
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || ,
    vm)
    ...
    initState(vm);


// core/instance/sate.js文件
export function initState(vm: Component) 
    vm._watchers = []
    const opts = vm.$options
    ....
    if (opts.watch && opts.watch !== nativeWatch) 
      initWatch(vm, opts.watch)
    
  

在_init方法中会对我们传入的选项配置进行处理,然后放入到实例的$options选项中。到了initState时,就会以我们编写的watch配置选项作为参数调用initWatch方法对用户watcher进行初始化。

function initWatch(vm: Component, watch: Object) 
  for (const key in watch) 
    const handler = watch[key]
    ...
    createWatcher(vm, key, handler)
  

这里会直接调用createWatcher方法

function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) 
  if (isPlainObject(handler)) 
   options = handler
   handler = handler.handler
  
  ...
  return vm.$watch(expOrFn, handler, options)

这里留意下handler处理部分,如果我们的watch的回调函数写法是标准带handler的那种写法的话,需要将配置项里面的handler拿出来作为本函数里面的handler函数。

跟着就是调用vue的原型函数$watch方法

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function 
    const vm: Component = this
    ...
    options = options || 
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
  

这里注意options.user = true,这就是我们这个要创建的watcher叫做用户watcher的原因。

在调用Watcher构造函数的时候,我们这次提供了四个参数,值得留意的是,相比上两篇文章分析的渲染watcher和计算属性watcher,我们这次提供了第三个参数,即回调函数,该函数将会在依赖的响应式属性变化时被watcher执行。而这个cb,就是我们上面的handler,也就是我们自己写的那个watch的handler函数。

下面看下用户watche的构造过程

export default class Watcher 
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) 
    this.vm = vm;
    ...
    // options
    if (options) 
      ...
      this.user = !!options.user;
     

    this.cb = cb;
    ...
    if (typeof expOrFn === "function") 
      this.getter = expOrFn;
     else 
      this.getter = parsePath(expOrFn);
      ...
    
    this.value = this.lazy ? undefined : this.get();
  

Watcher的构造函数其实我们看了很多遍了,只是为了方便分析,针对不同使用情况下的watcher,我们就会把和它不相关的代码给省略掉。这里也一样,我们这里只保留和今天学习用户Watcher相关的核心代码。

  • 首先,这里把用户watcher的标识位存储到user成员变量中
  • 然后,将我们编写的handler函数保存到cb成员变量中
  • 跟着,因为我们提供的expOrFn是’counter’或者’counter.total’这样的字符串,所以会调用parsePath来返回一个getter函数,该函数会接受一个对象,然后把对象里面的expOrFn为key的对象返回,比如接收vm对象,然后将下面的counter属性的的值给返回。该函数我们在《Vue源码分析基础之响应式原理》的4.3章节中已经做过详细的分析,这里就不再赘述。
  • 跟着,调用get成员方法去进行依赖收集

下面我们将再次看下get成员方法的代码

get() 
    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);
      
      popTarget();
      this.cleanupDeps();
    
    return value;
  

这个方法我们此前的文章也分析过很多次了,目的就是调用getter,即读取下我们监控的响应式属性,在我们的示例中,就是读取一下vm中的count,从而触发其getter,进而将我们的用户watcher加入到该响应式属性的依赖deb.subs中,即完成依赖收集过程。

如此一来,在下次有人修改了该响应式数据之后,将会遍历该响应式数据的所有订阅者,即所有依赖的watcher,包含这里的用户watcher,然后通知这些watcher去做事情,或者是像计算属性watcher那样去更新数据,或者像渲染watcher那样去重新计算虚拟DOM然后更新页面,或者是我们这里的用户watcher,则会重新执行下我们自己编写的handler回调。

以上,就是用户watcher的源码的简单分析。而这一系列的三个不同用途的watcher的源码分析也就告一段落了。多谢大家的观看和支持吧。

我是@天地会珠海分舵,「青葱日历」和「三日清单」作者。能力一般,水平有限,觉得我说的还有那么点道理的不妨点个赞关注下!

以上是关于Vue源码之用户watcher的主要内容,如果未能解决你的问题,请参考以下文章

Vue源码之渲染watcher

Vue源码之渲染watcher

Vue源码之计算属性watcher

Vue源码之计算属性watcher

Vue源码实现之watcher拾遗

Vue源码实现之watcher拾遗