【Vue 渲染流程揭秘】

渲染流程

前言

本文章以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()
		}
	}
}
相关推荐
孜燃8 小时前
Flutter APP跳转Flutter APP 携带参数
前端·flutter
脾气有点小暴8 小时前
前端页面跳转的核心区别与实战指南
开发语言·前端·javascript
lxh01138 小时前
最长递增子序列
前端·数据结构·算法
vipbic8 小时前
我封装了一个“瑞士军刀”级插件,并顺手搞定了自动化部署
vue.js·nuxt.js
Youyzq9 小时前
前端项目发布到cdn上css被编译失效问题rgba失效和rgb失效
前端·css·算法·cdn
San30.9 小时前
深入 JavaScript 内存机制:从栈与堆到闭包的底层原理
开发语言·javascript·udp
凯小默9 小时前
30-更新用户信息并且刷新表格
vue3
Fantastic_sj9 小时前
Vue3相比Vue2的改进之处
前端·javascript·vue.js
vipbic10 小时前
解决npm publish的404/403和配置警告全记录
前端·npm·node.js
Bigger10 小时前
🚀 “踩坑日记”:shadcn + Vite 在 Monorepo 中配置报错
前端·react.js·vite