在 Vue 3.x 的 Composition API 中,我们可以用近似 React Hooks 的方式组织代码的复用;ref/reactive/computed 等好用的响应式 API 函数可以摆脱组件的绑定,抽离出来被随处使用了。
传统上在 Vue 2.x Options API 的实践中,不太推荐过多使用组件定义中的 watch 属性 -- 理由是除了某些新旧值比较和页面副作用以外,用 computed 值会更“合理”。
而随着新 API 的使用,由于“生命周期”概念大幅收窄为“副作用”,故新的独立 watch/watchEffect 函数使用频率大大增加了,并且其更灵活的函数形式也让它使用起来愈加方便;不过或许正是这种“不习惯”和过度灵活,让我们在读过一遍官网文档后仍会有所疑问。
本文就将尝试聚焦于 Composition API 中的 watch/watchEffect,希望通过对相应模块的单元测试进行解读和归纳,并结合适度解析一部分源码,大抵上能够达到对其有更直观全面的了解、使用起来心中有数的效果;至于更深层次、更全面的框架层面原理等,请读者老爷们根据需要自行了解罢。
我们将要观察三个代码仓库,分别是
vue
- Vue 2.x 项目@vue/composition-api
- 结合 Vue 2.x “提前尝鲜” Composition API 的过渡性项目vue-next
- Vue 3.x 项目,本文分析的是其 3.0.0-beta.15 版本
I. Vue 2.x 和 @vue/composition-api 🔍
@vue/composition-api
是 Vue 3.x 尚不可用时代的替代产物,选择从该项目入手分析的主要原因有:
- 据本文成文时业已推出一年有余,国内外使用者众
- 其底层仍基于大家熟悉的 Vue 2.x,便于理解
- 相关单元测试比 Vue 3 beta 中的相同模块更直观和详细
此次谈论的主要是使用在 vue 组件 setup() 入口函数中的 watch/watchEffect 方法;涉及文件包括 test/apis/watch.spec.js
、src/apis/watch.ts
等。
1.1 composition-api 中的 watch() 函数签名
"watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。"
这里先适当考察一下源码中暴露的 watch() 函数相关的几种签名形式和参数设置,有利于理解后面的用例调用
- 函数签名1:
(目标数组 sources, 回调 cb, 可选选项 options) => stopFn
function watch< T extends Readonly<WatchSource<unknown>[]>, Immediate extends Readonly<boolean> = false >( sources: T, cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>, options?: WatchOptions<Immediate> ): n
- 函数签名2:
(单一基本类型目标 source, 回调 cb, 可选选项 options) => stopFn
function watch<T, Immediate extends Readonly<boolean> = false>( source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle
- 函数签名3:
(响应式对象单目标 source, 回调 cb, 可选选项 options) => stopFn
function watch< T extends object, Immediate extends Readonly<boolean> = false >( source: T, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle
- 函数签名4:
(回调 effect, 可选选项 options) => stopFn
⚠️注意:这时就需要换成调用 watchEffect() 了
"立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数"
function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle
1.2 测试用例直译
test 1: 'should work'
- 组件加载后,且
options
为{ immediate: true }
的情况下,cb
立即执行一次,观察到从旧值 undefined 变为默认值的过程 - 上述首次执行时,
cb(newV, oldV, onCleanup)
中的第三个参数 onCleanup 并不执行 - 对 vue 实例连续赋值只计最后一次,并在 nextTick 中调用一次 cb 并触发一次其中的 onCleanup
- vue 实例 $destroy() 后,cb 中的 onCleanup 调用一次
test 2: 'basic usage(value wrapper)'
- watch() 第一个参数
source
为单一的基本类型,且options
为{ flush: 'post', immediate: true }
的情况下,cb
立即执行一次,观察到从旧值 undefined 变为默认值的过程 - 对 vue 实例赋后,在 nextTick 中调用一次 cb
test 3: 'basic usage(function)'
source
为() => a.value
且options
为{ immediate: true }
的情况下- 表现同 test 2
test 4: 'multiple cbs (after option merge)'
- 分别在声明一个 Vue 对象和将其实例化时,对某个响应式对象
const a = ref(1)
进行 watch() - 在 nextTick 中,两次 watch 的回调都应该以
cb(2, 1)
的参数被执行
test 5: 'with option: lazy'
- 组件加载后,在 options 为
{ lazy: true }
的情况下,cb
并不会执行 - 对 vue 实例赋后,在 nextTick 中才调用一次 cb
test 6: 'with option: deep'
- 目标为响应式对象
const a = ref({ b: 1 })
,options 为{ lazy: true, deep: true }
- 组件加载后,立即以
vm.a.b = 2
的形式对 a 赋值,此时由于是 lazy 模式所以cb
仍并不会执行 - 在 nextTick 中,首次回调以
cb({b: 2}, {b: 2})
的参数被调用,显然以上赋值方式未达到预期 - 再次以
vm.a = { b: 3 }
的形式对 a 赋值,在下一个 nextTick 中,回调正确地以cb({b: 3}, {b: 2})
的参数被调用
test 7: 'should flush after render (immediate=false)'
- options 为
{ lazy: true }
- 组件加载后,立即对目标赋新值
- 在 nextTick 中,
cb
只运行一次且新旧参数正确,模板中也正确渲染出新值
test 8: 'should flush after render (immediate=true)'
- options 为
{ immediate: true }
- 组件加载后,由于没有指定 lazy,所以
cb
立即观察到从 undefined 到默认值的变化 - 对目标赋新值
- 在 nextTick 中,
cb
再次运行且新旧参数正确,模板中也正确渲染出新值
test 9: 'should flush before render'
- options 为
{ lazy: true, flush: 'pre' }
- 组件加载后,立即对目标赋新值
- 在 nextTick 中,
cb
首次运行且新旧参数正确,但在cb
内部访问到的模板渲染值仍是旧值 -- 说明cb
在模板重新渲染之前被调用了
test 10: 'should flush synchronously'
- options 为
{ lazy: true, flush: 'sync' }
- 组件加载后,
cb
由于指定了 lazy: true 而不会被默认调用 - 此时对目标赋新值 n 次,每次都能同步立即触发
cb
- 在 nextTick 中,重新考察
cb
调用次数,恰为 n
test 12: 'should allow to be triggered in setup'
- options 为
{ flush: 'sync', immediate: true })
,观察响应式对象const count = ref(0)
- 在 setup() 中,声明了 watch 后,同时对目标赋值
count.value++
- 组件加载后,
cb
就被调用了两次 - 第一次为
cb(0, undefined)
- 第二次为
cb(1, 0)
test 13: 'should run in a expected order'
- 结合 flush 选项的三种状态,分别用 watchEffect() 和 watch() 观察:
watchEffect(() => { void x.value; _echo('sync1'); }, { flush: 'sync' }); watchEffect(() => { void x.value; _echo('pre1'); }, { flush: 'pre' }); watchEffect(() => { void x.value; _echo('post1'); }, { flush: 'post' }); watch(x, () => { _echo('sync2') }, { flush: 'sync', immediate: true }) watch(x, () => { _echo('pre2') }, { flush: 'pre', immediate: true }) watch(x, () => { _echo('post2') }, { flush: 'post', immediate: true })
- 用例中的 vue 实例中还包含了一个赋值方法:
const inc = () => { result.push('before_inc') x.value++ result.push('after_inc') }
- 组件加载后,6 个回调都被 立即 执行,且顺序和声明的一致
- 调用 inc() 后,回调都被执行,且按照优先级和声明顺序,依次为
before_inc
->sync1
->sync2
->after_inc
->pre1
->pre2
->post1
->post2
test 14: 'simple effect - should work'
- 组件加载后,watchEffect() 中的
effect
回调被立即执行 - 此时能在
effect()
函数中,能访问到目标值 - 在 nextTick 中,onCleanup 被赋值为一个函数,即源码中的
registerCleanup(fn) => void
- 同时,onCleanup 只是被声明创建出来,其真正生效的 fn 参数尚不会被立即执行(见下文 1.3 清除 - 创建和运行)
- 同时,在
effect
回调中能访问到目标的初始值 - 对目标赋值
- 在 nextTick 中,
effect
回调中能访问到目标的新值 - 此时,由于目标变化,onCleanup 被执行一次
- 销毁 vue 实例后的 nextTick 中,onCleanup 再被执行一次
test 15: 'simple effect - sync=true'
- 使用 watchEffect(), 在 options 为
{ flush: 'sync' }
的情况下 - 组件加载后,
effect
回调被立即执行并访问到目标值 - 此时对目标赋新值,
effect
回调能立即执行并访问到新值
test 16: 'Multiple sources - do not store the intermediate state'
- 观察多个对象,且 options 为
{ immediate: true }
时 - 组件加载后,
cb
被立即调用一次,观察到值从 undefined 到 sources 初始值数组的变化 - 此时,对多个目标连续赋值几次
- 在 nextTick 中,
cb
又被调用一次,观察到最后一次赋值的变化 - 此时,对某一个目标连续赋值几次
- 在 nextTick 中,
cb
又被调用一次,观察到最后一次赋值的变化 - 见下文 1.3 中关于 immediate 的解释
test 17: 'Multiple sources - basic usage(immediate=true, flush=none-sync)'
- 观察多个对象,且 options 为
{ flush: 'post', immediate: true }
时 - 组件加载后,
cb
被立即调用一次,观察到值从 undefined 到 sources 初始值数组的变化 - 此时,对某个目标赋值;立即考察
cb
,并没有新的调用 - 在 nextTick 中,
cb
又被调用一次,并观察到目标值的变化 - 此时,对多个目标赋值
- 在 nextTick 中,
cb
又被调用一次,并观察到目标值的变化
test 18: 'Multiple sources - basic usage(immediate=false, flush=none-sync)'
- 观察多个对象,且 options 为
{ flush: 'post', immediate: false }
时 - 组件加载后,立即对某个目标赋值;考察
cb
并未被立即调用 - 在 nextTick 中,
cb
被调用一次,并观察到目标值最新的变化 - 此时,对多个目标赋值
- 在 nextTick 中,
cb
又被调用一次,并观察到目标值的变化
test 19: 'Multiple sources - basic usage(immediate=true, flush=sync)'
- 观察多个对象,且 options 为
{ flush: 'sync', immediate: true }
时 - 组件加载后,
cb
被立即调用一次,观察到值从 undefined 到 sources 初始值数组的变化 - 此时,对某个目标赋值;立即考察
cb
,应又被调用一次,并观察到目标值新的变化 - 此时,连续 n 次分别对多个目标赋值;立即考察
cb
,应被调用了 n 次,且每次都能正确观察到值的变化
test 20: 'Multiple sources - basic usage(immediate=false, flush=sync)'
- 观察多个对象,且 options 为
{ lazy: true, flush: 'sync' }
时 - 组件加载后,
cb
并未被立即调用 - 此时,对某个目标赋值;立即考察
cb
,应又被调用一次,并观察到目标值新的变化 - 此时,连续 n 次分别对多个目标赋值;立即考察
cb
,应被调用了 n 次,且每次都能正确观察到值的变化
test 21: 'Out of setup - should work'
- 不在 Vue 实例中,而是在一个普通函数里
- 用 watch() 观察一个响应式对象,且 options 为
{ immediate: true }
时 - 在 watch() 调用后,
cb
被立即调用一次,观察到目标值从 undefined 到初始值的变化 - 此时,对目标赋值
- 在 nextTick 中,
cb
又被调用一次,并观察到目标值新的变化
test 22: 'Out of setup - simple effect'
- 不在 Vue 实例中,而是在一个普通函数里
- 用 watchEffect() 观察一个响应式对象,没有指定 options
- 在 watchEffect() 调用后,
effect
被立即调用一次 - 在 nextTick 中,
effect
没有新的调用,且此时effect
中访问到的是目标初始值 - 此时,对目标赋值
- 在 nextTick 中,
effect
有一次新的调用,且此时effect
中访问到的是目标新值
test 23: 'cleanup - work with effect'
- 不在 Vue 实例中,而是在一个普通函数里
- 用 watchEffect() 观察一个响应式对象,没有指定 options
effect
的形式为 (onCleanup: fn => void) => void- 在 watchEffect() 调用后的 nextTick 中,对目标赋新值
- 此次赋值后,fn 中的清理行为应早于响应目标值变化的行为发生
- 见下文 1.3 中 “watch() 中的清除回调” 部分里的 watcher.before
test 24: 'run cleanup when watch stops (effect)'
- 不在 Vue 实例中,而是在一个普通函数里
- 在 watchEffect() 调用后的 nextTick 中,
effect
应被调用 - 此时,手动触发 watchEffect() 返回的 stop 方法
- onCleanup 应异步地被执行
- 见下文 1.3 中 “watch() 中的清除回调” 部分里的 “watcher 卸载”
test 25: 'run cleanup when watch stops'
- 不在 Vue 实例中,而是在一个普通函数里
- 用
watch(source, cb: (newV, oldV, onCleanup) => void, { immediate: true }) => stop
观察一个响应式对象 - 在 watch() 调用后,
cb
立即被调用 - 此时调用 stop,则 onCleanup 立即被调用
test 26: 'should not collect reactive in onCleanup'
- 不在 Vue 实例中,而是在一个普通函数里
- 用
watchEffect(effect: (onCleanup) => void) => stop
观察响应式对象 ref1 - 只在 onCleanup(fn => void) 的 fn 中,改变了另一个 ref2 的值
- 在 nextTick 中,effect 被调用一次,并观察到 ref1 的初始值
- 此时,对 ref1 赋新值
- 在 nextTick 中,effect 又被调用一次,并观察到 ref1 的新值
- 此时,对 ref2 赋新值
- 在 nextTick 中,effect 并无新的调用
test 27: 'cleanup - work with callback'
- 不在 Vue 实例中,而是在一个普通函数里
- 用 watch() 观察一个响应式对象,且 options 为
{ immediate: true }
cb
的形式为 (newV, oldV, onCleanup: fn => void) => void- 在 watch() 调用后,立即对目标赋新值
- 在 nextTick 中,fn 中的清理行为应早于响应目标值变化的行为发生
1.3 相关特性解析
watcher
无论是 watch() 还是 watchEffect() 最终都是利用 vue 2 中的 Watcher 类构建的。
lazy
- 在早期版本中,options 中默认是传递 lazy 的,现在改成了其反义词 immediate
- 途径1(watchEffect):在 createWatcher() 源码中,直接被赋值
watcher.lazy = false
- 途径2(watch):经由用户定义的 options 最终被传递到 Watcher 类
在 Watcher 类构造函数中,lazy 属性会赋给实例本身,也会影响到 dirty 属性:
if (options) { this.lazy = !!options.lazy } this.dirty = this.lazy // for lazy watchers this.value = this.lazy ? undefined // 懒加载,实例化后不立即取值 : this.get()
以及 Watcher 类相关的一些方法中:
update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false }
而后,会异步地通过 Vue.prototype._init
--> initState
--> initComputed
--> defineComputed
--> createComputedGetter()
--> watcher.evaluate()
--> watcher.get()
取值:
// src/core/instance/state.js if (watcher.dirty) { watcher.evaluate() }
watcher.get()
这里把 get() 稍微单说一下,同样是 Watcher 类中:
// src/core/observer/watcher.js import { traverse } from './traverse' import Dep, { pushTarget, popTarget } from './dep' /** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { if (this.deep) { // 深层遍历 traverse(value) } popTarget() this.cleanupDeps() } return value }
watcher 依靠 deps、newDeps 等数组维护依赖关系,如添加依赖就是通过 dep.depend()
--> watcher.addDep()
。
这里涉及到的几个主要函数(pushTarget、popTarget、cleanupDeps)都和 Dep 依赖管理相关,由其管理监听顺序、通知 watcher 实例 update() 等。
options.flush
- 默认为 'post'
- 如果为 'sync',则立即执行 cb
- 如果为 'pre' 或 'post',则用 queueFlushJob 插入队列前或后在 nextTick 异步执行
// src/apis/watch.ts const createScheduler = <T extends Function>(fn: T): T => { if ( isSync || fallbackVM ) { return fn } return (((args: any[]) => queueFlushJob( vm, () => { fn(args) }, flushMode as 'pre' | 'post' )) as any) as T } function installWatchEnv(vm: any) { vm[WatcherPreFlushQueueKey] = [] vm[WatcherPostFlushQueueKey] = [] vm.$on('hook:beforeUpdate', flushPreQueue) vm.$on('hook:updated', flushPostQueue) }
options.immediate
- 最终会传递给 vue2 中的
Vue.prototype.$watch
中,逻辑很简单,只要是 true 就在实例化 Watcher 后立即执行一遍就完事了;其相关部分的源码如下:
const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } }
- 而 Watcher 的实现中并没有 immediate 的相关逻辑,也就是说,后续的响应式回调还是异步执行
清除
"watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致" -- Composition API 文档
// src/apis/watch.ts // 即 watch() 的参数二 `cb` 的参数三(前俩是 newValue、oldValue) // 或 watchEffect() 的参数一 `effect` 的唯一参数 const registerCleanup: InvalidateCbRegistrator = (fn: () => void) => { cleanup = () => { try { fn() } catch (error) { logError(error, vm, 'onCleanup()') } } } // 下文中运行时间点中真正被执行的 const runCleanup = () => { if (cleanup) { cleanup() cleanup = null } }
watch() 中的清除回调
在 watch 的情况下,cb
回调中的 cleanup 会在两个时间点被调用:
一个是每次 cb
运行之前:
二是 watcher 卸载时:
// src/apis/watch.ts function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) { const _teardown = watcher.teardown watcher.teardown = function (args) { _teardown.apply(watcher, args) runCleanup() } }
watchEffect() 中的失效回调
在 watchEffect 的情况下,cb
回调中的 cleanup (这种情况下也称为 onInvalidate,失效回调)同样会在两个时间点被调用:
// src/apis/watch.ts const watcher = createVueWatcher(vm, getter, noopFn, { deep: options.deep || false, sync: isSync, before: runCleanup, }) patchWatcherTeardown(watcher, runCleanup)
首先是遍历执行每个 watcher 时, cleanup 被注册为 watcher.before,文档中称为“副作用即将重新执行时”:
// vue2 中的 flushSchedulerQueue() for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } }
其次也是 watcher 卸载时,文档中的描述为:“侦听器被停止 (如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)”。
watchEffect() 中的 options
- watchEffect 相当于没有第一个观察对象 source/sources 的 watch 函数
- 原来的
cb
函数在这里称为effect
,成为了首个参数,而该回调现在只包含 onCleanup 一个参数 - 相应的第二个参数仍是
options
默认的 options:
function getWatchEffectOption(options?: Partial<WatchOptions>): WatchOptions { return { { immediate: true, deep: false, flush: 'post', }, options, } }
调用 watchEffect() 时和传入的 options 结合:
export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { const opts = getWatchEffectOption(options) const vm = getWatcherVM() return createWatcher(vm, effect, null, opts) }
实际能有效传入的只有 deep 和 flush:
// createWatcher const flushMode = options.flush const isSync = flushMode === 'sync' // effect watch if (cb === null) { const getter = () => (source as WatchEffect)(registerCleanup) const watcher = createVueWatcher(vm, getter, noopFn, { deep: options.deep || false, sync: isSync, before: runCleanup, }) }
function createVueWatcher( vm, getter, callback, options ): VueWatcher { const index = vm._watchers.length vm.$watch(getter, callback, { immediate: options.immediateInvokeCallback, deep: options.deep, lazy: options.noRun, sync: options.sync, before: options.before, }) return vm._watchers[index] }
关于这点也可以参考下文的 2.1 - test 23、test 24
II. Vue 3.x beta 🔍
Vue 3.x beta 中 watch/watchEffect 的签名和之前 @vue/composition-api
中一致,在此不再赘述。
对比、结合前文,该部分将主要关注其单元测试的视角差异,并列出其实现方面的一些区别,希望能加深对本文主题的理解。
主要涉及文件为 packages/runtime-core/src/apiWatch.ts
和 packages/runtime-core/__tests__/apiWatch.spec.ts
等。
2.1 部分测试用例
因为函数的用法相比 @vue/composition-api 中并无改变,Vue 3 中相关的单元测试覆盖的功能部分和前文的版本差不多,写法上似乎更偏重于对 ref/reactive/computed 几种响应式类型的考察。
test 4: 'watching single source: computed ref'
- 用 watch() 观察一个 computed 对象
- 在 watch() 调用后,立即对原始 ref 目标赋新值
- 在 nextTick 中,观察到 computed 对象的新旧值变化符合预期
test 6: 'directly watching reactive object (with automatic deep: true)'
- 用 watch() 观察一个
const src = reactive({ count: 0 })
对象 - 在 watch() 调用后,立即赋值
src.count++
- 在 nextTick 中,能观察到 count 的新值
test 14: 'cleanup registration (effect)'
- 用
watchEffect(effect: onCleanup: fn => void) => stop
观察一个响应式对象 - 在 watchEffect() 调用后,其中立即能观察到目标初始值(默认 immediate: true)
- 此时,对目标赋新值
- 在 nextTick 中,观察到新值,且 fn 被调用一次(见 1.3 清理 - watcher.before)
- 此时,手动调用 stop()
- fn 立即又被执行一次
test 15: 'cleanup registration (with source)'
- 用
watch(source, cb: onCleanup: fn => void) => stop
观察一个响应式对象 - 在 watch() 调用后,立即对目标赋新值
- 在 nextTick 中,观察到新值,且此时 fn 未被调用 (见 1.2 - test 14 / 1.3 清理 - watch() 中的清除回调)
- 此时,再次对目标赋新值
- 在 nextTick 中,观察到新值,且此时 fn 被调用了一次
- 此时,手动调用 stop()
- fn 立即又被执行一次
test 19: 'deep'
- 在 options 为
{ deep: true }
的情况下 - 即便是如下这样各种类型互相嵌套,也能正确观察
const state = reactive({ nested: { count: ref(0) }, array: [1, 2, 3], map: new Map([['a', 1], ['b', 2]]), set: new Set([1, 2, 3]) })
test 23: 'warn immediate option when using effect'
- 使用 watchEffect() 的情况下,指定 options 为
{ immediate: false }
- 在 vue 3 中,会忽略 immediate 选项,并 warning 提示
watch() "immediate" option is only respected when using the watch(source, callback, options?) signature.
test 24: 'warn and not respect deep option when using effect'
- 使用 watchEffect() 的情况下,指定 options 为
{ deep: true }
- 在 vue 3 中,会忽略 deep 选项,并 warning 提示
watch() "deep" option is only respected when using the watch(source, callback, options?) signature.
test 25: 'onTrack'
- 观察目标为
const obj = reactive({ foo: 1, bar: 2 })
- 使用 watchEffect(),观察行为依次是
obj.foo
、'bar' in obj
、Object.keys(obj)
- options.onTrack 被调用 3 次,每次的参数依次为:
// 1st { target: obj, type: TrackOpTypes.GET, key: 'foo' } // 2nd { target: obj, type: TrackOpTypes.HAS, key: 'bar' } // 3rd { target: obj, type: TrackOpTypes.ITERATE, key: ITERATE_KEY }
test 26: 'onTrigger'
- 使用 watchEffect(),观察目标为
const obj = reactive({ foo: 1 })
obj.foo++
后,options.onTrigger 参数为:
{ type: TriggerOpTypes.SET, key: 'foo', oldValue: 1, newValue: 2 }
delete obj.foo
后,options.onTrigger 参数为:
{ type: TriggerOpTypes.DELETE, key: 'foo', oldValue: 2 }
2.2 调用关系
// vue-next/packages/runtime-core/src/apiWatch.ts function doWatch( source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { }
2.3 新特性归纳
onTrack() 和 onTrigger()
- @vue/composition-api 中未实现的两个调试方法,通过 options 传递
- 实际是传递给 effect() 生效的,对应于其中两个依赖收集的基础特性 track() 和 trigger():
// packages/reactivity/src/effect.ts export interface ReactiveEffectOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void // 也就是 watcher 中的 onCleanup } export type DebuggerEvent = { effect: ReactiveEffect target: object type: TrackOpTypes | TriggerOpTypes key: any } & DebuggerEventExtraInfo export function track( target: object, type: TrackOpTypes, key: unknown ) { } export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { }
// packages/reactivity/src/operations.ts export const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate' } export const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', CLEAR = 'clear' }
watchEffect() 和 effect()
在前文中我们看到了 watch/watchEffect 对 effect() 的间接调用。实际上除了名称相近,其调用方式也差不多:
// packages/reactivity/src/effect.ts export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> export function stop(effect: ReactiveEffect)
二者区别主要在于:
- effect 和 ref/reactive/computed 等定义同样位于 packages/reactivity 中,属于相对基础的定义
- watchEffect() 会随 vue 实例的卸载而自动触发失效回调;而 effect() 则需要在 onUnmounted 等处手动调用 stop
选项式 watch 的执行时机
对于 Vue 传统的 Options API 组件写法:
const App = { data() { return { message: "Hello" }; }, watch: { message: { handler() { console.log("Immediately Triggered") } } } }
- 在 Vue 2.x 中,watch 中的属性默认确实是立即执行的
- 而在 Vue 3 beta 中,则需要手动指定 immediate: true (和 handler 并列),否则不会立即执行
- 按照 github 知名用户
yyx990803
的说法,这种改动其实是和组合式 API 中的行为一致的 -- watch() 默认不会立即执行,而 watchEffect() 相反 - 社区也在讨论未来是否增加 runAndWatch() 等 API 来明确化开发者的使用预期
source 不再支持字符串
同样有别于 Vue 2.x 的一点是,在传统写法中:
const App = { data() { foo: { bar: { qux: 'val' } } }, watch: { 'foo.bar.qux' () { } } }
或者在 Vue 2.x + @vue/composition-api 中,也可以写成:
const App = { props: ['aaa'], setup(props) { watch('aaa', () => { }); return { }; } }
Vue 2.x 中对以 .
分割的 magic strings 实际上做了特别解析:
// vue/src/core/util/lang.js /** * Parse simple path. */ export function parsePath (path: string): any { const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
而如果回过头看 1.1 中的 watch 函数签名,并结合以下定义:
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
也就不难理解,新式的 source 目前只支持三种类型:
- 原生变量
- ref/reactive/computed 等响应式对象
- 一个返回某个值的函数对象
所以,
- 在 Vue 3 beta 中,这种被
yyx990803
称为 “magic strings” 的字符串 source 也不再支持 - 可以用
() => this.foo.bar.baz
或() => props.aaa
代替
参考资料 📕
- github.com/vuejs/rfcs/…
- zhuanlan.zhihu.com/p/146097763
- github.com/vuejs/vue-n…
- github.com/vuejs/rfcs/…
- juejin.cn/post/684490…