本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
作者:水煮鱼写前端
出现问题的 demo
最近看了一个视频,讲了一个在 watchEffect 中使用 async 函数出现的问题 www.douyin.com/video/73275…[1] 它的业务场景是调接口获取的视频地址并播放,同时可以手动修改播放速度倍率,这其实很简单,视频里的代码我敲了一下,确实有问题,当你点击修改倍率的时候,视频播放速度并没有变化,相当于 rate 的没有被追踪到!
借着这个问题,我们来一起看看watchEffect的源码,来找到问题之所在。
阅读 vue3 源码
watchEffect
全局搜索一下,可以定位到,watchEffect 函数位于packages/runtime-core/src/apiWatch.ts这个文件里,它的代码也很简单。
typescript复制代码// 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 | undefinedexport 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,它都不会丢失。
参考资料
[1]
https://www.douyin.com/video/7327589377025084712: https://link.juejin.cn?target=https%3A%2F%2Fwww.douyin.com%2Fvideo%2F7327589377025084712
最后
如果你觉得这篇内容对你挺有启发,我想邀请你帮我个小忙:
点个「喜欢」或「在看」,让更多的人也能看到这篇内容
我组建了个氛围非常好的前端群,里面有很多前端小伙伴,欢迎加我微信「sherlocked_93」拉你加群,一起交流和学习
关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。
点个喜欢支持我吧,在看就更好了