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
- 侦听器可以在组件中定义
export default { watch: { a(newVal, oldVal) { console.log("newVal, oldVal", newVal, oldVal) } } }
- 侦听器也可以在全局定义
全局定义的侦听器将会返回一个
unWatch
, 通过这个函数来取消侦听const unWatch = vm.$watch('a', (newVal, oldVal) => { console.log("newVal, oldVal", newVal, oldVal) }) // 当需要取消侦听时 unWatch()
Vue3中的侦听器
- 侦听一个getter函数
const state = reactive({ count: 0 }) watch(() => state.count, (count, prevCount) => { // update when state.count changed })
- 侦听一个响应式对象
const count = ref(0) watch(count, (count, prevCount) => { // update when count changed })
- 侦听多个响应式对象
const count = ref(0) const count2 = ref(2) watch([count, count2], ([count, count2], [prevCount, prevCount2]) => { // update when count or count2 changed })
侦听器的实现方式
- 调用
doWatch
函数, 调用前进行了一些前置判断
- 标准化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 })
- 构造回调函数, 处理回调函数
注册了一个
onInvalidate
的回调函数, 用来处理停止监听的逻辑定义了一个旧值的初始值
oldValue
, 并处理为数组的格式执行runner函数(其实这里就是之前在标准化source的getter函数), 并将值赋值给
newValue
, 作为新值使用
newValue
, oldValue
, onInvalidate
三个参数作为参数, 调用callback
- 处理
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的默认值来回改了好几次了)
- scheduler的核心内容
主要就是通过effect来创建核心的runner执行对象
Watch的异步队列机制
创建一个
watcher
时, flush
为pre
或post
, 则watcher
的回调函数就会异步执行分别是通过
queueJob
和queuePostRenderEffect
来将回调函数推入异步队列中如果是
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的不同点在于
- 侦听的源不同,
watch
侦听的是一个或者多个响应式对象, 或者一个getter函数, 而watchEffect
则是侦听的一个普通函数, 内部访问了响应式对象, 不需要返回响应式对象.
watchEffect
没有回调函数, 当副作用函数中内部的响应式对象发生改变之后, 会再次执行这个副作用函数
watchEffect
会在定义之后立即执行
本质上则是通过定义之后立即执行副作用函数来获取响应式侦听对象, 并侦听对象变更来触发副作用函数
watchPostEffect
Alias of watchEffect() with flush: 'post' option.
watchSyncEffect
Alias of watchEffect() with flush: 'sync' option.