使用Vue逐步实现Watch属性详解

目录
  • watch
  • 初始化watch
  • deep、immdediate属性
  • 结语

watch

对于watch的用法,在Vue文档 中有详细描述,它可以让我们观察data中属性的变化。并提供了一个回调函数,可以让用户在属性值变化后做一些事情。

watch对象中的value分别支持函数、数组、字符串、对象,较为常用的是函数的方式,当想要观察一个对象以及对象中的每一个属性的变化时,便会用到对象的方式。

下面是官方的一个例子,相信在看完之后就能对watch几种用法有大概的了解:

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // string method name
    b: 'someMethod',
    // the callback will be called whenever any of the watched object properties change regardless of their nested depth
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // the callback will be called immediately after the start of the observation
    d: {
      handler: 'someMethod',
      immediate: true
    },
    // you can pass array of callbacks, they will be called one-by-one
    e: [
      'handle1',
      function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
        /* ... */
      }
    ],
    // watch vm.e.f's value: {g: 5}
    'e.f': function (val, oldVal) { /* ... */ }
  }
})
vm.a = 2 // => new: 2, old: 1

初始化watch

在了解了watch的用法之后,我们开始实现watch

在初始化状态initState时,会判断用户在实例化Vue时是否传入了watch选项,如果用户传入了watch,就会进行watch的初始化操作:

// src/state.js
function initState (vm) {
  const options = vm.$options;
  if (options.watch) {
    initWatch(vm);
  }
}

initWatch中本质上是为每一个watch中的属性对应的回调函数都创建了一个watcher

// src/state.js
function initWatch (vm) {
  const { watch } = vm.$options;
  for (const key in watch) {
    if (watch.hasOwnProperty(key)) {
      const userDefine = watch[key];
      if (Array.isArray(userDefine)) { // userDefine是数组,为数组中的每一项分别创建一个watcher
        userDefine.forEach(item => {
          createWatcher(vm, key, item);
        });
      } else {
        createWatcher(vm, key, userDefine);
      }
    }
  }
}

createWatcher中得到的userDefine可能是函数、对象或者字符串,需要分别进行处理:

function createWatcher (vm, key, userDefine) {
  let handler;
  if (typeof userDefine === 'string') { // 字符串,从实例上取到对应的method
    handler = vm[userDefine];
    userDefine = {};
  } else if (typeof userDefine === 'function') { // 函数
    handler = userDefine;
    userDefine = {};
  } else { // 对象,userDefine中可能会包含用户传入的deep,immediate属性
    handler = userDefine.handler;
    delete userDefine.handler;
  }
  // 用处理好的参数调用vm.$watch
  vm.$watch(key, handler, userDefine);
}

createWatcher中对参数进行统一处理,之后调用了vm.$watch,在vm.$watch中执行了Watcher的实例化操作:

export function stateMixin (Vue) {
  // some code ...
  Vue.prototype.$watch = function (exprOrFn, cb, options) {
    const vm = this;
    const watch = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
  };
}

此时new Watcher时传入的参数如下:

  • vm: 组件实例
  • exprOrFnwatch选项对应的key
  • cbwatch选项中key对应的value中提供给用户处理逻辑的回调函数,接收keydata中的对应属性的旧值和新值作为参数
  • options{user: true, immediate: true, deep: true}immediatedeep属性当key对应的value为对象时,用户可能会传入

Watcher中会判断options中有没有user属性来区分是否是watch属性对应的watcher:

class Watcher {
  constructor (vm, exprOrFn, cb, options = {}) {
    this.user = options.user;
    if (typeof exprOrFn === 'function') {
      this.getter = this.exprOrFn;
    }
    if (typeof exprOrFn === 'string') { // 如果exprFn传入的是字符串,会从实例vm上进行取值
      this.getter = function () {
        const keys = exprOrFn.split('.');
        // 后一次拿到前一次的返回值,然后继续进行操作
        // 在取值时,会收集当前Dep.target对应的`watcher`,这里对应的是`watch`属性对应的`watcher`
        return keys.reduce((memo, cur) => memo[cur], vm);
      };
    }
    this.value = this.get();
  }

