「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
前言
前文 Vue3.0源码学习——初始化流程分析(3.patch过程)
学习了首次挂载的过程中,在 patch 函数中会一路调用 mountComponent挂载组件 =》 setupRenderEffect 副作用安装函数,这个函数的执行过程中会建立更新机制,当前组件响应式数据发生变化将会重新执行更新函数,内部又会递归调用 patch
调用栈
为了方便查看更新的过程,首先新建一个能触发Vue更新机制的页面
<div id="app">
<h1>vue3 更新流程</h1>
<p>{{ counter }}</p>
</div>
<script>
const app = Vue.createApp({
data() {
return {
counter: 1
}
},
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
})
app.mount('#app')
</script>
-
这个页面中有一个响应式的数据
counter,在页面加载完成后,每隔1秒会将counter+1 -
在浏览期中打开页面,然后找到
patch函数定义的位置/packages/runtime-core/src/renderer.ts,在switch处打上断点,这里是判断当前要比较新的vonde的类型,立马就进入了断点,证明counter的值已经改变了并触发了patch(此时还未渲染到页面上)
- 此时查看调用栈就是在更新流程中调用的函数
从下往上看
- anonymous 自定义的
setInterval方法 - set 响应式拦截
- set3 调用拦截函数
- trigger 触发组件更新函数
- triggerEffects 调用触发函数 => queueJob => queueFlush => 这里其实是一个异步更新机制,将需要更新的地方放入队列然后在异步队列中一次性更新 => flushJobs
- run => componentUpdateFn 这里就是调用了
setupRenderEffect副作用安装函数中的组件更新函数 - ptach
单步调试
- 在测试页面中打断点,然后根据调用栈找到对应调用函数的地方
- 单步进入响应式拦截操作
PublicInstanceProxyHandlers函数,位置/packages/runtime-core/src/componentPublicInstance.ts,触发了第一次set拦截(proxy对象),并会重新对counter赋值
- 进入到
set拦截函数,位置/packages/reactivity/src/baseHandlers.ts触发trigger组件更新函数,由于是更新counter因此走else if分支
- 进入
trigger,位置/packages/reactivity/src/effect.ts,触发triggerEffects
- 进入
triggerEffects,遍历依赖收集,依次触发依赖的scheduler
- 进入
scheduler,就到了setupRenderEffect中 ,这个scheduler就是将当前组件的更新函数放入队列中,这里是一个异步队列queueJob,将在将来某个时候执行,异步函数最终要执行componentUpdateFn组件更新函数,这里的effect是异步的方式调用componentUpdateFn函数产生的副作用
componentUpdateFn一开始已经挂载过,isMounted === true,因此走else 分支,也就是更新流程
-
更新流程中拿到新旧vnode,
nextTree和prevTree然后放入patch进行比较更新并更新视图 -
再单步执行完成
patch,这时候就可以看到视图发生了变化
- 到这里就跑完了更新流程
源码赏析
- 副作用安装函数
packages\runtime-core\src\renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
...
) => {
// 组件更新函数
// 对patch产生了调用,在调用之前会会获取渲染函数的结果,也就是当前组件的vnode
// 在首次执行渲染函数时,其实已经建立了依赖关系
const componentUpdateFn = () => {
if (!instance.isMounted) { // 首次挂载
...
} else {
// 更新组件
...
// 获取最新的vnode
const nextTree = renderComponentRoot(instance)
...
// 获取缓存的oldVnode
const prevTree = instance.subTree
instance.subTree = nextTree
...
// 执行diff patch
patch(
prevTree,
nextTree,
...
)
...
}
}
// create reactive effect for rendering
// 为组件的渲染创建一个响应式的副作用函数
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, // 执行函数
() => queueJob(instance.update), // scheduler定时器任务
instance.scope // track it in component's effect scope
))
// 前面被queueJob的是effect.run
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
...
update()
}
- 异步更新队列
packages\runtime-core\src\scheduler.ts
export function queueJob(job: SchedulerJob) {
...
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (job.id == null) {
queue.push(job) // 将更新函数直接放入队列
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush() // 启动批量任务执行
}
}
...
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs) // 异步执行队列中的更新函数
}
}