渲染流程
前言
本文章以vue@3.5.22版本和组合式写法作为基准进行的调试分析,如为vue2或者setup的选项式的写法,请不要按照当前文章来参考,其实现的方式不同。其中所使用的代码和图片均以指引作用,会省略部分非关键代码。
初始渲染流程
入口
typescript
// 项目main.ts文件初始化逻辑
createApp(App).mount('#app')
// vue源码mount逻辑
export const createApp () {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = () => {
const proxy = mount(container, false, resolveRootNamespace(container)) // 开启生成vNode的入口方法
return proxy
}
return app
}
开始渲染
执行setup函数
下面提到的调用栈是以自定义vue组件也就是App进行调试的,当遇到其它类型的元素(如div等),其调用执行栈会有所不同
typescript
// main.ts中的 createApp(App).mount('#app')会按照以下调用执行栈处理
// mount -> render -> patch -> processComponent -> mountComponent -> setupComponent
function setupComponent() {
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 触发setup函数的执行-
: undefined
return setupResult
}
准备收集依赖所需的ReactiveEffect
typescript
// setupComponent执行之后,调用栈弹出到mountComponent,然后继续执行setupRenderEffect 方法
// 关键代码
const setupRenderEffect = (instance) => {
// ...其它逻辑
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn)) // 借助ReactiveEffect创建effect实体类
// ...其它逻辑
const update = (instance.update = effect.run.bind(effect)) // 创建等待执行的this指向正确的effect.run函数
const job = (instance.job = effect.runIfDirty.bind(effect))
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job) // 创建等待响应式状态发生时最终调用的任务
// ...其它逻辑
update() // 执行ReactiveEffect中的run方法
}
// ReactiveEffect类关键代码
class ReactiveEffect {
constructor(public fn: () => T) {
}
run () {
const prevEffect = activeSub
activeSub = this // 将当前activeSub更新为当前新生成的ReactiveEffect的实例化对象,方便后续解析template模板的时候将依赖正确收集起来
try {
return this.fn()
} finally {
activeSub = prevEffect
}
}
}
解析template模板,构建链式响应式触发
typescript
// 继续上一阶段的函数调用栈顺序开始
// ReactiveEffect.run -> this.fn(componentUpdateFn) -> renderComponentRoot
// renderComponentRoot 随即开启template的解析,并返回subTree(子元素),开启循环渲染的逻辑。

对于computed,ref,都会返回一个带有get和set访问器属性的对象。在模板解析过程中触发get方法,来将使用到的响应式状态收集起来(dep.track)。

get收集依赖的流程
typescript
// this.dep.track
class Dep {
track() {
let link = this.activeLink // 首次触发track,当前的dep.activeLink为undefined
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this) // 此处activeSub不做过多赘述,我们放在文章的末尾补充一下,这里可以简单理解为当前响应式被依赖者的上下文(我们简称父集)对象信息。 link主要作用就是将当前响应式状态的dep与父集做一个关联。
// ...其它逻辑
addSub(link)
} else {
// ...其它逻辑
}
}
}
function addSub(link) {
link.dep.subs = link // link.dep即为上面track中传入的第二个参数this
}
通过上述逻辑,将当前响应式对象的dep与父集关联起来了,举一个例子
typescript
<template>
<div>doubleCount is : {{ doubleCount }}</div>
<button @click="updateCount">点击增加功能</button>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
debugger
console.log("开始执行testComputed的值")
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function updateCount() {
count.value++;
}
</script>

如果看不懂可以先按照我下面的结论进行理解,当我点击button触发点击事件更新ref的响应式状态时。vue底层会通过 dep.subs.sub一层层向上层递归,最终回到组件-activeSub执行预先准备的重新渲染任务。
循环渲染
获取subTree,然后递归遍历渲染。注意这里这里与React存在明显的区别,React是通过链式方式(可以参考我的这篇文章)关联顺序,并通过全局变量记录当前的位置,方便重启-中断渲染过程。但是vue完全是通过循环加函数递归的方式实现,所以他们两个虽然都是深度优先策略,但是在渲染过程中还是存在很大的区别的

以下是我的App组件的实现,仅为上述图片图片提供更加直观的展示,可以跳过不看

更新渲染流程
当我们触发某一个响应式状态发生变化时,组件会进入更新的逻辑中,最终重新调用componentUpdateFn,执行产生新的VNode的过程,最后再将变化的部分更新。
还是以上面的例子进行分析
更新响应式数据,触发set方法
typescript
class RefImpl {
set value(newValue) {
// ...其它逻辑
this.dep.trigger()
}
}
class Dep {
notify() {
startBatch()
try {
for(let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
;(link.sub as ComputedRefImpl).dep.notify() // 仅有link.sub.notify()返回true的时候才会执行这里的逻辑
}
}
} finally {
endBatch() // 确定最终的顶层activeSub所执行的逻辑
}
}
trigger() {
// ...其它逻辑
this.notify()
}
}
!!!在notify方法中我们有看到subs,sub这两个变量,看到了这里如果比较疑惑可以回到get收集依赖的流程部分中的图进行辅助参考。subs保存的是link,而link中的sub保存着父集的activeSub。
至此开始一层层的向上循环,在到达组件-activeSub(ReactiveEffect实例化的对象)的时候notify逻辑如下
typescript
export function batch(sub) {
// ...其它逻辑
batchedSub = sub // 将顶层组件-activeSub赋值给全局变量batchedSub
}
class ReactiveEffect {
notify() {
if (
this.flags & EffectFlags.RUNNING &&
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
return
}
if (!(this.flags & EffectFlags.NOTIFIED)) {
batch(this) // 关键点
}
}
}
此时函数执行栈弹出到了endBatch()了
typescript
export function endBatch() {
// ...其它逻辑
while (batchedSub) {
let e: Subscriber | undefined = batchedSub
batchedSub = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
if (e.flags & EffectFlags.ACTIVE) {
try {
// ACTIVE flag is effect-only
;(e as ReactiveEffect).trigger() // 重新渲染template的执行函数
} catch (err) {
if (!error) error = err
}
}
e = next
}
}
}
class ReactiveEffect {
trigger() {
if (this.flags & EffectFlags.PAUSED) {
pausedQueueEffects.add(this)
} else if (this.scheduler) {
this.scheduler() // 以我举例的组件会执行到这里,这个scheduler的属性值即为准备收集依赖所需的ReactiveEffect的setupRenderEffect方法中生成的。
} else {
this.runIfDirty()
}
}
runIfDirty() {
if (isDirty(this)) {
this.run()
}
}
}
