51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3913|回复: 3
打印 上一主题 下一主题

[原创] 用单元测试读懂 vue3 watch 函数

[复制链接]
  • TA的每日心情
    擦汗
    昨天 09:04
  • 签到天数: 1047 天

    连续签到: 5 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2020-7-16 13:51:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    传统上在 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
    1. function watch<

    2.   T extends Readonly<WatchSource<unknown>[]>,

    3.   Immediate extends Readonly<boolean> = false

    4.   >(

    5.   sources: T,

    6.   cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,

    7.   options?: WatchOptions<Immediate>

    8.   ): WatchStopHandle
    复制代码
     函数签名2:(单一基本类型目标 source, 回调 cb, 可选选项 options) => stopFn
    1. function watch<T, Immediate extends Readonly<boolean> = false>(

    2.   source: WatchSource<T>,

    3.   cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,

    4.   options?: WatchOptions<Immediate>

    5.   ): WatchStopHandle
    复制代码
     函数签名3:(响应式对象单目标 source, 回调 cb, 可选选项 options) => stopFn
    1. function watch<

    2.   T extends object,

    3.   Immediate extends Readonly<boolean> = false

    4.   >(

    5.   source: T,

    6.   cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,

    7.   options?: WatchOptions<Immediate>

    8.   ): WatchStopHandle
    复制代码
    函数签名4:(回调 effect, 可选选项 options) => stopFn
      注意:这时就需要换成调用 watchEffect() 了
      "立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数"
    1. function watchEffect(

    2.   effect: WatchEffect,

    3.   options?: WatchOptionsBase

    4.   ): WatchStopHandle
    复制代码



    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    昨天 09:04
  • 签到天数: 1047 天

    连续签到: 5 天

    [LV.10]测试总司令

    2#
     楼主| 发表于 2020-7-16 14:01:23 | 只看该作者
    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() 观察:
    1. watchEffect(() => { void x.value; _echo('sync1'); }, { flush: 'sync' });

    2.   watchEffect(() => { void x.value; _echo('pre1'); }, { flush: 'pre' });

    3.   watchEffect(() => { void x.value; _echo('post1'); }, { flush: 'post' });

    4.   watch(x, () => { _echo('sync2') }, { flush: 'sync', immediate: true  })

    5.   watch(x, () => { _echo('pre2') }, { flush: 'pre', immediate: true  })

    6.   watch(x, () => { _echo('post2') }, { flush: 'post', immediate: true  })
    复制代码
    用例中的 vue 实例中还包含了一个赋值方法:
    1. const inc = () => {

    2.   result.push('before_inc')

    3.   x.value++

    4.   result.push('after_inc')

    5.   }
    复制代码
    组件加载后,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 属性:

    1. if (options) {

    2.   this.lazy = !!options.lazy

    3.   ...

    4.   }

    5.   ...

    6.   this.dirty = this.lazy // for lazy watchers

    7.   ...

    8.   this.value = this.lazy

    9.   ? undefined // 懒加载,实例化后不立即取值

    10.   : this.get()
    复制代码
    以及 Watcher 类相关的一些方法中:
    1. update () {

    2.   if (this.lazy) {

    3.   this.dirty = true

    4.   } else if (this.sync) {

    5.   this.run()

    6.   } else {

    7.   queueWatcher(this)

    8.   }

    9.   }

    10.   ...

    11.   /**

    12.   * Evaluate the value of the watcher.

    13.   * This only gets called for lazy watchers.

    14.   */

    15.   evaluate () {

    16.   this.value = this.get()

    17.   this.dirty = false

    18.   }
    复制代码
    而后,会异步地通过 Vue.prototype._init --> initState --> initComputed --> defineComputed --> createComputedGetter() --> watcher.evaluate() --> watcher.get() 取值:
    1. // src/core/instance/state.js

    2.   if (watcher.dirty) {

    3.   watcher.evaluate()

    4.   }
    复制代码
    watcher.get()
      这里把 get() 稍微单说一下,同样是 Watcher 类中:

    1. // src/core/observer/watcher.js

    2.   import { traverse } from './traverse'

    3.   import Dep, { pushTarget, popTarget } from './dep'

    4.   ...

    5.   /**

    6.   * Evaluate the getter, and re-collect dependencies.

    7.   */

    8.   get () {

    9.   pushTarget(this)

    10.   let value

    11.   const vm = this.vm

    12.   try {

    13.   value = this.getter.call(vm, vm)

    14.   } catch (e) {

    15.   ...

    16.   } finally {

    17.   if (this.deep) {

    18.   // 深层遍历

    19.   traverse(value)

    20.   }

    21.   popTarget()

    22.   this.cleanupDeps()

    23.   }

    24.   return value

    25.   }
    复制代码
    watcher 依靠 deps、newDeps 等数组维护依赖关系,如添加依赖就是通过 dep.depend() --> watcher.addDep()。
      这里涉及到的几个主要函数(pushTarget、popTarget、cleanupDeps)都和 Dep 依赖管理相关,由其管理监听顺序、通知 watcher 实例 update() 等。
      options.flush
      默认为 'post'
      如果为 'sync',则立即执行 cb
      如果为 'pre' 或 'post',则用 queueFlushJob 插入队列前或后在 nextTick 异步执行

    1. // src/apis/watch.ts
    2.   const createScheduler = <T extends Function>(fn: T): T => {

    3.   if ( isSync || fallbackVM ) {

    4.   return fn

    5.   }

    6.   return (((...args: any[]) =>

    7.   queueFlushJob(

    8.   vm,

    9.   () => {

    10.   fn(...args)

    11.   },

    12.   flushMode as 'pre' | 'post'

    13.   )) as any) as T

    14.   }

    15.   ...

    16.   function installWatchEnv(vm: any) {

    17.   vm[WatcherPreFlushQueueKey] = []

    18.   vm[WatcherPostFlushQueueKey] = []

    19.   vm.$on('hook:beforeUpdate', flushPreQueue)

    20.   vm.$on('hook:updated', flushPostQueue)

    21.   }
    复制代码



    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    昨天 09:04
  • 签到天数: 1047 天

    连续签到: 5 天

    [LV.10]测试总司令

    3#
     楼主| 发表于 2020-7-16 14:05:24 | 只看该作者
     options.immediate
      最终会传递给 vue2 中的 Vue.prototype.$watch 中,逻辑很简单,只要是 true 就在实例化 Watcher 后立即执行一遍就完事了;其相关部分的源码如下:
    1. const watcher = new Watcher(vm, expOrFn, cb, options)

    2.   if (options.immediate) {

    3.   try {

    4.   cb.call(vm, watcher.value)

    5.   } catch (error) {

    6.   handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)

    7.   }

    8.   }
    复制代码
    而 Watcher 的实现中并没有 immediate 的相关逻辑,也就是说,后续的响应式回调还是异步执行
      清除
      "watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致" -- Composition API 文档
      创建和运行

    1. // src/apis/watch.ts

    2.   // 即 watch() 的参数二 `cb` 的参数三(前俩是 newValue、oldValue)

    3.   // 或 watchEffect() 的参数一 `effect` 的唯一参数

    4.   const registerCleanup: InvalidateCbRegistrator = (fn: () => void) => {

    5.   cleanup = () => {

    6.   try {

    7.   fn()

    8.   } catch (error) {

    9.   logError(error, vm, 'onCleanup()')

    10.   }

    11.   }

    12.   }

    13.   // 下文中运行时间点中真正被执行的

    14.   const runCleanup = () => {

    15.   if (cleanup) {

    16.   cleanup()

    17.   cleanup = null

    18.   }

    19.   }
    复制代码
    watch() 中的清除回调
      在 watch 的情况下,cb 回调中的 cleanup 会在两个时间点被调用:
      一个是每次 cb 运行之前:

    1. const applyCb = (n: any, o: any) => {

    2.   // cleanup before running cb again

    3.   runCleanup()

    4.   cb(n, o, registerCleanup)

    5.   }

    6.   // sync 立即执行 cb,或推入异步队列

    7.   let callback = createScheduler(applyCb)
    复制代码
    二是 watcher 卸载时:
    1. // src/apis/watch.ts

    2.   function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) {

    3.   const _teardown = watcher.teardown

    4.   watcher.teardown = function (...args) {

    5.   _teardown.apply(watcher, args)

    6.   runCleanup()

    7.   }

    8.   }
    复制代码
    watchEffect() 中的失效回调
      在 watchEffect 的情况下,cb 回调中的 cleanup (这种情况下也称为 onInvalidate,失效回调)同样会在两个时间点被调用:

    1. // src/apis/watch.ts

    2.   const watcher = createVueWatcher(vm, getter, noopFn, {

    3.   deep: options.deep || false,

    4.   sync: isSync,

    5.   before: runCleanup,

    6.   })

    7.   patchWatcherTeardown(watcher, runCleanup)
    复制代码
    首先是遍历执行每个 watcher 时, cleanup 被注册为 watcher.before,文档中称为“副作用即将重新执行时”:
    1. // vue2 中的 flushSchedulerQueue()

    2.   for (index = 0; index < queue.length; index++) {

    3.   watcher = queue[index]

    4.   if (watcher.before) {

    5.   watcher.before()

    6.   }

    7.   ...

    8.   }
    复制代码
     其次也是 watcher 卸载时,文档中的描述为:“侦听器被停止 (如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)”。
      watchEffect() 中的 options
      watchEffect 相当于没有第一个观察对象 source/sources 的 watch 函数
      原来的 cb 函数在这里称为 effect,成为了首个参数,而该回调现在只包含 onCleanup 一个参数
      相应的第二个参数仍是 options
      默认的 options:

    1. function getWatchEffectOption(options?: Partial<WatchOptions>): WatchOptions {

    2.   return {

    3.   ...{

    4.   immediate: true,

    5.   deep: false,

    6.   flush: 'post',

    7.   },

    8.   ...options,

    9.   }

    10.   }
    复制代码
    调用 watchEffect() 时和传入的 options 结合:
    1. export function watchEffect(

    2.   effect: WatchEffect,

    3.   options?: WatchOptionsBase

    4.   ): WatchStopHandle {

    5.   const opts = getWatchEffectOption(options)

    6.   const vm = getWatcherVM()

    7.   return createWatcher(vm, effect, null, opts)

    8.   }
    复制代码
    实际能有效传入的只有 deep 和 flush:
    1. // createWatcher

    2.   const flushMode = options.flush

    3.   const isSync = flushMode === 'sync'

    4.   ...

    5.   // effect watch

    6.   if (cb === null) {

    7.   const getter = () => (source as WatchEffect)(registerCleanup)

    8.   const watcher = createVueWatcher(vm, getter, noopFn, {

    9.   deep: options.deep || false,

    10.   sync: isSync,

    11.   before: runCleanup,

    12.   })

    13.   ...

    14.   }
    复制代码
    1. function createVueWatcher( vm, getter, callback, options ): VueWatcher {

    2.   const index = vm._watchers.length

    3.   vm.$watch(getter, callback, {

    4.   immediate: options.immediateInvokeCallback,

    5.   deep: options.deep,

    6.   lazy: options.noRun,

    7.   sync: options.sync,

    8.   before: options.before,

    9.   })

    10.   return vm._watchers[index]

    11.   }
    复制代码
    关于这点也可以参考下文的 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 } 的情况下
      即便是如下这样各种类型互相嵌套,也能正确观察

    1. const state = reactive({

    2.   nested: {

    3.   count: ref(0)

    4.   },

    5.   array: [1, 2, 3],

    6.   map: new Map([['a', 1], ['b', 2]]),

    7.   set: new Set([1, 2, 3])

    8.   })
    复制代码
    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 次,每次的参数依次为:

    1. // 1st

    2.   {

    3.   target: obj,

    4.   type: TrackOpTypes.GET,

    5.   key: 'foo'

    6.   }

    7.   // 2nd

    8.   {

    9.   target: obj,

    10.   type: TrackOpTypes.HAS,

    11.   key: 'bar'

    12.   }

    13.   // 3rd

    14.   {

    15.   target: obj,

    16.   type: TrackOpTypes.ITERATE,

    17.   key: ITERATE_KEY

    18.   }
    复制代码



    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    昨天 09:04
  • 签到天数: 1047 天

    连续签到: 5 天

    [LV.10]测试总司令

    4#
     楼主| 发表于 2020-7-16 14:09:24 | 只看该作者
    test 26: 'onTrigger'
      使用 watchEffect(),观察目标为 const obj = reactive({ foo: 1 })
      obj.foo++ 后,options.onTrigger 参数为:
    1. {

    2.   type: TriggerOpTypes.SET,

    3.   key: 'foo',

    4.   oldValue: 1,

    5.   newValue: 2

    6.   }
    复制代码
    delete obj.foo 后,options.onTrigger 参数为:
    1. {

    2.   type: TriggerOpTypes.DELETE,

    3.   key: 'foo',

    4.   oldValue: 2

    5.   }
    复制代码
    2.2 调用关系

    1. // vue-next/packages/runtime-core/src/apiWatch.ts

    2.   function doWatch(

    3.   source: WatchSource | WatchSource[] | WatchEffect,

    4.   cb: WatchCallback | null,

    5.   { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ

    6.   ): WatchStopHandle {

    7.   ...

    8.   }
    复制代码
    2.3 新特性归纳
      onTrack() 和 onTrigger()
      @vue/composition-api 中未实现的两个调试方法,通过 options 传递
      实际是传递给 effect() 生效的,对应于其中两个依赖收集的基础特性 track() 和 trigger():

    1. // packages/reactivity/src/effect.ts

    2.   export interface ReactiveEffectOptions {

    3.   ...

    4.   onTrack?: (event: DebuggerEvent) => void

    5.   onTrigger?: (event: DebuggerEvent) => void

    6.   onStop?: () => void // 也就是 watcher 中的 onCleanup

    7.   }

    8.   export type DebuggerEvent = {

    9.   effect: ReactiveEffect

    10.   target: object

    11.   type: TrackOpTypes | TriggerOpTypes

    12.   key: any

    13.   } & DebuggerEventExtraInfo

    14.   export function track(

    15.   target: object,

    16.   type: TrackOpTypes,

    17.   key: unknown

    18.   ) {

    19.   ...

    20.   }

    21.   export function trigger(

    22.   target: object,

    23.   type: TriggerOpTypes,

    24.   key?: unknown,

    25.   newValue?: unknown,

    26.   oldValue?: unknown,

    27.   oldTarget?: Map<unknown, unknown> | Set<unknown>

    28.   ) {

    29.   ...

    30.   }
    复制代码
    1. // packages/reactivity/src/operations.ts

    2.   export const enum TrackOpTypes {

    3.   GET = 'get',

    4.   HAS = 'has',

    5.   ITERATE = 'iterate'

    6.   }

    7.   export const enum TriggerOpTypes {

    8.   SET = 'set',

    9.   ADD = 'add',

    10.   DELETE = 'delete',

    11.   CLEAR = 'clear'

    12.   }
    复制代码
    watchEffect() 和 effect()
      在前文中我们看到了 watch/watchEffect 对 effect() 的间接调用。实际上除了名称相近,其调用方式也差不多:

    1. // packages/reactivity/src/effect.ts

    2.   export function effect<T = any>(

    3.   fn: () => T,

    4.   options: ReactiveEffectOptions = EMPTY_OBJ

    5.   ): ReactiveEffect<T>

    6.   export function stop(effect: ReactiveEffect)
    复制代码
     二者区别主要在于:
      effect 和 ref/reactive/computed 等定义同样位于 packages/reactivity 中,属于相对基础的定义
      watchEffect() 会随 vue 实例的卸载而自动触发失效回调;而 effect() 则需要在 onUnmounted 等处手动调用 stop
      选项式 watch 的执行时机
      对于 Vue 传统的 Options API 组件写法:

    1. const App = {

    2.   data() {

    3.   return {

    4.   message: "Hello"

    5.   };

    6.   },

    7.   watch: {

    8.   message: {

    9.   handler() {

    10.   console.log("Immediately Triggered")

    11.   }

    12.   }

    13.   }

    14.   }
    复制代码
    在 Vue 2.x 中,watch 中的属性默认确实是立即执行的
      而在 Vue 3 beta 中,则需要手动指定 immediate: true (和 handler 并列),否则不会立即执行
      按照 github 知名用户 yyx990803 的说法,这种改动其实是和组合式 API 中的行为一致的 -- watch() 默认不会立即执行,而 watchEffect() 相反
      社区也在讨论未来是否增加 runAndWatch() 等 API 来明确化开发者的使用预期
      source 不再支持字符串
      同样有别于 Vue 2.x 的一点是,在传统写法中:

    1. const App = {

    2.   data() {

    3.   foo: {

    4.   bar: {

    5.   qux: 'val'

    6.   }

    7.   }

    8.   },

    9.   watch: {

    10.   'foo.bar.qux' () { ... }

    11.   }

    12.   }
    复制代码
    或者在 Vue 2.x + @vue/composition-api 中,也可以写成:
    1. const App = {

    2.   props: ['aaa'],

    3.   setup(props) {

    4.   watch('aaa', () => { ... });

    5.   return { ... };

    6.   }

    7.   }
    复制代码
    Vue 2.x 中对以 . 分割的 magic strings 实际上做了特别解析:
    1.   // vue/src/core/util/lang.js

    2.   /**

    3.   * Parse simple path.

    4.   */

    5.   export function parsePath (path: string): any {

    6.   ...

    7.   const segments = path.split('.')

    8.   return function (obj) {

    9.   for (let i = 0; i < segments.length; i++) {

    10.   if (!obj) return

    11.   obj = obj[segments[i]]

    12.   }

    13.   return obj

    14.   }

    15.   }
    复制代码
     而如果回过头看 1.1 中的 watch 函数签名,并结合以下定义:
    1. 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 代替


    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-16 10:22 , Processed in 0.070054 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表