【vue源码】watch实现原理

1,419 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前期准备和资料

  • 本文基于vue2.6.14版本进行解读
  • demo地址传送门(为了方便查看源码执行过程,请在debugger下进行调试查看,在demo关键代码处已打debugger)
  • demo相关代码
  • 源码解读中会有封装的部分公共方法,类似于:isPlainObject()请自行查看源码

解读

前端相关代码

<div id="app">
    <input type="text" v-model="name">
    <p>{{name}}</p>
    <button v-on:click="changeName">改变名字</button>
</div>
<script>
    let vue = new Vue({
        el: '#app',
        data: {
            name: '张三'
        },
        watch: {
            name: function (val, oldVal) {
                console.log(`newVal:${val}; oldVal:${oldVal}`);
            },
        },
        methods:{
            changeName(){
                this.name = this.name === '张三' ? '李四' : '张三'
            }
        },
    });
</script>

在刚初始化页面的时候会调用vue中的initState()方法,在initState中分别调用初始化props、methods、data、computed和wantch相关的方法。代码如下所示:

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    // 此处只需要关注这里,在此处调用initWatch方法去初始化watch
    debugger
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

image.png 接下来我们来看看initWatch()方法中都做了什么,initWatch接收vm和watch(初始化时watch中的内容)两个参数,在此处遍历watch中需要监听的数据,判断watch中的值是否是一个数组(可以查看watch的使用可以定义为一个函数或者一个数组传送门),并分别调用createWatcher方法去创建对应的观察者。代码如下:

  function initWatch (vm, watch) {
    debugger
    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);
      }
    }
  }

image.png 在createWatcher方法中判断watch中的hander(指的是案例watch对象中name对应的function (val, oldVal) {}),isPlainObject方法判断是对象类型,如果是一个对象则设置options参数为handler,并对handler进行递归;如果hangdler只是一个字符串,则把vm实例中对应值赋值(可能是对应的method中的方法名)给handler。最后执行vm中的$watch方法。代码如下:

function createWatcher (vm,expOrFn,handler,options) {
    debugger
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }

image.png 在vue的原型中定义$watch方法,解读放到代码注释中(new Watcher、pushTarget、invokeWithErrorHandling、popTarget、watcher.teardown后续紧跟)代码如下:

Vue.prototype.$watch = function (expOrFn,cb,options) {
      debugger
      var vm = this;
      // 判断cb回调函数是否是对象类型,如果是的话继续回调createWatcher方法
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      // options赋值
      options = options || {};
      options.user = true;
      // 调用new Watcher创建对应的观察者,收集依赖,监听数据变化并作出对应的处理
      var watcher = new Watcher(vm, expOrFn, cb, options);
      // 判断是否有immediate立即执行属性,如果有的话,会立即执行一次cb
      if (options.immediate) {
        var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
        pushTarget();
        invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
        popTarget();
      }
      // 返回unwatchFn去取消观察数据,把watcher实例从当前正在观察的状态的依赖列表中移除
      return function unwatchFn () {
        watcher.teardown();
      }
    };

image.png

创建Watcher实例实现对数据变换的监听和处理。除此以外,Watcher原型还还定义了get评估getter并重新收集依赖项,addDep将依赖项添加到此指令,cleanupDeps清理依赖项,update在依赖项更改时调用,depend依赖于此观察者收集的所有DEP,teardown从所有依赖项的列表中删除self等方法。代码如下:

  var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
    debugger
    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是函数,则把它赋值给getter,如果不是则使用parsePath读取属性路径中的数据,例如a.b.c
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn("观察者只接受简单的点分隔路径", vm);
      }
    }
    // 调用Watcher原型上的get()方法进行依赖收集
    this.value = this.lazy ? undefined : this.get();
  };

image.png Watcher原型中的get方法会进行依赖收集,代码如下:

  Watcher.prototype.get = function get () {
    // 调用pushTarget方法,评估当前Watcher是否订阅过该dep,如果没有则进行依赖收集
    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 {
      // 深度观察的依赖
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

image.png

当我们在依赖中收集到自己订阅哪些dep后,就可以在$watch方法代码中使用unwatchFn方法通过调用watcher原型中的teardown方法从所有依赖项的列表中删除self。下面来看看teardown的代码:

  Watcher.prototype.teardown = function teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      var i = this.deps.length;
      // 循环订阅列表,执行他的removeSub方法,把自己从依赖列表中删除
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  };

更多细节方面(不同的回调方式,不同的使用方式)可自行下载代码调试