  get () {
    pushTarget(this);
    const value = this.getter();
    popTarget();
    return value;
  }

  // some code ...
}

这里有俩个重要的逻辑:

  • 由于传入的exprOrFn是字符串,所以this.getter的逻辑就是从vm实例上找到exprOrFn对应的值并返回
  • watcher实例化时,会执行this.get,此时会通过this.getter方法进行取值。取值就会触发对应属性的get方法,收集当前的watcher作为依赖
  • this.get的返回值赋值给this.value,此时拿到的就是旧值

当观察的属性值发生变化后,会执行其对应的set方法,进而执行收集的watch对应的watcherupdate方法:

class Watcher {

  // some code ...
  update () {
    queueWatcher(this);
  }

  run () {
    const value = this.get();
    if (this.user) {
      this.cb.call(this.vm, value, this.value);
      this.value = value;
    }
  }
}

和渲染watcher相同,update方法中会将对应的watch watcher去重后放到异步队列中执行,所以当用户多次修改watch属性观察的值时,并不会不停的触发对应watcher 的更新操作,而只是以它最后一次更新的值作为最终值来执行this.get进行取值操作。

当我们拿到观察属性的最新值之后,执行watcher中传入的回调函数,传入新值和旧值。

下面画图来梳理下这个过程:

deep、immdediate属性

当用户传入immediate属性后,会在watch初始化时便立即执行对应的回调函数。其具体的执行位置是在Watcher实例化之后:

Vue.prototype.$watch = function (exprOrFn, cb, options) {
  const vm = this;
  const watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
  if (options.immediate) { // 在初始化后立即执行watch
    cb.call(vm, watcher.value);
  }
};

此时watcher.value是被观察的属性当前的值,由于此时属性还没有更新,所以老值为undefined

如果watch观察的属性为对象,那么默认对象内的属性更新,并不会触发对应的回调函数。此时,用户可以传入deep选项,来让对象内部属性更新也调用对应的回调函数:

class Watcher {
  // some code ...
  get () {
    pushTarget(this);
    const value = this.getter();
    if (this.deep) { // 继续遍历value中的每一项,触发它的get方法,收集当前的watcher
      traverse(value);
    }
    popTarget();
    return value;
  }
}

当用户传入deep属性后,get方法中会执行traverse方法来遍历value中的每一个值,这样便可以继续触发value中属性对应的get方法,为其收集当前的watcher作为依赖。这样在value 内部属性更新时,也会通知其收集的watch watcher进行更新操作。

traverse的逻辑只是递归遍历传入数据的每一个属性,当遇到简单数据类型时便停止递归:

// traverse.js
// 创建一个Set,遍历之后就会将其放入,当遇到环引用的时候不会行成死循环
const seenObjects = new Set();

export function traverse (value) {
  _traverse(value, seenObjects);
  // 遍历完成后,清空Set
  seenObjects.clear();
}

function _traverse (value, seen) {
  const isArr = Array.isArray(value);
  const ob = value.__ob__;
  // 不是对象并且没有被观测过的话,终止调用
  if (!isObject(value) || !ob) {
    return;
  }
  if (ob) {
    // 每个属性只会有一个在Observer中定义的dep
    const id = ob.dep.id;
    if (seen.has(id)) { // 遍历过的对象和数组不再遍历,防止环结构造成死循环
      return;
    }
    seen.add(id);
  }
  if (isArr) {
    value.forEach(item => {
      // 继续遍历数组中的每一项,如果为对象的话,会继续遍历数组的每一个属性,即对对象属性执行取值操作,收集watch watcher
      _traverse(item, seen);
    });
  } else {
    const keys = Object.keys(value);
    for (let i = 0; i < keys.length; i++) {
      // 继续执行_traverse,这里会对 对象 中的属性进行取值
      _traverse(value[keys[i]], seen);
    }
  }
}

需要注意的是,这里利用Set来存储每个属性对应的depid。这样当出现环时,Set中已经存储过了其对应depid,便会终止递归。

结语

