Vue 中的 watch 侦听器

74 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

文章首发于语雀,如有问题欢迎评论指正,感谢!

watchcomputed的区别

1、computed计算属性,关注点在模版,主要是抽离和复用模版中复杂的数据逻辑。

特点:当函数内部的依赖发生变化的后,数据才会重新计算。

2、watch侦听器,关注点在数据(datacomputed中的数据)更新,主要负责给数据增加坚挺,当数据更新的时候监听器函数就会执行。

特点:数据更新的时候,需要完成什么样的逻辑。

export default {
  data(){
    return{
      result: 0
    }
  }
  watch: {
    // 可以获取到数据更新的新值和旧值
    result(newVal, oldVal) {
      console.log(newVal, oldVal);
    }
  }
};

简单模拟 watch 的原理

例如我们实现一个简单的watch侦听器,先看一下我们的结构目录:

├─ 03-watch
  ├─ Vue.js
  ├─ index.html
  ├─ main.js
  ├─ reactive.js
  └─ watcher.js

首先我们先去main.js文件中写出我们大致的结构:

import Vue from "./Vue.js";

const vm = new Vue({
  data() {
    return {
      result: 0
    };
  },
  watch: {
    // 监听 data 中的 result
    result(newVal, oldVal) {
      console.log("watch result:", newVal, oldVal);
    }
  }
});

console.log(vm);

vm.result = 100;
vm.result = 200;

console.log(vm.result);

接下来我们专注Vue.js文件就可以了,我们采用ES6的类来书写:

class Vue {
  constructor(options) {
    // 这里是 Vue 类的入口
  }

  init(vm, watch) {
    // 这里主要处理一些初始化的数据
  }

  initData(vm) {
    // 处理 data() 中的响应式数据
  }

  initWatcher(vm, watch) {
    // 处理 watch 中的监听数据
  }
}

export default Vue;

以上就是我们今天Vue文件的大概结构,下面我们先来处理data中的数据:

import { reactive } from "./reactive.js";

class Vue {
  constructor(options) {
    // 我们在实例 Vue 的时候,传递进来一个对象,所以我们可以进行解构
    const { data, watch } = options;

    // 因为 data 中的数据是可以直接被访问到的,就像这样 vm.result
    // 所以我们需要把 data 中的数据挂在到 Vue 实例上面
    this.$data = data();
    this.init(this, watch);
  }

  init(vm, watch) {
    // 调用初始化 data
    this.initData(vm);
  }

  initData(vm) {
    // 我们把处理 data 这部分的代码,抽离出去为一个 reactive.js 文件
    // reactive 方法介绍 3 个参数:当前实例、当 get data 中数据的回调、当 set data 中数据的回调
    reactive(vm, (key, value) => {}, (key, newVal, oldVal) => {});
  }
}

export default Vue;

处理data中的数据:

export function reactive(vm, __get__, __set__) {
  const _data = vm.$data;

  // 遍历 data 中的数据,也就是 { result: 0 }
  for (const key in _data) {
    // 添加拦截
    Object.defineProperty(vm, key, {
      get() {
        // 当获取 vm.result 的时候就会执行回调,且返回数据
        __get__(key, _data[key]);
        return _data[key];
      },
      set(newVal) {
        // 当给 vm.result 设置值的时候就会执行回调,且赋值
        const oldVal = _data[key];

        _data[key] = newVal;
        __set__(key, newVal, oldVal);
      }
    });
  }
}

到这里我们已经处理好data的数据了,实例数据如下:


根据reactive.jsset方法,我们这样打算:既然到result改变的时候,我们可以拦截到,且执行了回调方法,那我们在回调了去执行watch不就可以了吗?

那么我们就继续来写watcher.js文件:

class Watcher {
  constructor() {
    this.watchers = [];
    /**
     * watchers 的数据结构:
     * 	{
     *  	key: result
     *  	fn: result(newVal, oldVal)
     * 	}
     *
     * 通过 addWatcher(vm, watcher, key) 去往 watchers 里面添加数据
     *  */
  }

  // 往 watchers 里面添加数据
  addWatcher(vm, watcher, key) {
    this._addWatchProp({
      key,
      fn: watcher[key].bind(vm)
    });
  }

  invoke(key, newVal, oldVal) {
    // 用 addWatcher 保存的值去遍历对比
    this.watchers.map((item) => {
      if (item.key === key) {
        // 调用 result() 传入新值、旧值
        item.fn(newVal, oldVal);
      }
    });
  }

  _addWatchProp(watchProp) {
    this.watchers.push(watchProp);
  }
}

export default Watcher;

接着我们在Vue.js文件中引入watcher.js

import { reactive } from "./reactive.js";
import Watcher from "./watcher.js";

class Vue {
  constructor(options) {
    // 我们在实例 Vue 的时候,传递进来一个对象,所以我们可以进行解构
    const { data, watch } = options;

    // 因为 data 中的数据是可以直接被访问到的,就像这样 vm.result
    // 所以我们需要把 data 中的数据挂在到 Vue 实例上面
    this.$data = data();
    this.init(this, watch);
  }

  init(vm, watch) {
    // 调用初始化 data
    this.initData(vm);

    // 初始化 watch,且传入实例对象和 watch 对象(也就是 { watch: { result:xxx }})
    // 然后我们就能得到一个 watcher 的实例,并把它保存到实例对象上面,方面后面直接调用执行
    const watcherIns = this.initWatcher(vm, watch);
    this.$watch = watcherIns.invoke.bind(watcherIns);
  }

  initData(vm) {
    // 我们把处理 data 这部分的代码,抽离出去为一个 reactive.js 文件
    // reactive 方法介绍 3 个参数:当前实例、当 get data 中数据的回调、当 set data 中数据的回调
    reactive(vm, (key, value) => {}, (key, newVal, oldVal) => {
      // 当设置 vm.result 的时候我们就去执行 watch 的 invoke 方法
      this.$watch(key, newVal, oldVal);
    });
  }

  initWatcher(vm, watch) {
    // 枚举 watch 然后新增监听器
    // 返回实例,实例有调用 watch 的方法,执行监听器
    const watcherIns = new Watcher();

    for (const key in watch) {
      // 实例、watch 对象、result
      watcherIns.addWatcher(vm, watch, key);
    }

    return watcherIns;
  }
}

export default Vue;

这样就算完成了,当我们去改变vm.result的时候,watch中的result()方法就会被执行。


最后献上源码地址