2-3-4 Vue3 watch effect

Watch:监听Reactive值的变化

我们经常需要在Reactive值发生变化时附加逻辑。

举例:

- Input内容变化后通知搜索服务进行输入提示

- 用户选定的曲目发生变化时通知播放器切换曲目

- 购物车数据变化后通知服务端记录


在`Reactive` 模型中,每个Reactive值发生变化,都可能会触发另一组行为。再比如:

- 用户的切换导致显示头像的切换

- 购买数量的变化触发重新计算营销卡券使用

因此,Reactive值,需要一个监听机制, 这就是`watch` 和`watchEffect` 。

Side Effect(副作用)、Side Effect Invalidate(副作用失效)

在理解`watch` 和`watchEffect` 前,需要理解两个基础概念:

- Side Effect

- Side Effect Invalidation

副作用(Side Effect)

简单理解:副作用是计算之外的逻辑。

Vue、React这类渲染引擎,根据属性、状态计算视图。

view = f(props, state)

计算视图之外的,就是副作用(Effect)。

例如:

function SomeComponent(a, b) {
    window.location.href = '...' // 副作用
    const c = ref(0) // 副作用
    watchEffect(..) // 副作用
    return <div>{a + b + c.value}</div>
}

- 当reactive值更新的时候,触发vue组件更新可以看作一个副作用。

- 当用户打开页面,发送请求REST API,可以看作副作用

**React/Vue内部是很纯的计算逻辑,所有【人机交互】,都是副作用。**

**划重点:简单的,你可以将副作用理解成纯计算背后产生的效果。**

举例:

- 当你点击菜单的时候,切换了`currentIndex` (一个整数), 右侧显示内容变化是因为副作用。

- 当你删除文件的时候,你仅仅按下了del键,文件真实在磁盘上被删除是你操作的del键的副作用。

`watch` 和`watchEffect` 监听Reactive值变化(ref, reactive),本质是监听它们产生的副作用。从设计上,`ref` 和`reactive` 是值,但是它们会触发`track` 和`trigger` 两个过程,`trigger` 还会触发重绘——这些都是副作用。

副作用失效(Side Effect Invalidation)问题

设想你点击一个导航菜单,切换页面, 有可能会出现这种情况:

- 页面A还没打开,你已经点击了打开页面B的按钮

【点击打开A】和【点击打开B】是两次计算,比如我们这样:

const currentIndex = ref("")

// 点击打开A
currentIndex.value = "A"

// 点击打开B
currentIndex.value = "B"

但是这两次计算产生的副作用——打开页面A和B,可能只有B完成——因为用户点太快了,页面A还没加载完,就切换了。

像这样,一个副作用没有执行完,下一个副作用已经到来了,上一次的副作用我们称为一个失效的副作用(Invalidate)。

这样看, `watch` 和`watchEffect` 要处理所有的副作用,还需要处理副作用失效的问题。

watchEffect

在实现UI的过程中,我们经常需要添加一些副作用,这个时候,我们需要了解UI内部状态的变化,比如:

- 何时一个reactive的值发生变化?

- 何时页面发生渲染(前、后)?

我们需要在发生上述行为的时候,添加副作用。

这时,可以用`watchEffect` 。当:

- `reactive` 值发生变化

- 界面的初次渲染

`watchEffect` 中注册的副作用会执行。

准确说,当reactive值被追踪 (track)的时候,watchEffect中的回调函数会执行;当watchEffect的依赖发生变化的时候,watchEffect中的回调函数会重复执行。

- watchEffect仅处理跟踪到的依赖

- 没有依赖的watchEffect可以当成一定会执行一次的效果处理发送请求等逻辑

因此`watchEffect` 在`track` 和`trigger` 阶段都会执行。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

副作用失效的处理

watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

另一个例子:

const data = ref(null)
watchEffect(async onInvalidate => {
  onInvalidate(() => {
    /* ... */
  }) // we register cleanup function before Promise resolves
  data.value = await fetchData(props.id)
})

watchEffect的执行时机(不推荐)

`effect` 有3种执行时机:

- pre(默认):effect在render之前执行

- post:将effect推迟到更新后执行

- sync :  当值变更时立刻执行

这3种时机可以用options.flush配置,具体参考示例。

Vue用一个队列来存储所有的effect(callback),当值变化多次的时候,只会有1次effect被执行,因为effect只会被写入队列一次。

Watch

watch和watchEffect是一类东西,底层实现一致,本质都是对变更的追踪(vue2 Watcher)。

实际使用的过程中,watch提供对单个reactive值的追踪,语义更明确(推荐用watch)。

function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

watch提供对观察源(WatcherSource)的监控,当观察源发生变化的时候,触发`callback` 函数。

观察源的定义:

type WatcherSource<T> = Ref<T> | (() => T)

1

export const WatcheExample01 = defineComponent({
  setup : () => {

    const c = ref(0)
    watch(c, (newVal, old) => {
      console.log(`c changed from ${old} to ${newVal}`)
    })

    setTimeout(() => {
      c.value ++
    }, 1000)
    return () => {
      return <div>{c.value}</div>
    }
  }
})

2、监听多个

export const WatcheExample02 = defineComponent({
  setup : () => {

    const c = ref(0)
    const d = ref(0)
    const m = ref([1,2,3,4,5])
    // m.value.push(1)
    watch(m, () => {

    })
    watch([c, d], (arrVals, oldValues) => {
      console.log(arrVals, oldValues)
    })

    setTimeout(() => {
      c.value ++
      d.value = 10 
    }, 1000)
    return () => {
      return <div>{c.value + d.value}</div>
    }
  }
})

3、监听函数

export const WatcheExample03 = defineComponent({
  setup : () => {

    const c = ref(0)
    // const state = reactive({...})
    watch(() => c.value, (newVal, old) => {
      console.log(`c changed from ${old} to ${newVal}`)
    })

    setTimeout(() => {
      c.value ++
    }, 1000)
    return () => {
      return <div>{c.value}</div>
    }
  }
})

从类型上,Reactive也是一种Ref。

我们可以观察一个函数,或者一个`Ref` 。举个例子(可以直接观察Ref或者一个返回值的函数)

// watching a getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// directly watching a ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

也可以同时观察多个对象:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

当然,watch有重载版的定义:

function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle

type WatcherSource<T> = Ref<T> | (() => T)

type MapSources<T> = {
  [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}

对于Watch Option,这里有这样的定义:

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // default: false
  deep?: boolean
}

如果我们需要在初始化后,直接执行`watch` 中的回调函数,可以使用`immediate=true`。当我们需要深度监听一个对象,例如一个数组,可以带上`deep=true` 。

1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值