Vue watch原理源码层深入讲解

 更新时间:2022年10月21日 15:23:05   作者:Young soul2  
watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。那么本文就来带大家从源码理解 watch 的工作流程,以及依赖收集和深度监听的实现

由于我在从源码看vue(v2.7.10)的computed的实现原理中详细的讲解过computed的实现,本篇跟computed的原理类似。我就带大家简单分析一下。

添加依赖

代码如下:

<template>
  <div>
    {{a}}
    <button @click="addModule">新增</button>
  </div>
</template>
<script>
export default {
  name: "TestWebpackTest",
  mounted() {
    console.log(this);
  },
  data() {
    return {
      num: 1,
      a:2
    };
  },
  watch:{
    a: function (val, oldVal) {
      console.log(val, oldVal)
    },
  },  
  methods: {
    addModule() {
      this.a++;
    }
  }
};
</script>
<style lang="scss">
div {
  .test {
    width: 10px;
    height: 15px;
    background-color: blue;
  }
}
</style>

初始化watch方法发生在initState(vm)方法中,该方法执行initWatch方法:

function initState(vm) {
  var opts = vm.$options;
  ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
...
function initWatch(vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    if (isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    }
    else {
      createWatcher(vm, key, handler);
    }
  }
}
...
function createWatcher(vm, expOrFn, handler, options) {
   if (isPlainObject(handler)) {
     options = handler;
     handler = handler.handler;
   }
   if (typeof handler === 'string') {
     handler = vm[handler];
   }
   return vm.$watch(expOrFn, handler, options);
 }

initWatch函数会判断当前的watch方法a是不是个数组,不是数组执行else的 createWatcher(vm, key, handler)方法。主要执行vm.$watch(expOrFn, handler, options)方法:

Vue.prototype.$watch = function (expOrFn, cb, options) {
  var vm = this;
  ...
  options = options || {};
  options.user = true;
  var watcher = new Watcher(vm, expOrFn, cb, options);
  ...
  return function unwatchFn() {
    watcher.teardown();
  };
};

$watch方法主要是实例化了一个观察者Watcher:

function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
      ...
      this.dirty = this.lazy; // for lazy watchers
      this.deps = [];
      this.newDeps = [];
      ...
      // expOrFn = 'a'
      if (isFunction(expOrFn)) {
        this.getter = expOrFn;
      }
      else {
        this.getter = parsePath(expOrFn);
        ...
      }
      this.value = this.lazy ? undefined : this.get();
    }

由于expOrFn是字符串’a’,所以会执行 parsePath(expOrFn)方法:

function parsePath(path) {
  ...
  // ['a']
  var segments = path.split('.');
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj)
        return;
      obj = obj[segments[i]];
    }
    return obj;
  };
}

该方法返回一个函数,并赋值给watcher实例的getter方法。此时执行完this.getter = parsePath(expOrFn)方法,继续执行this.get()方法:

Watcher.prototype.get = function () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  }
  catch (e) {
   ...
  }
  finally {
    ...
    popTarget();
    this.cleanupDeps();
  }
  return value;
};

该方法执行pushTarget将Dep.target设置为当前观察者(watcher),然后执行 this.getter.call(vm, vm)方法,由于getter方法是parsePath(expOrFn)方法的返回函数:

// obj = 'vm' segments = ['a']
function (obj) {
  for (var i = 0; i < segments.length; i++) {
     if (!obj)
       return;
     obj = obj[segments[i]];
   }
   return obj;
 }

这里可以看出遍历watch方法的key值,这里是’a’,然后去当前的vm实例中获取该变量,触发该变量的getter方法从而建立该观察者和该变量之间的关系。

当前的watch方法a有一个deps放的就是发布者,该发布者的更新要触发订阅者,所以subs里面放的是watch方法a的watcher。

触发依赖

触发依赖的过程很简单,当数据改变时会触发变量的setter方法。会获取该变量的订阅者,并执行订阅者中的update方法:

