从源码看vue3的watchEffect为什么不应该传入异步函数

出现问题的demo

最近看了一个渡一的视频,讲了一个在watchEffect中使用async函数出现的问题www.douyin.com/video/73275…
它的业务场景是调接口获取的视频地址并播放,同时可以手动修改播放速度倍率,这其实很简单,视频里的代码我敲了一下,确实有问题,当你点击修改倍率的时候,视频播放速度并没有变化,相当于rate的没有被追踪到!

借着这个问题,我们来一起看看watchEffect的源码,来找到问题之所在。

阅读vue3源码

watchEffect

全局搜索一下,可以定位到,watchEffect函数位于packages/runtime-core/src/apiWatch.ts这个文件里,它的代码也很简单。

// Simple effect.
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
  // 如果是watch
  // return doWatch(source as any, cb, options)
}

可以看出来与watch相比,也只是第二个参数的不同,因此可以说doWatch是这个整个watch的核心代码。

doWatch

// 支持监听reactive,数组,函数(仅watchEffect)
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object, 
cb: WatchCallback | null, //watch时的callback,watchEffect时为null
{
immediate,
deep,
flush,
once,
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ, // watch的第三个参数
): WatchStopHandle {
// !cb时是watchEffect,反之是watch,后面会多次判断
// 当watch 且带有once参数,即监听一次后马上停止监听
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
unwatch()
}
}
// deep参数将在后续支持传入数字,盲猜是监听到对象第几层
if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
warn(
`watch() "deep" option with number value will be used as watch depth in future versions. ` +
`Please use a boolean instead to avoid potential breakage.`,
)
}
// 如果是watchEffect,不支持一些参数
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`,
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`,
)
}
if (once !== undefined) {
warn(
`watch() "once" option is only respected when using the ` +
`watch(source, callback, options?) signature.`,
)
}
}
// 这个函数用于提示监听到对象不正确
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`,
)
}
// 当前组件对象,可能为null
const instance = currentInstance
// 递归获取reactive对象的keyName,用以实现深度监听
const reactiveGetter = (source: object) =>
deep === true
? source // traverse will happen in wrapped getter below
: // for deep: false, only traverse root-level properties
traverse(source, deep === false ? 1 : undefined)
let getter: () => any // 获取监听源的函数
let forceTrigger = false // 是否强制执行一次getter
let isMultiSource = false // 是否是复合监听源
if (isRef(source)) { // 如果监听的是ref,watch(ref,cb)
getter = () => source.value
forceTrigger = isShallow(source) 
} else if (isReactive(source)) { // 如果监听的是reactive,watch(reactive,cb)
getter = () => reactiveGetter(source) //递归获取监听的属性
forceTrigger = true 
} else if (isArray(source)) { // 如果监听的是数组,watch([a,b],cb)
isMultiSource = true // 复合监听源
// 判断里面有没有reactive 
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () => //把数据源转化为数据值的getter
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return reactiveGetter(s)
} else if (isFunction(s)) { //如果是函数就应该报错,函数不能也不需要被监听
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s) //如果这些都不是,那就直接报错
}
})
} else if (isFunction(source)) { //如果数据源是单一的function
if (cb) { // 如果是watch(fun,cn),报错
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// watchEffect时
getter = () => {
if (cleanup) { //如果有副作用函数,先清理副作用
cleanup()
}
//执行effect,这里封装了执行和拦截报错
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
}
} else {
// 错误情况
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 2.x array mutation watch compat
// TODO:还没搞懂__COMPAT__是什么,有没有懂的给我留个言
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter() //拷贝了一个监听源
if ( // 如果是数组就深度遍历
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
// 如果是watch,拷贝一遍监听源,并遍历key
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// 在ssr,我们不能在setup里立即执行监听回调,因为有可能还在node环境里
// unless it's eager or sync flush 除非是立即执行的回调
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup,
])
}
if (flush === 'sync') {
// ssr时清理监听
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else {
return NOOP
}
}
//设置监听的初始值,如果是复合的,那就是[{}],反之就是{}
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 所谓job就是一次监听执行  
const job: SchedulerJob = () => {
if (!effect.active || !effect.dirty) {
return
}
if (cb) {
// 普通watch
const newValue = effect.run() //其实从这里都能看到effect总是一个同步方法
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// 每次执行都先清理之前的副作用,这里表示值已经发生了改变
if (cleanup) {
cleanup()
}
// 执行一次回调
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup,
])
oldValue = newValue
}
} else {
// 这里是watchEffect
effect.run()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// post指mounted之后,所以需要延迟一下执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job) //二分查找去重,然后插入一个 内部维护的队列
}
// 这里正是watch核心代码,也是本文的主要内容
const effect = new ReactiveEffect(getter, NOOP, scheduler)
const scope = getCurrentScope()
// 取消监听的方法
const unwatch = () => {
effect.stop()
if (scope) {
remove(scope.effects, effect)
}
}
//dev环境下暴露两个勾子,方便调试
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
// 监听的过程
if (cb) {
//如果是watch
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
//如果是watchEffect
effect.run()
}
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
}

ReactiveEffect

从上面的代码,我们可以很明显的看出,watchEffect的关键就是ReactiveEffect这个类

// 注意这个变量
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = [] // 依赖的
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean // 是否允许自己触发自己
// 新暴露的几个勾子,主要是在debugger时候用
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
/**
* @internal
*/
_dirtyLevel = DirtyLevels.Dirty
/**
* @internal
*/
_trackId = 0
/**
* @internal
*/
_runnings = 0
/**
* @internal
*/
_shouldSchedule = false
/**
* @internal
*/
_depsLength = 0
constructor(
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope,
) {
// 收集当前这个函数至当前scope依赖
recordEffectScope(this, scope)
}
// 这个dirty其实是vue内部的函数,盲猜是更新时根据这个等级去合并更新
public get dirty() {
if (this._dirtyLevel === DirtyLevels.MaybeDirty) {
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i]
if (dep.computed) {
triggerComputed(dep.computed)
if (this._dirtyLevel >= DirtyLevels.Dirty) {
break
}
}
}
if (this._dirtyLevel < DirtyLevels.Dirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
}
return this._dirtyLevel >= DirtyLevels.Dirty
}
public set dirty(v) {
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}
// 主要函数,即执行这个依赖的effect函数
run() {
this._dirtyLevel = DirtyLevels.NotDirty
// 如果当前函数不执行,则先执行,这里是节流
if (!this.active) {
return this.fn()
}
// 储存旧的函数
let lastShouldTrack = shouldTrack
// 这里非常重要,把上一次执行的函数保存,然后会修改activeEffect = this
let lastEffect = activeEffect 
try {
shouldTrack = true
activeEffect = this
//执行次数+1
this._runnings++
// 清理之前的副作用
preCleanupEffect(this)
return this.fn()
} finally {
//如果报错了
postCleanupEffect(this)
this._runnings--
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
}
stop() {
// 停止监听,这很好理解
if (this.active) {
preCleanupEffect(this)
postCleanupEffect(this)
this.onStop?.()
this.active = false
}
}
}

activeEffect

从上面的代码阅读结束之后,我认为问题就是出现在activeEffect这个变量,当我们使用async/await时,其实js会把这个函数放到微任务队列里,如果此时有其他的同步effect,可能就会覆盖你的async/await。

因为activeEffect其实是个内部模块导出的全局变量,它可能会被其他的函数修改,也可能会被其他的ReactiveEffect所修改,这是无法追踪的,因此我们应当避免在watchEffect为什么不应该传入异步函数。

结语

解决方案1

在没研究源码之前,我会尽量不使用watchEffect来避免这个bug。大部分时间,url其实只需要获取一次的

解决方案2:改为watch

其实归根结底,这个问题就是你的依赖跟丢了,本来应该数据变化就执行函数,但是数据与执行函数的对应关系丢了。这时候我们就想到了,watch,watch其实是一种强绑定的依赖的,是你手动维护的依赖关系,无论有没有async/await,它都不会丢失。

原文链接:https://juejin.cn/post/7333144462225752098 作者:前端之光_水煮鱼

(0)
上一篇 2024年2月14日 上午11:06
下一篇 2024年2月14日 下午4:00

相关推荐

发表回复

登录后才能评论