vue中的wactch可以监听到data的变化,执行定义的回调,在某些场景是很有用的,本文将深入源码揭开watch额面纱

  • version: v2.5.17-beta.0
  • 阅读本文需读懂vue数据驱动部分

当改变data值,同时会引发副作用时,可以用watch。比如:有一个页面有三个用户行为会触发this.count的改变,当this.count改变,需要重新获取list值,这时候就可以用watch轻松完成

  1. new Vue({
  2. el: '#app',
  3. data: {
  4. count: 1,
  5. list: []
  6. },
  7. watch: {
  8. // 不管多少地方改变count,都会执行到这里去改变list的值
  9. count(val) {
  10. ajax(val).then(list => {
  11. this.list = list;
  12. })
  13. }
  14. },
  15. methods: {
  16. // 点击+1,count + 1,刷新列表
  17. handleClick() {
  18. this.count += 1;
  19. },
  20. // 点击重置,count = 1,刷新列表
  21. handleReset() {
  22. this.count = 1;
  23. },
  24. // 点击随机, count随机数,刷新列表
  25. handleRamdon() {
  26. this.count = Math.ceil(Math.random() * 10);
  27. }
  28. }
  29. })

这样的好处就是把所有源头聚集到了watch中,不需要在多个count改变的地方手动去调用方法,减少代码冗余。

watch的写法有多种,以上案例是最常见的一种方法,接下来介绍所有写法。

  1. new Vue({
  2. data: {
  3. count: 1
  4. },
  5. watch: {
  6. count() {
  7. console.log('count改变')
  8. }
  9. }
  10. })

最常见的写法,count改变时将会触发传值的回调函数

  1. new Vue({
  2. data: {
  3. count: 1
  4. },
  5. watch: {
  6. count: [
  7. () => {
  8. console.log('count改变')
  9. },
  10. () => {
  11. console.log('count watch2')
  12. }
  13. ]
  14. }
  15. })

传数组,count改变后会依次执行数组内每一个回调函数

  1. new Vue({
  2. data: {
  3. count: 1
  4. },
  5. watch: {
  6. count: 'handleChange'
  7. },
  8. methods: {
  9. handleChange(val) {
  10. console.log('count改变了')
  11. }
  12. }
  13. })

我们也可以传值字符串handleChange,然后在methods写handleChange函数的逻辑,同样可以做到count改变执行handleChange

  1. new Vue({
  2. data: {
  3. count: 1
  4. },
  5. watch: {
  6. count: {
  7. handler() {
  8. console.log('count改变')
  9. }
  10. }
  11. }
  12. })

可以传值对象,该对象包含一个handler函数,当count改变时,会执行此handler函数,为什么多此一举需要包装一层对象呢?存在即合理,是有其特殊作用的。

watch为监听属性的变化,调用回调函数,因此,在初始化时,并不会触发,在初始化后属性改变才触发,如果想要初始时也要触发watch,那就需要传值对象,如下:

  1. new Vue({
  2. data: {
  3. count: 1
  4. },
  5. watch: {
  6. count: {
  7. immediate: true, // 加此属性
  8. handler() {
  9. console.log('count改变')
  10. }
  11. }
  12. }
  13. })

传的对象有immediate属性为true,则watch会立刻触发。

本节进行源码分析,探索watch的真面貌

  1. // 初始化
  2. function initState (vm) {
  3. vm._watchers = [];
  4. var opts = vm.$options;
  5. if (opts.data) {
  6. initData(vm);
  7. }
  8. if (opts.watch && opts.watch !== nativeWatch) {
  9. initWatch(vm, opts.watch);
  10. }
  11. }
  12. // watch初始化
  13. function initWatch (vm, watch) {
  14. for (var key in watch) {
  15. var handler = watch[key];
  16. if (Array.isArray(handler)) {
  17. for (var i = 0; i < handler.length; i++) {
  18. createWatcher(vm, key, handler[i]);
  19. }
  20. } else {
  21. createWatcher(vm, key, handler);
  22. }
  23. }
  24. }

从vue的执行流程,读到了initWatch函数,此函数的用法很清晰,将传入的每一个watch属性执行createWatcher处理。如果传值是数组,则遍历去调用。

下面看一下createWatcher函数

  1. function createWatcher (
  2. vm,
  3. expOrFn,
  4. handler,
  5. options
  6. ) {
  7. // 如果是对象,则处理
  8. if (isPlainObject(handler)) {
  9. // 将对象缓存,给$watch函数
  10. options = handler;
  11. handler = handler.handler;
  12. }
  13. if (typeof handler === 'string') {
  14. handler = vm[handler];
  15. }
  16. return vm.$watch(expOrFn, handler, options)
  17. }