Dep.prototype.notify = function (info) {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  ...
  for (var i = 0, l = subs.length; i < l; i++) {
    if (info) {
      var sub = subs[i];
      sub.onTrigger &&
        sub.onTrigger(__assign({ effect: subs[i] }, info));
    }
    subs[i].update();
  }
};
Watcher.prototype.update = function () {
 // this.lazy = false
  if (this.lazy) {
    this.dirty = true;
  }
  ...
  else {
    queueWatcher(this);
  }
};

最后会执行queueWatcher(this)方法,接下来一系列的过程就是异步执行watcher.run()方法:

Watcher.prototype.run = function () {
  if (this.active) {
    var value = this.get();
    if (value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        var info = "callback for watcher \"".concat(this.expression, "\"");
        // this.cb是watch方法a的函数
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
      }
      else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};

该方法获取将新值和旧值放入invokeWithErrorHandling函数中:

function invokeWithErrorHandling(handler, context, args, vm, info) {
  var res;
  try {
    res = args ? handler.apply(context, args) : handler.call(context);
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
      res._handled = true;
    }
  }
  catch (e) {
    handleError(e, vm, info);
  }
  return res;
}

该方法执行回调,至此watch方法a执行完毕。

总结

  • 初始化执行initWatch(vm, opts.watch)方法创建watcher并定义了watcher的getter方法,随后触发getter方法去触发变量的getter方法建立变量和watcher相互之间的联系。
  • 变量发生变化会触发变量的订阅者的update方法并执行run方法去获取最新的值,并通过执行订阅者的cb方法传入新旧值。

到此这篇关于Vue watch原理源码层深入讲解的文章就介绍到这了,更多相关Vue watch原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue3中使用router4 keepalive的问题

    vue3中使用router4 keepalive的问题

    这篇文章主要介绍了vue3中使用router4 keepalive的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • VueJs与ReactJS和AngularJS的异同点

    VueJs与ReactJS和AngularJS的异同点

    这篇文章主要为大家详细介绍了VueJs与ReactJS和AngularJS的异同点,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • Vue.js系列之项目搭建(1)

    Vue.js系列之项目搭建(1)

    今天要讲讲Vue2.0了。最近将公司App3.0用vue2.0构建了一个web版,因为是第一次使用vue,而且一开始使用的时候2.0出来一个月不到,很多坑都是自己去踩的,现在项目要上线了,所以记录一些过程
    2017-01-01
  • 解决vue-router 切换tab标签关闭时缓存问题

    解决vue-router 切换tab标签关闭时缓存问题

    这篇文章主要介绍了解决vue-router 切换tab标签关闭时缓存问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • 详解vue的数据劫持以及操作数组的坑

    详解vue的数据劫持以及操作数组的坑

    这篇文章主要介绍了vue的数据劫持以及操作数组的坑,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • vue项目中实现图片预览的公用组件功能

    vue项目中实现图片预览的公用组件功能

    小编接到查看影像的功能需求,根据需求,多个组件需要用到查看影像的功能,所以考虑做一个公用组件,通过组件传值的方法将查看影像文件的入参传过去。下面小编通过实例代码给大家分享vue项目中实现图片预览的公用组件功能,需要的朋友参考下吧
    2018-10-10
  • vue获取元素宽、高、距离左边距离,右,上距离等还有XY坐标轴的方法

    vue获取元素宽、高、距离左边距离,右,上距离等还有XY坐标轴的方法

    今天小编就为大家分享一篇vue获取元素宽、高、距离左边距离,右,上距离等还有XY坐标轴的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09
  • 详解VueJS 数据驱动和依赖追踪分析

    详解VueJS 数据驱动和依赖追踪分析

    这篇文章主要介绍了详解VueJS 数据驱动和依赖追踪分析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • 在Uni中使用Vue的EventBus总线机制操作

    在Uni中使用Vue的EventBus总线机制操作

    这篇文章主要介绍了在Uni中使用Vue的EventBus总线机制操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • vue created钩子函数与mounted钩子函数的用法区别

    vue created钩子函数与mounted钩子函数的用法区别

    这篇文章主要介绍了vue created钩子函数与mounted钩子函数的用法区别,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11

最新评论