第10章:Vue 内核深入
深入理解 Vue3 内部机制,掌握调度器、Diff 算法、Transition 系统等核心原理
📚 本章目标
通过本章学习,你将掌握:
- 调度器与批量更新机制
- Diff 算法原理与实现
- Transition 过渡系统
- 异步组件与 Suspense
- 自定义渲染器开发
⚡ 10.1 调度器与批量更新
10.1.1 调度器核心概念
Vue3 的调度器负责管理异步更新,确保在同一个事件循环中多次数据变更只触发一次重新渲染。
// 调度器核心实现
class Scheduler {
private queue: Function[] = []
private isFlushing = false
private isFlushPending = false
// 添加任务到队列
queueJob(job: Function) {
if (!this.queue.includes(job)) {
this.queue.push(job)
}
this.queueFlush()
}
// 刷新队列
private queueFlush() {
if (!this.isFlushing && !this.isFlushPending) {
this.isFlushPending = true
// 使用微任务执行
Promise.resolve().then(() => {
this.flushJobs()
})
}
}
// 执行队列中的任务
private flushJobs() {
this.isFlushPending = false
this.isFlushing = true
// 按 id 排序,确保父组件在子组件之前更新
this.queue.sort((a, b) => a.id - b.id)
try {
for (let i = 0; i < this.queue.length; i++) {
const job = this.queue[i]
job()
}
} finally {
this.queue.length = 0
this.isFlushing = false
}
}
}
const scheduler = new Scheduler()
10.1.2 nextTick 实现
// nextTick 实现
const callbacks: Function[] = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
function nextTick(cb?: Function): Promise<void> {
return new Promise((resolve) => {
callbacks.push(() => {
if (cb) cb()
resolve()
})
if (!pending) {
pending = true
// 优先使用微任务
if (typeof Promise !== 'undefined') {
Promise.resolve().then(flushCallbacks)
} else if (typeof MutationObserver !== 'undefined') {
const textNode = document.createTextNode('')
const observer = new MutationObserver(flushCallbacks)
observer.observe(textNode, { characterData: true })
textNode.data = '1'
} else {
setTimeout(flushCallbacks, 0)
}
}
})
}
10.1.3 批量更新示例
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="updateMultiple">批量更新</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const updateMultiple = async () => {
console.log('更新前:', count.value, double.value) // 0, 0
// 多次同步更新
count.value = 1
count.value = 2
count.value = 3
console.log('同步更新后:', count.value, double.value) // 3, 6
// 等待 DOM 更新
await nextTick()
console.log('DOM 更新后:', count.value, double.value) // 3, 6
}
</script>
🔄 10.2 Diff 算法详解
10.2.1 同层比较策略
Vue3 的 Diff 算法采用同层比较策略,避免跨层级的比较,提高性能。
// 简化的 Diff 算法实现
function diffChildren(
oldChildren: VNode[],
newChildren: VNode[],
container: Element
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newEndIdx = newChildren.length - 1
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 跳过已处理的节点
if (!oldStartVNode) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (!oldEndVNode) {
oldEndVNode = oldChildren[--oldEndIdx]
}
// 头头比较
else if (isSameVNode(oldStartVNode, newStartVNode)) {
patchVNode(oldStartVNode, newStartVNode, container)
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
}
// 尾尾比较
else if (isSameVNode(oldEndVNode, newEndVNode)) {
patchVNode(oldEndVNode, newEndVNode, container)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
}
// 头尾比较
else if (isSameVNode(oldStartVNode, newEndVNode)) {
patchVNode(oldStartVNode, newEndVNode, container)
insertBefore(container, oldStartVNode.el, oldEndVNode.el.nextSibling)
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
}
// 尾头比较
else if (isSameVNode(oldEndVNode, newStartVNode)) {
patchVNode(oldEndVNode, newStartVNode, container)
insertBefore(container, oldEndVNode.el, oldStartVNode.el)
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
}
// 都不匹配,查找新节点在旧节点中的位置
else {
const idxInOld = findIdxInOld(newStartVNode, oldChildren, oldStartIdx, oldEndIdx)
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
patchVNode(vnodeToMove, newStartVNode, container)
insertBefore(container, vnodeToMove.el, oldStartVNode.el)
oldChildren[idxInOld] = undefined
} else {
// 新节点,直接创建
mountElement(newStartVNode, container, oldStartVNode.el)
}
newStartVNode = newChildren[++newStartIdx]
}
}
// 处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 新节点有剩余,批量插入
for (let i = newStartIdx; i <= newEndIdx; i++) {
mountElement(newChildren[i], container, newEndVNode.el.nextSibling)
}
} else if (newStartIdx > newEndIdx) {
// 旧节点有剩余,批量删除
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldChildren[i]) {
unmount(oldChildren[i])
}
}
}
}
function isSameVNode(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
10.2.2 Key 的作用
Key 是 Diff 算法的重要优化手段,帮助 Vue 识别节点的身份。
<template>
<div>
<h3>没有 Key 的列表(性能较差)</h3>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
<input type="text" />
</li>
</ul>
<h3>有 Key 的列表(性能更好)</h3>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
<input type="text" />
</li>
</ul>
<button @click="addItem">添加项目</button>
<button @click="removeItem">删除项目</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' },
{ id: 3, name: '项目 3' }
])
const addItem = () => {
const newId = Math.max(...items.value.map(i => i.id)) + 1
items.value.push({ id: newId, name: `项目 ${newId}` })
}
const removeItem = () => {
if (items.value.length > 0) {
items.value.pop()
}
}
</script>
10.2.3 最长递增子序列优化
Vue3 使用最长递增子序列算法优化移动节点的性能。
// 最长递增子序列算法
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
🎭 10.3 Transition 过渡系统
10.3.1 过渡类名机制
Vue3 的 Transition 组件通过类名控制过渡效果。
<template>
<div>
<button @click="show = !show">切换</button>
<Transition name="fade">
<div v-if="show" class="box">内容</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
/* 过渡类名 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.box {
width: 100px;
height: 100px;
background: #42b883;
margin: 20px 0;
}
</style>
10.3.2 过渡钩子函数
<template>
<div>
<button @click="show = !show">切换</button>
<Transition
name="slide"
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div v-if="show" class="box">内容</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(false)
const onBeforeEnter = (el: Element) => {
console.log('进入前')
el.setAttribute('data-old-overflow', (el as HTMLElement).style.overflow)
;(el as HTMLElement).style.overflow = 'hidden'
}
const onEnter = (el: Element, done: () => void) => {
console.log('进入中')
const el = el as HTMLElement
el.style.height = '0px'
el.offsetHeight // 触发重排
el.style.transition = 'height 0.3s ease'
el.style.height = '100px'
el.addEventListener('transitionend', done, { once: true })
}
const onAfterEnter = (el: Element) => {
console.log('进入后')
const el = el as HTMLElement
el.style.height = 'auto'
el.style.overflow = el.getAttribute('data-old-overflow') || ''
}
const onBeforeLeave = (el: Element) => {
console.log('离开前')
const el = el as HTMLElement
el.style.height = el.offsetHeight + 'px'
el.offsetHeight // 触发重排
}
const onLeave = (el: Element, done: () => void) => {
console.log('离开中')
const el = el as HTMLElement
el.style.transition = 'height 0.3s ease'
el.style.height = '0px'
el.addEventListener('transitionend', done, { once: true })
}
const onAfterLeave = (el: Element) => {
console.log('离开后')
}
</script>
<style scoped>
.slide-enter-active,
.slide-leave-active {
transition: height 0.3s ease;
}
.box {
width: 100px;
background: #42b883;
margin: 20px 0;
overflow: hidden;
}
</style>
10.3.3 列表过渡
<template>
<div>
<button @click="addItem">添加项目</button>
<button @click="removeItem">删除项目</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.name }}
<button @click="removeItemById(item.id)">删除</button>
</li>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' },
{ id: 3, name: '项目 3' }
])
let nextId = 4
const addItem = () => {
items.value.push({ id: nextId++, name: `项目 ${nextId - 1}` })
}
const removeItem = () => {
if (items.value.length > 0) {
items.value.pop()
}
}
const removeItemById = (id: number) => {
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value.splice(index, 1)
}
}
</script>
<style scoped>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease;
}
.list-item {
padding: 10px;
margin: 5px 0;
background: #f0f0f0;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
🔄 10.4 异步组件与 Suspense
10.4.1 异步组件定义
// 基础异步组件
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
// 高级异步组件
const AdvancedAsyncComponent = defineAsyncComponent({
loader: () => import('./AdvancedComponent.vue'),
loadingComponent: {
template: '<div class="loading">加载中...</div>'
},
errorComponent: {
template: '<div class="error">加载失败</div>'
},
delay: 200,
timeout: 3000,
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
retry()
} else {
fail()
}
}
})
10.4.2 Suspense 使用
<template>
<div>
<h1>异步组件示例</h1>
<Suspense>
<template #default>
<AsyncDataComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// 异步数据组件
const AsyncDataComponent = defineAsyncComponent(async () => {
// 模拟异步数据获取
const data = await fetchData()
return {
template: `
<div>
<h2>数据加载完成</h2>
<p>数据: {{ data }}</p>
</div>
`,
setup() {
return { data }
}
}
})
async function fetchData() {
await new Promise(resolve => setTimeout(resolve, 2000))
return '这是异步获取的数据'
}
</script>
<style scoped>
.loading {
text-align: center;
padding: 20px;
color: #666;
}
</style>
10.4.3 异步组件最佳实践
<template>
<div>
<Suspense>
<template #default>
<LazyChart :data="chartData" />
</template>
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const chartData = ref(null)
// 懒加载图表组件
const LazyChart = defineAsyncComponent({
loader: () => import('@/components/Chart.vue'),
loadingComponent: {
template: '<div class="chart-loading">图表加载中...</div>'
},
errorComponent: {
template: '<div class="chart-error">图表加载失败</div>'
},
delay: 100,
timeout: 5000
})
// 骨架屏组件
const ChartSkeleton = {
template: `
<div class="chart-skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-chart"></div>
<div class="skeleton-legend"></div>
</div>
`
}
onMounted(async () => {
// 模拟数据获取
await new Promise(resolve => setTimeout(resolve, 1000))
chartData.value = {
labels: ['一月', '二月', '三月', '四月', '五月'],
datasets: [{
label: '销售额',
data: [12, 19, 3, 5, 2]
}]
}
})
</script>
<style scoped>
.chart-skeleton {
padding: 20px;
}
.skeleton-header {
height: 20px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 20px;
}
.skeleton-chart {
height: 300px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 20px;
}
.skeleton-legend {
height: 40px;
background: #f0f0f0;
border-radius: 4px;
}
</style>
🎨 10.5 自定义渲染器
10.5.1 基础自定义渲染器
// 自定义渲染器实现
import { createRenderer } from '@vue/runtime-core'
const { createApp } = createRenderer({
// 创建元素
createElement(type, isSVG, isCustomizedBuiltIn) {
const el = document.createElement(type)
return el
},
// 设置元素文本内容
setElementText(el, text) {
el.textContent = text
},
// 插入元素
insert(el, parent, anchor) {
parent.insertBefore(el, anchor || null)
},
// 移除元素
remove(el) {
const parent = el.parentNode
if (parent) {
parent.removeChild(el)
}
},
// 设置元素属性
patchProp(el, key, prevValue, nextValue) {
if (key.startsWith('on')) {
// 事件处理
const eventName = key.slice(2).toLowerCase()
if (prevValue) {
el.removeEventListener(eventName, prevValue)
}
if (nextValue) {
el.addEventListener(eventName, nextValue)
}
} else {
// 属性设置
if (nextValue === null || nextValue === undefined) {
el.removeAttribute(key)
} else {
el.setAttribute(key, nextValue)
}
}
},
// 创建文本节点
createText(text) {
return document.createTextNode(text)
},
// 设置文本内容
setText(node, text) {
node.nodeValue = text
},
// 获取父节点
parentNode(node) {
return node.parentNode
},
// 获取下一个兄弟节点
nextSibling(node) {
return node.nextSibling
}
})
// 使用自定义渲染器
const app = createApp({
template: `
<div>
<h1>自定义渲染器示例</h1>
<button @click="count++">点击次数: {{ count }}</button>
</div>
`,
setup() {
const count = ref(0)
return { count }
}
})
app.mount('#app')
10.5.2 Canvas 渲染器
// Canvas 渲染器
import { createRenderer } from '@vue/runtime-core'
interface CanvasElement {
type: string
x: number
y: number
width: number
height: number
color: string
children?: CanvasElement[]
}
const { createApp } = createRenderer({
createElement(type, isSVG, isCustomizedBuiltIn) {
return {
type,
x: 0,
y: 0,
width: 0,
height: 0,
color: '#000000',
children: []
}
},
setElementText(el, text) {
el.text = text
},
insert(el, parent, anchor) {
if (parent.children) {
parent.children.push(el)
}
},
remove(el) {
// Canvas 中不需要特殊处理
},
patchProp(el, key, prevValue, nextValue) {
if (key === 'x') el.x = nextValue
else if (key === 'y') el.y = nextValue
else if (key === 'width') el.width = nextValue
else if (key === 'height') el.height = nextValue
else if (key === 'color') el.color = nextValue
},
createText(text) {
return { type: 'text', text, x: 0, y: 0, color: '#000000' }
},
setText(node, text) {
node.text = text
},
parentNode(node) {
return node.parent
},
nextSibling(node) {
return node.nextSibling
}
})
// Canvas 渲染函数
function renderToCanvas(canvas: HTMLCanvasElement, rootElement: CanvasElement) {
const ctx = canvas.getContext('2d')!
function renderElement(element: CanvasElement) {
if (element.type === 'rect') {
ctx.fillStyle = element.color
ctx.fillRect(element.x, element.y, element.width, element.height)
} else if (element.type === 'text') {
ctx.fillStyle = element.color
ctx.fillText(element.text, element.x, element.y)
}
if (element.children) {
element.children.forEach(renderElement)
}
}
renderElement(rootElement)
}
// 使用 Canvas 渲染器
const app = createApp({
template: `
<rect x="10" y="10" width="100" height="50" color="red" />
<text x="20" y="30" color="white">Hello Canvas!</text>
`
})
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const rootElement = app.mount(canvas)
renderToCanvas(canvas, rootElement)
10.5.3 服务端渲染器
// 服务端渲染器
import { createRenderer } from '@vue/runtime-core'
const { renderToString } = createRenderer({
createElement(type, isSVG, isCustomizedBuiltIn) {
return {
type,
props: {},
children: []
}
},
setElementText(el, text) {
el.text = text
},
insert(el, parent, anchor) {
if (parent.children) {
parent.children.push(el)
}
},
remove(el) {
// 服务端不需要处理
},
patchProp(el, key, prevValue, nextValue) {
el.props[key] = nextValue
},
createText(text) {
return { type: 'text', text }
},
setText(node, text) {
node.text = text
},
parentNode(node) {
return node.parent
},
nextSibling(node) {
return node.nextSibling
}
})
// 将虚拟 DOM 转换为 HTML 字符串
function vnodeToHTML(vnode: any): string {
if (vnode.type === 'text') {
return vnode.text
}
const tag = vnode.type
const props = vnode.props || {}
const children = vnode.children || []
const propsStr = Object.entries(props)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')
const childrenStr = children.map(vnodeToHTML).join('')
return `<${tag}${propsStr ? ' ' + propsStr : ''}>${childrenStr}</${tag}>`
}
// 使用服务端渲染器
const app = createApp({
template: `
<div>
<h1>服务端渲染</h1>
<p>这是服务端渲染的内容</p>
</div>
`
})
const html = renderToString(app)
console.log(vnodeToHTML(html))
🎯 本章总结
通过本章学习,你掌握了:
- 调度器机制:批量更新、nextTick 实现
- Diff 算法:同层比较、Key 优化、最长递增子序列
- Transition 系统:类名机制、钩子函数、列表过渡
- 异步组件:defineAsyncComponent、Suspense 使用
- 自定义渲染器:Canvas 渲染、服务端渲染
📝 练习题
- 实现一个简化版的调度器,支持任务队列和批量更新
- 编写一个 Diff 算法的可视化演示工具
- 创建一个复杂的 Transition 动画组件
- 实现一个基于 Canvas 的图表组件
- 开发一个支持服务端渲染的简单应用