本文一步步实现了Vuewatch属性,并对内部的实现逻辑提供了笔者相应的理解 。到此这篇关于使用Vue逐步实现Watch属性详解的文章就介绍到这了,更多相关Vue Watch属性内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue3中watch和watchEffect实战梳理

    目录 前言 watch watch监听单个数据 watch监听多个数据 watch监听对象 watch监听对象的某一个值 watchEffect watchEffect副作用 停止监听 区别 前言 watch和watchEffect都是vue3中的监听器,但是在写法和使用上是有区别的,这篇文章主要是介绍一下watch和watchEffect的使用方法以及他们之间的区别. watch watch监听单个数据 <template> <input type="text" v

  • Vue 中的 computed 和 watch 的区别详解

    目录 computed 注意 应用场景 watch 总结 computed computed 看上去是方法,但是实际上是计算属性,它会根据你所依赖的数据动态显示新的计算结果.计算结果会被缓存,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算. 1)下面是一个比较经典简单的案例 <template> <div class="hello"> {{ful

  • Vue watch原理源码层深入讲解

    目录 添加依赖 触发依赖 总结 由于我在从源码看vue(v2.7.10)的computed的实现原理中详细的讲解过computed的实现,本篇跟computed的原理类似.我就带大家简单分析一下. 添加依赖 代码如下: <template> <div> {{a}} <button @click="addModule">新增</button> </div> </template> <script> exp

  • 老生常谈Vue中的侦听器watch

    目录 一.侦听器watch 1.1.初识侦听器watch 1.2.Vue的data的watch 1.3.Vue的watch侦听选项 一.侦听器watch (思维导图不太完善,因为是按照自己看懂的方式记的,如有错误,还请指正) 1.1.初识侦听器watch watch:观看,监视 那么什么是侦听器watch呢 开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中: 当数据变化时,template会自动进行更新来显示最新的数据: 但是在某些情况下,我们希望在

  • Vue3中watch监听使用详解

    目录 Vue2使用watch Vue3使用watch 情况1 情况2 情况3 情况4 情况5 特殊情况 总结 Vue2使用watch <template> <div>总合:{{ sum }}<button @click="sum++">点击累加</button></div> </template> <script> import { ref } from "vue"; export

  • Vue3源码分析侦听器watch的实现原理

    目录 watch 的本质 watch 的函数签名 侦听多个源 侦听单一源 watch 的实现 watch 函数 source 参数 cb 参数 options 参数 doWatch 函数 doWatch 函数签名 初始化变量 递归读取响应式数据 定义清除副作用函数 封装 scheduler 调度函数 设置 job 的 allowRecurse 属性 flush 选项指定回调函数的执行时机 创建副作用函数 执行副作用函数 返回匿名函数,停止侦听 总结 watch 的本质 所谓的watch,其本质就

  • 详解Vue3中侦听器watch的使用教程

    目录 watch 侦听器使用. 侦听器监听 reactive 监听多个参数执行各自逻辑 监听多个参数执行相同逻辑 上一节我们简单的介绍了一下vue3 项目中的计算属性,这一节我们继续 vue3 的基础知识讲解. 这一节我们来说 vue3 的侦听器. 学过 vue2 的小伙伴们肯定学习过侦听器,主要是用来监听页面数据或者是路由的变化,来执行相应的操作,在 vue3里面呢,也有侦听器的用法,功能基本一样,换汤不换药的东西. 侦听器是常用的 Vue API 之一,它用于监听一个数据并在数据变动时做一些

  • VUE 组件的计算属性详解

    目录 前言 计算属性 总结 前言 今天也是元气满满的一天,今天整理一下VUE组件的计算属性!~~ 开始我们的学习之旅 计算属性 先引用一张图 来看一下计算属性之间的关联: 注意: methods和computed里的东西不能重名 method:定义方法,调用方法使用currentTime(),需要带括号 computed:定义计算属性,调用属性使用currenTime2,不需要带括号:this.message是为了能够让currentTime2观察到数据变化 如何在方法中的值发生了变化,则缓存就

  • 使用Vue逐步实现Watch属性详解

    目录 watch 初始化watch deep.immdediate属性 结语 watch 对于watch的用法,在Vue文档 中有详细描述,它可以让我们观察data中属性的变化.并提供了一个回调函数,可以让用户在属性值变化后做一些事情. watch对象中的value分别支持函数.数组.字符串.对象,较为常用的是函数的方式,当想要观察一个对象以及对象中的每一个属性的变化时,便会用到对象的方式. 下面是官方的一个例子,相信在看完之后就能对watch的几种用法有大概的了解: var vm = new

  • Vue中的computed属性详解

    目录 插值表达式 methods computed 总结 今天来说说vue中的计算属性computed,为了更好的理解计算属性的好处,我们先通过一个案例来慢慢 了解计算属性,有如下案例:定义两个输入框以及一个span标签,span标签中的内容为两个输入框中的值,span标签中的内容随着输入框中的内容变化而变化 插值表达式 我们先用插值表达式的方法来实现这一效果 <body> <div id="app"> 姓: <input type="text&

  • Vue组件选项props实例详解

    前面的话 组件接受的选项大部分与Vue实例一样,而选项props是组件中非常重要的一个选项.在 Vue 中,父子组件的关系可以总结为 props down, events up.父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息.本文将详细介绍Vue组件选项props 静态props 组件实例的作用域是孤立的.这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据.要让子组件使用父组件的数据,需要通过子组件的 props 选项 使用Prop传递数据

  • vue中的scope使用详解

    我们都知道vue slot插槽可以传递任何属性或html元素,但是在调用组件的页面中我们可以使用 template scope="props"来获取插槽上的属性值,获取到的值是一个对象. 注意:scope="它可以取任意字符串"; 上面已经说了 scope获取到的是一个对象,是什么意思呢?我们先来看一个简单的demo就可以明白了~ 如下模板页面: <!DOCTYPE html> <html> <head> <title>

  • vue自定义指令directive实例详解

    下面给大家介绍vue自定义指令directive,具体内容如下所示: 官网截图实例 vue除了一些核心的内部定义的指令(v-model,v-if,v-for,v-show)外,vue也允许用户注册自己的一些功能性的指令,有时候你实在是要对Dom操作,这个时候是自定义指令最合适的了. 来直接看例子:当页面加载时使得元素获得焦点(autofocus 在移动版 Safari 是不支持的),就是当页面加载好了,不做任何的操作使得表单自动获得焦点,光标自动在某个表单上代码如下: Vue.directive

  • Vue 中mixin 的用法详解

    说下我对vue中mixin的一点理解 vue中提供了一种混合机制--mixins,用来更高效的实现组件内容的复用.最开始我一度认为这个和组件好像没啥区别..后来发现错了.下面我们来看看mixins和普通情况下引入组件有什么区别? 组件在引用之后相当于在父组件内开辟了一块单独的空间,来根据父组件props过来的值进行相应的操作,单本质上两者还是泾渭分明,相对独立. 而mixins则是在引入组件之后,则是将组件内部的内容如data等方法.method等属性与父组件相应内容进行合并.相当于在引入后,父

  • vue 注册组件的使用详解

    一.介绍 组件系统是Vue.js其中一个重要的概念,它提供了一种抽象,让我们可以使用独立可复用的小组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树 那么什么是组件呢? 组件可以扩展HTML元素,封装可重用的HTML代码,我们可以将组件看作自定义的HTML元素. 二.如何注册组件 Vue.js的组件的使用有3个步骤:创建组件构造器.注册组件和使用组件. 下面用代码演示这三步 <!DOCTYPE html> <html> <body> <div id=&q

  • vue双向绑定及观察者模式详解

    在Vue中,使用了Object.defineProterty()这个函数来实现双向绑定,这也就是为什么Vue不兼容IE8 1 响应式原理 让我们先从相应式原理开始.我们可以通过Object.defineProterty()来自定义Object的getter和setter 从而达到我们的目的. 代码如下 function observe(value, cb) { Object.keys(value).forEach((key) => defineReactive(value, key, value

  • Vue中$refs的用法详解

    说明:vm.$refs 一个对象,持有已注册过 ref 的所有子组件(或HTML元素) 使用:在 HTML元素 中,添加ref属性,然后在JS中通过vm.$refs.属性来获取 注意:如果获取的是一个子组件,那么通过ref就能获取到子组件中的data和methods <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>D

随机推荐