Vue中的侦听器(watch)
2021-9-4
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 03:23 PM
type
Post
status
Published
date
Sep 4, 2021
slug
summary
介绍Vue2和Vue3中侦听器watch的使用及实现方式
tags
Vue
Vue Watch
Vue源码
category
源码
icon
  1. 侦听器可以在组件中定义
 export default {    watch: {      a(newVal, oldVal) {        console.log("newVal, oldVal", newVal, oldVal)     }   }  }
  1. 侦听器也可以在全局定义
全局定义的侦听器将会返回一个unWatch, 通过这个函数来取消侦听
 const unWatch = vm.$watch('a', (newVal, oldVal) => {    console.log("newVal, oldVal", newVal, oldVal)  })  // 当需要取消侦听时  unWatch()

Vue3中的侦听器

  1. 侦听一个getter函数
 const state = reactive({ count: 0 })  watch(() => state.count, (count, prevCount) => {    // update when state.count changed  })
  1. 侦听一个响应式对象
 const count = ref(0)  watch(count, (count, prevCount) => {    // update when count changed  })
  1. 侦听多个响应式对象
 const count = ref(0)  const count2 = ref(2)  watch([count, count2], ([count, count2], [prevCount, prevCount2]) => {    // update when count or count2 changed  })

侦听器的实现方式

  1. 调用doWatch函数, 调用前进行了一些前置判断
  1. 标准化source, 也就是传入的第一个源参数
  • 当source是ref对象时, 则创建一个访问source.value的getter函数
  • 如果source是reactive对象, 则创建一个访问source的getter函数, 并设置deep为true(本质上是调用了traverse函数, 递归遍历深层依赖, 因此, 如果我们侦听了一个大的响应式对象时, 递归调用就会产生一定的性能损耗)
  • 如果source是一个函数, 则对source函数简单封装来作为getter函数来使用
假如, 我们需要侦听b的改变, 对于侦听对象的选择, 可以考虑进行以下的优化
 const state = reactive({    count: {      a: {        b: 1     }   }  })  watch(state.count.a, (newVal, oldVal) ={    // update when state.count.a changed  })
因为watch只能侦听一个响应式对象, 因此我们需要侦听到我们需要实际侦听的对象的上层响应式对象.
如果与b同级的对象有非常多, 其实这样也会有一定的性能损耗, 有没有办法只侦听b的变化呢? 这里就可以用到函数侦听了
 watch(() => state.count.a.b, (newVal, oldVal) ={    // update when state.count.a.b changed  })
  1. 构造回调函数, 处理回调函数
注册了一个onInvalidate的回调函数, 用来处理停止监听的逻辑
定义了一个旧值的初始值oldValue, 并处理为数组的格式
执行runner函数(其实这里就是之前在标准化source的getter函数), 并将值赋值给newValue, 作为新值
使用newValue, oldValue, onInvalidate三个参数作为参数, 调用callback
  1. 处理options(watch的第三个参数)基于参数创建scheduler函数
 interface WatchOptions extends WatchEffectOptions {    immediate?: boolean // default: false    deep?: boolean // default: false    flush?: 'pre' | 'post' | 'sync' // default: 'pre'    onTrack?: (event: DebuggerEvent) => void    onTrigger?: (event: DebuggerEvent) => void  }
如果immediate为true时, 将会在创建侦听器时立即执行一次回调
当flush为sync时, 表示是一个同步watcher, 即当数据变化时同步执行回调函数
当flush为pre时, 回调函数通过queueJob的方式在组件更新之前执行
当flush为post时, 回调函数会在组件更新完毕之后执行, 这个时候可以访问到更新之后的DOM
(小声BB: 好像flush的默认值来回改了好几次了)
  1. scheduler的核心内容
主要就是通过effect来创建核心的runner执行对象

Watch的异步队列机制

创建一个watcher时, flushprepost, 则watcher的回调函数就会异步执行
分别是通过queueJobqueuePostRenderEffect来将回调函数推入异步队列中
如果是sync, 就是直接同步进行执行了

queuePostRenderEffect的实现

创建了一个异步的任务队列
创建了一个任务队列执行完成之后的回调函数队列
定义queueJob函数, queuePostFlushCB函数, 分别push到对应队列, 生成一对一关系
执行queueFlush, 来对任务进行逐一执行. 主要就是通过isFlushPending来判断是否在执行中, 执行中则不执行, 否则使用nextTick来执行任务flushJobs(nexttick基于Promise.resolve().then来实现)

flushJobs的实现

设置isFlushPending为false, isFlushing为true
对queue进行了排序, 主要原因是组件的更新是应该先父后子, 因此父组件的effect id一定是小于子组件的, 如果组件在其父组件更新过程中卸载, 那么其更新则应该被跳过
遍历queue来逐步执行任务, 其中使用checkRecursiveUpdates来检测是否在watcher callback中又执行了监听对象的更新导致循环更新而产生OOM问题.
执行flushPostFlushCbs来执行queueJob对应的callback

WatchEffect

与Watch的不同点在于
  1. 侦听的源不同, watch侦听的是一个或者多个响应式对象, 或者一个getter函数, 而watchEffect则是侦听的一个普通函数, 内部访问了响应式对象, 不需要返回响应式对象.
  1. watchEffect没有回调函数, 当副作用函数中内部的响应式对象发生改变之后, 会再次执行这个副作用函数
  1. watchEffect会在定义之后立即执行
本质上则是通过定义之后立即执行副作用函数来获取响应式侦听对象, 并侦听对象变更来触发副作用函数

watchPostEffect

Alias of watchEffect() with flush: 'post' option.

watchSyncEffect

Alias of watchEffect() with flush: 'sync' option.
源码
  • Vue
  • Vue Watch
  • Vue源码
  • Vue中Slot的实现机制SOLID原则
    目录