概述
- watch 可以监听响应式数据的变化,从而触发指定的函数
- watch 函数监听三个参数
- 监听的响应式对象
- 回调函数 cb
- 配置对象 options
- imediate: 初始化完成后会被立刻触发一次
- deep: 深度监听
Debug Watch 源码
-
测试示例: watch.html,测试vue源码的watch函数
<script src='../../dist/vue.global.js'></script> <body> <div id='app'></div> </body> <script> const { reactive, watch } = Vue const obj = reactive({ name: '张三' }) watch(obj, (val, oldVal) => { console.log('watch ...') console.log(val) }) const timer = setTimeout(() => { clearTimeout(timer) obj.name = '李四' }, 2000) </script> -
进入 watch 函数,在源码 packages/runtime-core/src/apiWatch.ts 中
// implementation export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate> ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` ) } return doWatch(source as any, cb, options) } -
从这里可以看出,它本质上是一个 doWatch 函数的触发
function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, // 这个参数解析了options对象 { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { 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.` ) } } 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.` ) } // 这里引用一下 currentInstance const instance = currentInstance let getter: () => any let forceTrigger = false let isMultiSource = false // 注意这里,暂不会执行 if (isRef(source)) { getter = () => source.value // ref 数据访问需要使用 .value的方式 forceTrigger = isShallow(source) } else if (isReactive(source)) { // 注意这里会执行,因为我们定义的就是 reactive 响应式数据 getter = () => source // 这里响应式数据,getter 直接被赋值 deep = true // 这里deep会默认标识为 true, 及时程序员没有传递 } else if (isArray(source)) { isMultiSource = true forceTrigger = source.some(s => isReactive(s) || isShallow(s)) getter = () => source.map(s => { if (isRef(s)) { return s.value } else if (isReactive(s)) { return traverse(s) } else if (isFunction(s)) { return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) } else if (isFunction(source)) { if (cb) { // getter with cb getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup] ) } } } else { getter = NOOP __DEV__ && warnInvalidSource(source) } // 2.x array mutation watch compat if (__COMPAT__ && cb && !deep) { const baseGetter = getter getter = () => { const val = baseGetter() if ( isArray(val) && checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) ) { traverse(val) } return val } } // 这里会执行 if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) // 这里做一层包装,先不管 } let cleanup: () => void let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } // in SSR there is no need to setup an actual effect, and it should be noop // unless it's eager 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 ]) } return NOOP } // 这里有一个三元运算,会走到 INITIAL_WATCHER_VALUE 这里 let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE // 核心的job函数 const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again 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 : 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 // 这里看下 issue #1727 // 这里声明调度器 let scheduler: EffectScheduler if (flush === 'sync') { scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // 此时调度器变成一个回调函数,包装了 job 函数 // default: 'pre' scheduler = () => queuePreFlushCb(job) } // 这里同样走了老套路,传入 getter 和 调度器 const effect = new ReactiveEffect(getter, scheduler) if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger } // 这里开始最核心的最后一块逻辑 // initial run if (cb) { // 这里读取配置 if (immediate) { job() // 这里可以看出job的执行会代表watch函数的自动触发 } else { // 这里会执行, 执行run函数中的 fn 函数 oldValue = effect.run() } } else if (flush === 'post') { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ) } else { effect.run() } // 最终会return这个函数 return () => { effect.stop() // 停止effect监听 if (instance && instance.scope) { remove(instance.scope.effects!, effect) } } } -
这个watch是比较复杂的,里面核心的几个点:
- scheduler 调度器和我们的effect实例两者之间相互作用
- 一旦effect触发了 scheduler ,就会调用 queuePreFlushCb
- 同时,我们知道,一旦job函数执行,因为回调cb的传递,watch函数必定会被触发
-
当watch函数执行完毕,2s之后,测试代码中会触发 setter 行为
const timer = setTimeout(() => { clearTimeout(timer) obj.name = '李四' }, 2000) -
setter行为的触发本质上是之前收集的依赖被触发,也就是之前分析过的
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }- triggerEffects() 后续一系列的行为,因为有调度器的存在,所有会执行
- effect.scheduler()
- triggerEffects() 后续一系列的行为,因为有调度器的存在,所有会执行
-
这个调度器就是watch中的
- scheduler = () => queuePreFlushCb(job)
- 好,我们看下进入 queuePreFlushCb 函数
-
这个函数 在 scheduler.ts 中
// 注意,这里的 cb 参数,就是我们的核心job函数 export function queuePreFlushCb(cb: SchedulerJob) { queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) } -
其本质还是 queueCb 函数,进入
function queueCb( cb: SchedulerJobs, activeQueue: SchedulerJob[] | null, pendingQueue: SchedulerJob[], index: number ) { if (!isArray(cb)) { if ( !activeQueue || !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index) ) { // 这里会执行, pendingQueue.push(cb) } } else { // if cb is an array, it is a component lifecycle hook which can only be // triggered by a job, which is already deduped in the main queue, so // we can skip duplicate check here to improve perf pendingQueue.push(...cb) } // 这里进入 queueFlush() } -
进入 queueFlush
function queueFlush() { // 判断状态,这里会进入 if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } } -
注意这里的 resolvedPromise 的定义,它实际就是Promise的resolve 函数
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any> -
也就是说通过异步微任务来处理队列任务,这里通过then函数执行 flushJobs,这个是最后的异步执行函数,会等待所有同步任务执行完成后立即被触发, 进入它
function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || new Map() } // 这里会执行 flushPreFlushCbs(seen) // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child so its render effect will have smaller // priority number) // 2. If a component is unmounted during a parent component's update, // its update can be skipped. queue.sort((a, b) => getId(a) - getId(b)) // conditional usage of checkRecursiveUpdate must be determined out of // try ... catch block since Rollup by default de-optimizes treeshaking // inside try-catch. This can leave all warning code unshaked. Although // they would get eventually shaken by a minifier like terser, some minifiers // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610) const check = __DEV__ ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job) : NOOP try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { if (__DEV__ && check(job)) { continue } // console.log(`running:`, job.id) callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { flushIndex = 0 queue.length = 0 flushPostFlushCbs(seen) isFlushing = false currentFlushPromise = null // some postFlushCb queued jobs! // keep flushing until it drains. if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) { flushJobs(seen) } } } -
进入内部的 flushPreFlushCbs 函数
// 这个函数通过任务队列的形式,触发job函数 export function flushPreFlushCbs( seen?: CountMap, parentJob: SchedulerJob | null = null ) { // 检测是否有数据,这里是之前的job函数 if (pendingPreFlushCbs.length) { currentPreFlushParentJob = parentJob // 这里 activePreFlushCbs 取代了 pendingPreFlushCbs activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 去重 pendingPreFlushCbs.length = 0 // 这里清空自己,保证下次不会被触发 if (__DEV__) { seen = seen || new Map() } for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++ ) { if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex]) ) { continue } // 这里会执行,也就是job函数会被触发 activePreFlushCbs[preFlushIndex]() } activePreFlushCbs = null preFlushIndex = 0 currentPreFlushParentJob = null // recursively flush until it drains flushPreFlushCbs(seen, parentJob) } } -
既然上述函数是为了处理队列中的job函数,我们再回到job函数中看看,位置:apiWatch.ts 中,doWatch 函数中的一段逻辑
const job: SchedulerJob = () => { if (!effect.active) { return } // 这里会执行 if (cb) { // watch(source, cb) const newValue = effect.run() // 运行run函数,产生了 newValue, 这里是我们的响应性对象 if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } // 这里是一个通用的 try catch 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 : oldValue, onCleanup ]) // 更新挂载 oldValue 变量 oldValue = newValue } } else { // watchEffect effect.run() } } -
我们看一下这个 通用的 callWithAsyncErrorHandling 函数
export function callWithAsyncErrorHandling( fn: Function | Function[], instance: ComponentInternalInstance | null, type: ErrorTypes, args?: unknown[] ): any[] { if (isFunction(fn)) { const res = callWithErrorHandling(fn, instance, type, args) if (res && isPromise(res)) { res.catch(err => { handleError(err, instance, type) }) } return res } const values = [] for (let i = 0; i < fn.length; i++) { values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)) } return values } // 其实这个函数的位置,在 callWithAsyncErrorHandling 之上,目前是拿过来看看 export function callWithErrorHandling( fn: Function, instance: ComponentInternalInstance | null, type: ErrorTypes, args?: unknown[] ) { let res try { res = args ? fn(...args) : fn() } catch (err) { handleError(err, instance, type) } return res } -
也就是说,vue内部,通过一个 callWithAsyncErrorHandling 函数来统一处理 所有可能出现的代码,统一进行try catch的操作
-
从本质上来说 watch函数触发,是基于job函数触发来执行 effect.run 函数的调用,也就是会执行 我们的 fn 函数,所以,只要job被触发,watch就会被触发,当run() 执行了,就会拿到新的值
关于 scheduler 调度器的探究
- 对于watch和computed都用到了我们的调度器,scheduler
- 调度器包含两块内容
- 懒执行
- 调度器本身
1 )懒执行
- 在effect.ts中的 effect 函数内部,有懒执行的相关逻辑
if (!options || !options.lazy) { _effect.run() }- 这里可以看出懒执行为false时,run函数也会立即被执行,在watch里的懒执行是true,所以watch不会立即执行run函数
2 ) 调度器
-
调度器主要分成两部分
- 控制执行逻辑
- 控制执行顺序
-
测试调度器,新建测试用例 scheduler.html
<script src='../../dist/vue.global.js'></script> <body> <div id='app'></div> </body> <script> const { reactive, effect } = Vue const obj = reactive({ count: 1 }) effect(() => { console.log(obj.count) }) obj.count = 2 console.log('over') </script>- 这里的执行打印顺序是:1, 2, over
2.1 ) 控制代码执行顺序
-
我们想,修改这个打印执行顺序,想要的顺序是: 1, over, 2
- 这个意思是:effect函数的执行是懒的,是后发的,是异步的
-
这时候,我们就需要用到的是调度器,我们修改下测试示例程序
const { reactive, effect } = Vue const obj = reactive({ count: 1 }) effect(() => { console.log(obj.count) }, { scheduler() { setTimeout(() => { console.log(obj.count) }) } }) obj.count = 2 console.log('over')- 这时候的输出顺序就是: 1, over, 2
-
在 effect.ts 中的 triggerEffect 函数中,有以下的流程控制
if (effect.scheduler) { effect.scheduler() } else { effect.run() } -
存在调度器,则会执行调度器,否则才会执行 run 函数,因为示例程序修改后,传入了 调度器,所以此时run函数不再会执行,因为调度器中传入的是异步任务,所以在同步任务执行完毕后,开始执行调度器里的流程
-
所以,scheduler可以起到影响代码执行顺序的功能
2.2 ) 控制代码执行规则
- 修改下测试用例的程序
const { reactive, effect } = Vue const obj = reactive({ count: 1 }) effect(() => { console.log(obj.count) }) obj.count = 2 obj.count = 3 - 上述代码中有两次setter行为,实际上是不需要的,在vue源码中 queuePreFlushCb 可以完成这个需求
- 在测试示例中,它会依次输出:1, 2, 3
- 我们修改vue源码,导出这个 queuePreFlushCb 函数,来让用例使用
export { nextTick, queuePreFlushCb } from './scheduler' - 修改完成vue源码后,执行下 $
npm run dev - 如果我们继续修改测试用例
const { reactive, effect, queuePreFlushCb } = Vue const obj = reactive({ count: 1 }) effect(() => { console.log(obj.count) }, { scheduler() { queuePreFlushCb(() => console.log(obj.count)) } }) obj.count = 2 obj.count = 3 - 这时候,只会输出 1, 3 达成我们的目标了
- 实际上 在watch中的调度器就是 queuePreFlushCb 这个函数
scheduler = () => queuePreFlushCb(job)
- 这个调度器最终会触发 resolvedPromise.then 的函数,这个就是把我们当前队列扔进微任务中来顺序执行
- 最终obj.count是3,最终在微任务中输出的也是3,因为微任务是在同步代码执行完毕后,才会去执行
删减后的核心实现代码
1 )实现调度器
- 新建 packages/runtime-core/src/scheduler.ts
let isFlushPending = false // 状态表示 const pendingPreFlushCbs: Function[] = [] // 回调队列 const resolvedPromise = Promise.resolve() as Promise<any> let currentFlushPromise: Promise<void> | null = null export function queuePreFlushCb(cb: Function) { queueCb(cb, pendingPreFlushCbs) } function queueCb(cb: Function, pendingQueue: Function[]) { pendingQueue.push(cb) queueFlush() } // 依次执行队列中的函数 function queueFlush() { if (isFlushPending) return // 真,什么都不做 // 只有这个状态为假,才继续执行 isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } // 处理队列 function flushJobs() { isFlushPending = false flushPreFlushCbs() } // 用于循环进行队列处理 export function flushPreFlushCbs() { if (!pendingPreFlushCbs.length) return // 有队列内容时,使用新的标识,进行内容拷贝 let activePreFlushCbs = [...new Set(pendingPreFlushCbs)] pendingPreFlushCbs.length = 0 // 清空这个旧的队列 for (let i = 0; i < activePreFlushCbs.length; i++) { activePreFlushCbs[i]() } }
2 )实现watch函数
-
首先补充下 effect.ts 中的effect()函数和ReactiveEffect类,添加一个option参数,用于支持懒执行
import { extend } from '@vue/shared' // 这个api就是 Object.assign,目的是通用 export interface ReactiveEffectOptions { lazy?: boolean, scheduler?: EffectScheduler } export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions) { const _effect = new ReactiveEffect(fn) // 如果存在,则与_effect合并,这就意味着 options包含调度器,_effect中也会有调度器 if (options) { extend(_effect, options) } if (!options || !options.lazy) { _effect.run() } } // 这个函数主要添加一个stop的空函数 export class ReactiveEffect<T = any> { computed?: ComputedRefImpl<T> constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {} run() { activeEffect = this return this.fn() } // 后期实现 stop() {} } -
再扩展 reactive.ts 补充 一个 isReactive 判断是否为响应性对象的函数,需要补充如下:
// 定义枚举 export const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive' // 定义一个是 reactive 对象的私有属性标识 } // 补充这个createReactiveObject函数 function createReactiveObject( target: object, baseHandlers: ProxyHandler<any>, proxyMap: WeakMap<object, any> ) { const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, baseHandlers) // 添加下面这行 proxy[ReactiveFlags.IS_REACTIVE] = true // 将私有属性标识标记为 true proxyMap.set(target, proxy) return proxy } // 判断是否是 响应式对象 export function isReactive(value): boolean { return !!(value && value[ReactiveFlags.IS_REACTIVE]) } -
新建 packages/runtime-core/src/apiWatch.ts
import { EMPTY_OBJ, hasChanged } from '@vue/shared' import { isReactive, ReactiveEffect } from '@vue/reactivity' import { queuePreFlushCb } from './scheduler' export interface WatchOptions<immediate = boolean> { immediate?: immediate, deep?: boolean } export function watch(source, cb: Function, options?: WatchOptions) { return doWatch(source, cb, options) } function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) { let getter: () => any // 当前是响应性对象 if (isReactive(source)) { getter = () => source deep = true } else { // 否则 getter = () => {} } // 处理deep场景 if (cb && deep) { // 这种写法需要注意:这里实际上是内部进行依赖收集的地方 const baseGetter = getter // 浅拷贝引用 getter = () => baseGetter() } let oldValue = {} const job = () => { if (cb) { const newValue = effect.run() if (deep || hasChanged(newValue, oldValue)) { cb(newValue, oldValue) oldValue = newValue } } } let scheduler = () => queuePreFlushCb(job) const effect = new ReactiveEffect(getter, scheduler) if (cb) { if (immediate) { job() } else { oldValue = effect.run() } } else { effect.run() } return () => { effect.stop() } } -
导出各项需要的api, 比如watch, 重新 npm run dev 后,创建watch测试程序 watch.html
<script src="../dist/vue.js"></script> <body> <div id='app'></div> </body> <script> const { reactive, watch } = Vue const obj = reactive({ name: '张三' }) watch(obj, (val, oldVal) => { console.log('watch ...') console.log(val) }, { immediate: true // 添加这个配置,会立即执行一次,但是下面2s后的异步回调没有被执行,这里watch没有监听到 reactive 的变化 }) const timer = setTimeout(() => { clearTimeout(timer) obj.name = '李四' }, 2000) </script> -
此时,watch是无法监听到reactive变化的, Why?
- 因为我们没有进行依赖的收集,只有在 2s 的 setTimeout 函数中进行 setter 行为
- 所以没有依赖可以触发
-
实际上,我们自己的 apiWatcher.ts 中
getter = () => baseGetter()这里,和源码是不一样的,Vue源码是getter = () => traverse(baseGetter()) -
Vue源码中的 traverse函数就是为了一直收集getter行为,也就是说在测试程序中没有手动触发过getter行为, 我们要在watch内部把整个source对象里的所有属性,主动触发一次getter行为来完成依赖收集
-
修改上述的 apiWatch.ts 中的getter, 如下
function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) { // ... // 处理deep场景 if (cb && deep) { // 这种写法需要注意:这里是依赖收集的地方 const baseGetter = getter // 浅拷贝引用 getter = () => traverse(baseGetter()) // 内部进行依赖收集触发getter行为 } // ... } export function traverse(value: unknown) { if (!isObject(value)) { return value } // 循环属性 for (const key in value as object) { traverse((value as object)[key]) // 访问当前属性,触发 getter 行为 } return value } -
这样,整个watch函数就可以正常运行了
总结
- 整个watch的实现逻辑还是依赖于 reactiveEffect 的功能(依赖收集和触发的过程)
- 和之前reactive, ref 的依赖收集,触发的区别在于 watch 中的依赖收集操作是被动收集的
- 还有就是watch中的scheduler调度器,用于控制执行顺序和规则,本身是一个函数,内部配合各个方便实现调度功能, 比如在watch中的 queuePreFlushCb 函数
243



被折叠的 条评论
为什么被折叠?



