前言
最近在公司排查bug得时候发现一个有关于watch和nextTick执行顺序得问题,原本我以为watch得触发会向微任务队列里push一条任务,nextTick也是一样,这样的话如果nextTick先执行,之后在触发watch得话会先执行nextTick得回调,然后在执行watch得监听函数,可是项目里得代码怎么调试执行顺序都和我想象得不一样,为了一谈究竟,搞清楚watch和nextTick之间到底有什么关系,我准备将vue2得源码打包出一份带有map得集成到项目中,来彻底搞懂这个流程。
打包vue2源码生成带有map文件vue.js
vue2得源码从git上clone下来之后,先执行install安装依赖 我得vue2版本是2.7.16,查看其package.json文件下得打包命令发现执行了一个js文件,查看这个build.js文件,其实做的事情很简单,就是读取一下所有得打包配置,然后使用rollup进行打包。
我得工程里面只用到了vue.js,所以我们可以把用不到得注释掉,只保留这一个文件即可,并把返回的配置项中output中添加一行sourcemap:true详情见下图

接下来还需要调整一下build.js文件让生成得map写入磁盘中
之后执行npm run build在dist目录下就生成了带有map得vue.js

将带有map得vue.js集成到项目中
我得项目是基于vue2得版本也是2.7.16,与打包出来得vue.js版本一致,之后使用我们自己编译出来的vue.js和vue.js.map替换项目中得vue文件,直接放对应得dist文件中即可
我得项目是基于vite构建得,本身能够加载node_modules中得map文件,但是由于vite构建存在缓存情况,可以查看下图目录 实际加载es模块得时候都是这个目录下面得,如果构建之后没有vue.js.map要执行一下vite --force清空.vite重新生成

之后在断点调试得时候就可以看到调用栈上得vue源码了,都是编译前得ts文件,我们就可以分析项目中代码运行watch和nextTick得真实执行流程了

watch与nextTick流程梳理
现在写两个简单组件,通过他们分析一下vue得执行流程


- 触发函数ccc得时候先改变了响应式数据
testProps父组件会将一个Wather放入queue队列中,这个Wather是渲染Wather,渲染Watcher会执行组件重新渲染,vue在组件渲染得时候会创建这个Wather并关联组件得响应式数据,当组件内得响应式数据发生改变得时候,会触发这个Wather,进行组件重新渲染

-
这个watcher并不会立刻执行而是将一个函数
flushSchedulerQueue放到nextTick中,flushSchedulerQueue这个函数得作用就是执行queue这个队列
-
之后我们看下nextTick中做了什么,看他的代码就是把nextTick中得回调函数放到了一个
callback数组中,之后如果没有pending就执行timerFunc()第一次执行得时候没有pending所以回直接执行这个函数

- 这个函数内部就是把
flushCallbacks放到了微任务队列中

- 到目前为止我们梳理一下已经出现得两个队列,queue和callBacks以及两个刷新函数他们之间得关系如下图,wather1指渲染wather

- 之后在这个渲染周期中就没有其他操作了,按顺序执行微任务,先执行
flushSchedulerQueue中的队列,先执行watcher1(渲染watcher)执行渲染函数渲染自己得组件,接着触发子组件得渲染,在渲染子组件得过程中发现,子组件监听得testProps值改变了,于是就将testProps得监听函数放入queue中,于是在执行flushSchedulerQueue得过程中queue得长度变化了,flushSchedulerQueue会执行完所有队列中所有得watcher。这也是vue中多次响应式数据变更但只有一次渲染得原理,其实多次变更都被收集到queue队列中,一个渲染周期中callbacks队列中只会有一个flushSchedulerQueue这个函数,保证了多个响应式数据改变,在同一个渲染周期内只渲染一次 - 刷新完watcher队列(flushSchedulerQueue函数执行完毕)之后执行nextTick得回调,将data得值改变,这个值也是响应式数据,触发流程和testProps一样,但是由于这个渲染周期中得flushSchedulerQueue函数已经执行完毕,所以waiting标记改为了false,导致这次得渲染watcher不会立刻执行,而是放到了微任务队列中,在本次微任务执行完毕之后在执行

- 此时得情况如下图所示,第一个微任务已经执行完毕

总结
在调试完毕之后我发现我之前存在得问题: watch 本身不会创建微任务,它只是把 watcher 放入 queue 队列,如果在nextTick之后有watcher被放入队列,那么这个watcher得执行并不一定会在nextTick得回调之后,具体要看flushSchedulerQueue在callbacks中得顺序
下图情况会先输出222;nextTick回调函数先被放入callbacks队列中 
下图情况则会先输出两个watcher结果,在输出222,flushSchedulerQueue先被放到callbacks队列中