createWatcher中做了兼容处理:

  1. 如果handler是个对象,则进行一步转换;
  2. 如果handler是字符串,则取vue实例的方法(methods里声明)
  3. 最后调用实例的$watch方法
  1. Vue.prototype.$watch = function (
  2. expOrFn,
  3. cb,
  4. options
  5. ) {
  6. var vm = this;
  7. if (isPlainObject(cb)) {
  8. return createWatcher(vm, expOrFn, cb, options)
  9. }
  10. options = options || {};
  11. options.user = true;
  12. var watcher = new Watcher(vm, expOrFn, cb, options);
  13. if (options.immediate) {
  14. cb.call(vm, watcher.value);
  15. }
  16. return function unwatchFn () {
  17. watcher.teardown();
  18. }
  19. };

vm.$watch里是最终实现watch的部分,在这里仍然做了兼容判断,如果是对象,回调createWatcher;接下来就最重要的new Watcher。

$watch的功能其实就是new了一个Watcher,那么,我们在代码里实现的一切响应,都来自于Watcher,下面看一下watch里的Watcher

Watcher是vue数据驱动核心部分的一员,他承载着依赖收集与事件的触发。下面重点解读一下watch的Watcher实现。

  1. if (typeof expOrFn === 'function') {
  2. this.getter = expOrFn;
  3. } else {
  4. // parsePath去解析expOrFn并返回getter函数
  5. this.getter = parsePath(expOrFn);
  6. if (!this.getter) {
  7. this.getter = function () {};
  8. }
  9. }

watch Watcher会执行上面部分,parsePath源码可自行查看,他会将obj.a这种写法兼容, 最终是返回需要监听的属性的getter函数

  1. if (this.computed) {
  2. this.value = undefined;
  3. this.dep = new Dep();
  4. } else {
  5. // 执行get方法
  6. this.value = this.get();
  7. }

拿到getter后,会执行this.get方法:

  1. Watcher.prototype.get = function get () {
  2. // 加入Dep.target
  3. pushTarget(this);
  4. var value;
  5. var vm = this.vm;
  6. try {
  7. value = this.getter.call(vm, vm); // 执行
  8. } catch (e) {
  9. } finally {
  10. popTarget();
  11. this.cleanupDeps();
  12. }
  13. return value
  14. };

以上为get方法,内容简单,但是做的事情举足轻重,他不仅做了值的获取,还做了依赖收集。

pushTarget会将当前watchWatcher赋值到Dep.target中,然后执行this.getter函数,要监听的属性如count会触发他的get钩子,与此同时会进行收集依赖,收集到的依赖就是前面Dep.target也就是当前的watchWatcher

正因为有上面的依赖收集,使count属性有了此watchWatcher的依赖,当this.count改变时,会触发set钩子,进行事件分发,从而执行回调函数

  1. Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
  2. var value = this.get();
  3. if (
  4. value !== this.value ||
  5. isObject(value) ||
  6. this.deep
  7. ) {
  8. // set new value
  9. var oldValue = this.value;
  10. this.value = value;
  11. this.dirty = false;
  12. if (this.user) {
  13. try {
  14. cb.call(this.vm, value, oldValue);
  15. } catch (e) {
  16. handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
  17. }
  18. } else {
  19. cb.call(this.vm, value, oldValue);
  20. }
  21. }
  22. };

上面就是this.count改变时,最终调用的方法,在这里会执行this.cb,也就是定义的watch的回调函数,会把value/oldValue传递过去

前面说到,watch只会在监听的属性改变值后才会触发回调,在初始化时不会执行回调,如果想要一开始初始化就执行回调,需要传参对象,并immediate为true,实现原理已经在创建Watcher贴出来了

  1. if (options.immediate) {
  2. cb.call(vm, watcher.value);
  3. }

创建watcher时,如果immediate为真值,会直接执行回调函数

computed是计算属性,watch是监听属性变化,有些场景计算属性做的事情,watch也可以做,当然要尽量用computed去做,为什么?

  1. new Vue({
  2. data: {
  3. num: 1,
  4. sum: 2
  5. },
  6. watch: {
  7. num(val) {
  8. this.sum = val + 1;
  9. }
  10. }
  11. })

watch实现需要声明2个data属性num 和 sum,2个都会加入数据驱动,当num改变后,num和sum都触发了set钩子。
而computed不会,computed只会触发num的set钩子,因为sum根本没有声明,num改变后是动态计算出来的。

版权声明:本文为xujiazheng原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xujiazheng/p/12188615.html