前言
原本打算本章讲讲computed,但是computed的源码相当复杂,使用文章的形式说清楚,难度真的很大,所以暂时跳过computed,先说说watch。
watch即为监听的意思:监听响应式数据,每当状态发生变化,就会触发回调函数。
如果大家对之前的源码分析有所理解的话,我相信大家可以猜到watch实现原理,一定是初始化的时候进行依赖收集,依赖项发生变化的时候依赖触发。
如果能领悟到这一层,那么对vue3的核心实现你已经有所理解啦。
接下来就让我们走进watch的世界,让我们看看,vue3是如何实现他的吧。
首先还是放出watch的逻辑图,watch的逻辑相对简单,因为对于watch而言,响应式是其一部分逻辑。
带着问题看源码
在我刚刚使用Vue3 watch的时候,经常出现以下让我无法解释的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const user = reactive({ name: '卖鱼强' }) watch(user, (value) => console.log('第一', value)) watch(user.name, (value) => console.log('第二', value)) watch(() => user, (value) => console.log('第三', value)) watch(() => user.name, (value) => console.log('第四', value))
user.name = '狂飙强'
const user = ref('卖鱼强') watch(user, value => console.log('第一个watch', value)) watch(user.value, value => console.log('第二个watch', value)) watch(() => user, value => console.log('第三次watch', value)) watch(() => user.value, value => console.log('第四次watch', value))
user.value = '狂飙强'
|
以上案例,我相信大部分写vue的同学,都很难在第一时间准确判断其watch是否有效无效,接下来就让我们一起从源码中寻找答案。
正文
watch的源码并不在reactivity中,而是在runtime-core中
关于这一点我会谈谈我的想法,讨论一下为什么不在reactivity中,而在runtime-core中。
watch初始化
当我们使用watch的时候,其执行的具体源码位置为packages/runtime-core/src/apiWatch.ts
line131
1 2 3 4 5 6 7 8 9 10 11 12
| export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions ): WatchStopHandle { return doWatch(source as any, cb, options) }
export interface WatchOptions { immediate?: boolean deep?: boolean }
|
通过以上代码,我们可以了解到,watch是存在三个参数的
- source :监听项
- cb:watch的回调函数
- options: 关于watch的设置,内部存在2个参数
- immediate 首次是否运行
- deep 是否深度监听
这些消息和我们通过Vue文档了解到的信息完全一致,最终我们会发现,其实际返回了一个doWatch函数,并将watch的三个参数传递了进去。
doWatch内部的逻辑就是watch实现的核心逻辑了,我们从三个阶段分析doWatch的代码。
第一阶段:处理source,监听项分析
第二阶段:构建响应式模块,完成依赖收集
第三阶段:明确依赖触发方式
第一阶段:处理source,监听项分析
我们在使用watch的时候,第一个参数,也就是被监听项,是可以传入很多类型的,ref reactive function array,在doWatch函数中,我们可以看到,针对不同类型与属性的source,都做了个性化的依赖处理。
接下来就让我们看看,doWatch都是如何处理这些变量的吧。
ref
后续getter函数一旦执行,将会访问ref,触发 ref本身的依赖收集
1 2 3 4 5
| if (isRef(source)) { getter = () => source.value }
|
reactive
后续getter函数一旦执行,将会访问reactive,触发 ReactiveEffect 完成依赖收集
1 2 3 4 5 6
| if (isReactive(source)) { getter = () => source deep = true }
|
Function
后续getter函数一旦执行,将会运行fn,访问函数返回值,如果fn返回的是ref 或者reactive 就会触发相应的依赖收集
1 2 3 4 5 6
| if (isFunction(source)) { getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) }
|
Array
后续getter函数一旦执行,将会访问getter中的所有的访问值,如果fn返回的是ref 或者reactive 就会触发相应的依赖收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| if (isArray(source)) { isMultiSource = true 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) } }) }
export function callWithErrorHandling(fn,instance,type,) { return fn() }
|
未知类型
1 2 3
| export const NOOP = () => {}
getter = NOOP
|
以上就是watch针对所有类型的source的处理。
我们可以发现其实就做了一件事,就是将其包装为getter函数,getter函数一旦运行,便可以触发相关依赖收集。
完成第一阶段的分析,其实我们文章开头提出的问题已经有了明确答案,我们回过头来继续看看
1 2 3 4 5 6 7 8 9 10
| const user = reactive({ name: '卖鱼强' })
watch(user, () => {})
watch(user.name, () => {})
watch(() => user, () => {})
watch(() => user.name, () => {})
|
以上就是reactive + watch不同使用方式的效果解读。
有兴趣的小伙伴可以试试解读一下ref + watch的结果。
如果真的记不住,我们就记住下面的这句话:watch 监听对象本身,使用对象的形式;watch监听对象内部属性,使用函数形式。
第二阶段:构建响应式模块,完成依赖收集
这上小节,我们完成getter函数的构建,这一步我们需要进行依赖触发,与依赖收集,使watch的监听功能正式生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }
let oldValue = isMultiSource ? [] : {}
const job: SchedulerJob = () => { const newValue = effect.run() if (deep || hasChange(newValue, oldValue)) { cb(newValue, oldValue) } }
let scheduler: EffectScheduler
if (flush === 'sync') { scheduler = job } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { scheduler = () => queuePreFlushCb(job) }
const effect = new ReactiveEffect(getter, scheduler)
if (cb) { if (immediate) { job() } else { oldValue = effect.run() } }
return () => { effect.stop() }
|
到此为止,我们可以明确了解到,在Vue的初始化阶段,watch其内部通过ReactiveEffect,以及effect.run()的触发,完成了watch需要监听的变量与触发函数的绑定,ReactiveEffect逻辑在Vue3硬核源码解析系列(3) reactive + effect源码解析可以了解其具体实现。
也就是相当于说,watch内部通过手动访问source,触发source的get事件,source依赖一旦触发,就会开始依赖收集,就会收集到watch的第二个参数cb,经进而完成watch的依赖收集;只要source发生改变,一定会触发cb函数。
其实到这里watch的核心源码就已经结束了,依赖已经完成收集;
当被监听变量或者属性发生变化的时候,cb函数一定会执行,但是watch的执行时机是非常有讲究的;
所以接下来就要讲讲watch第三个参数的flush,该字段就是控制cb函数的执行时机。
第三阶段:依赖触发
当我们watch监听的字段发生变化的时候,watch的第二个参数,cb会被触发,但是并不是监听字段发生变化的下一步就立刻触发。
这里我们回顾一下watch源码中变量scheduler的相关逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (flush === 'sync') { scheduler = job } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job) } else { scheduler = () => queuePreFlushCb(job) }
export const queuePostRenderEffect = __FEATURE_SUSPENSE__ ? queueEffectWithSuspense : queuePostFlushCb
|
我们可以看到,flush参数不同的时候scheduler的值也是不同的
如果我们指定了flush是sync,则source发生变化下一个同步任务就是执行watch的cb函数,
如果我们不进行指定,默认将是pre,则会触发queuePreFlushCb(job)
如果指定为post,则会触发queuePostFlushCb(job)
根据文档我们可以了解到当flush为pre的时候,watch第二个参数cb,将会在Vue组件更新之前被调用,post则会让cb函数在Vue组件更新之后被调用
接下来就让我们看看queuePreFlushCb与queuePostFlushCb内部是如何实现的吧!
queuePreFlushCb与queuePostFlushCb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| const resolvedPromise = Promise.resolve() let currentFlushPromise = null let isFlushPending = false
const pendingPreFlushCbs: SchedulerJob[] = [] let activePreFlushCbs: SchedulerJob[] | null = null let preFlushIndex = 0
export function queuePreFlushCb(cb: SchedulerJob) { queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) }
export function queuePostFlushCb(cb: SchedulerJobs) { queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex) }
function queueCb( cb: SchedulerJobs, activeQueue: SchedulerJob[] | null, pendingQueue: SchedulerJob[], index: number ) { pendingQueue.push(cb) queueFlush() }
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } }
function flushJobs() { isFlushPending = false flushPreFlushCbs() try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { flushPostFlushCbs(seen) isFlushing = false } }
export function flushPreFlushCbs() { if (pendingPreFlushCbs.length) { let activePreFlushCbs = [...new Set(pendingPreFlushCbs)] pendingPreFlushCbs.length = 0 for (let i = 0; i < activePreFlushCbs.length; i++) { activePreFlushCbs[i]() } } }
export function flushPostFlushCbs(seen?: CountMap) { flushPreFlushCbs() if (pendingPostFlushCbs.length) { const deduped = [...new Set(pendingPostFlushCbs)] pendingPostFlushCbs.length = 0 activePostFlushCbs = deduped
for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { activePostFlushCbs[postFlushIndex]() } activePostFlushCbs = null postFlushIndex = 0 } }
|
以上代码看起来似乎比较复杂,但是执行的逻辑其实非常简单,Vue3的更新队列存在三种分别是pre,queue,post,这三个队列按照顺序执行相应代码
- 执行pre队列中的代码
- 执行queue队列中的代码,(queue为组件update的相关逻辑)
- 执行post队列中的代码
这里对照vue3文档,我们可以发现,我们的分析是符合文档描述的。
因为涉及到vue3的更新队列,这并非watch关联的知识,为了方便源码阅读,可以假设watch的flush的参数为async,这样是最好理解的。
到此为止,我们的watch核心源码分析就全部完毕了。
关于ref的一些问题
watch的源码为什么在runtime-core中?
关于这一点我是这么理解的,watch不仅仅是一个响应式组件,他涉及到了组件的生命周期,更新渲染等等逻辑,放在runtime中更好与组件系统进行集成,
总结
通过以上源码分析我们可以发现,watch的响应式原理相对来说是比较简单的,完全依赖我们的之前说过的ReactiveEffect,所以如果小伙伴了解reactive的源码,相信看watch的源码的响应式部分是非常轻松的
相对于其他api,watch的响应式实现具备一下2个特点
- watch的依赖收集是被动触发的
- watch的依赖触发,实际上是调度器scheduler,然后通过不同的flush,达到控制执行顺序、规则的目的。
watch的源码分析就到这里,我们下期再见吧~👋🏻