Vue3 源码详解
1. 代理(Proxy)
Vue3 的响应式系统建立在 ES6 的 Proxy 基础上,彻底替代了 Vue2 中基于 Object.defineProperty 的响应式实现。让我们深入分析 Vue3 中的响应式实现源码。
核心实现:reactive 函数
首先,让我们看一下 Vue3 中 reactive 函数的简化版实现:
// packages/reactivity/src/reactive.ts
// 用于存储原始对象到代理对象的映射
export const reactiveMap = new WeakMap<Target, any>()
// reactive函数接收一个对象,返回该对象的代理
export function reactive(target: object) {
// 如果尝试将一个已经是代理的对象再次代理,直接返回
if (isProxy(target)) {
return target
}
// 如果目标已经有对应的代理对象,则直接返回已存在的代理
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只允许代理对象或数组
if (!isObject(target)) {
if (__DEV__) {
console.warn(`reactive() 只能用于对象或数组,而不是 ${typeof target}`)
}
return target
}
// 创建代理
const proxy = new Proxy(
target,
mutableHandlers // 这个函数包含各种代理处理程序
)
// 缓存原始对象和代理对象的映射关系
reactiveMap.set(target, proxy)
return proxy
}
每一行的说明:
export const reactiveMap = new WeakMap<Target, any>()- 创建一个 WeakMap 存储原始对象到其代理对象的映射,使用 WeakMap 可以避免内存泄漏。export function reactive(target: object)- 导出 reactive 函数,接收一个目标对象作为参数。if (isProxy(target)) return target- 如果目标已经是响应式对象(代理),则直接返回,避免重复代理。const existingProxy = reactiveMap.get(target)- 检查是否已经为该目标对象创建过代理。if (existingProxy) return existingProxy- 如果已存在代理,直接返回已存在的代理,确保同一个原始对象只有一个代理实例。if (!isObject(target)) {...}- 确保目标是对象或数组,如果不是,在开发模式下发出警告并返回原始值。const proxy = new Proxy(target, mutableHandlers)- 核心代码,使用 ES6 的 Proxy 创建代理对象。reactiveMap.set(target, proxy)- 缓存原始对象和代理的映射,便于后续检索。return proxy- 返回创建的代理对象。
mutableHandlers 详解
接下来,让我们看看 mutableHandlers 的实现,这是 Proxy 的处理程序,定义了拦截操作:
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get, // 拦截对象属性的读取操作
set, // 拦截对象属性的设置操作
deleteProperty, // 拦截删除对象属性的操作
has, // 拦截 in 操作符
ownKeys // 拦截 Object.keys, Object.getOwnPropertyNames 等
}
// get 处理程序实现
function get(target: Target, key: string | symbol, receiver: object) {
// 检查是否是特殊的内部属性
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
// 获取原始值
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, TrackOpTypes.GET, key)
// 如果读取的值是对象,则返回该对象的代理
if (isObject(res)) {
return reactive(res)
}
return res
}
// set 处理程序实现
function set(
target: Target,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取旧值
const oldValue = (target as any)[key]
// 设置新值
const result = Reflect.set(target, key, value, receiver)
// 如果值发生变化,触发响应
if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
// deleteProperty 处理程序实现
function deleteProperty(target: Target, key: string | symbol): boolean {
// 检查属性是否存在且是否是目标对象自身的属性
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
// 执行删除操作
const result = Reflect.deleteProperty(target, key)
// 如果删除成功且属性确实存在,触发响应
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
// has 处理程序实现
function has(target: Target, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// 依赖收集
track(target, TrackOpTypes.HAS, key)
return result
}
// ownKeys 处理程序实现
function ownKeys(target: Target): (string | symbol)[] {
// 依赖收集,使用特殊的 ITERATE_KEY 作为键
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
每个处理程序的详细分析:
get 处理程序
if (key === ReactiveFlags.IS_REACTIVE) return true- 检查是否请求特殊标志,用于判断对象是否是响应式的。const res = Reflect.get(target, key, receiver)- 使用 Reflect.get 获取原始属性值。track(target, TrackOpTypes.GET, key)- 执行依赖收集,记录当前副作用(effect)依赖于这个属性。if (isObject(res)) return reactive(res)- 如果获取的值是对象,则递归对其进行代理(深度响应性)。return res- 返回最终值。
set 处理程序
const oldValue = (target as any)[key]- 获取旧值,用于后续比较。const result = Reflect.set(target, key, value, receiver)- 使用 Reflect.set 设置新值。if (hasChanged(value, oldValue))- 检查值是否实际发生变化。trigger(target, TriggerOpTypes.SET, key, value, oldValue)- 如果值变化,触发响应更新。return result- 返回设置操作的结果(通常为 true)。
deleteProperty 处理程序
const hadKey = hasOwn(target, key)- 检查属性是否存在于目标对象上。const oldValue = (target as any)[key]- 获取旧值,用于在触发更新时提供。const result = Reflect.deleteProperty(target, key)- 执行删除操作。if (result && hadKey)- 如果删除成功且属性确实存在。trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)- 触发响应更新。return result- 返回删除操作的结果。
has 处理程序
const result = Reflect.has(target, key)- 使用 Reflect.has 检查属性是否在目标对象上。track(target, TrackOpTypes.HAS, key)- 执行依赖收集,这样当属性添加或删除时,使用 in 操作符的代码可以响应。return result- 返回检查结果。
ownKeys 处理程序
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)- 对迭代操作收集依赖。return Reflect.ownKeys(target)- 返回目标对象的所有自有属性键。
依赖收集与触发更新
Vue3 响应式系统的核心在于依赖收集(track)和触发更新(trigger):
// packages/reactivity/src/effect.ts
// 活动的 effect
let activeEffect: ReactiveEffect | undefined
// 依赖收集函数
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果没有活动的 effect,直接返回
if (!activeEffect) return
// 获取目标对象的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取特定键的依赖集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 将当前活动 effect 添加到依赖集合
trackEffects(dep)
}
// 将 effect 添加到依赖集合
export function trackEffects(dep: Dep) {
// 如果依赖已经被收集,则跳过
if (!dep.has(activeEffect!)) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
// 触发更新函数
export function trigger(
target: object,
type: TriggerOpTypes,
key: unknown,
newValue?: unknown,
oldValue?: unknown
) {
// 获取目标对象的依赖映射
const depsMap = targetMap.get(target)
if (!depsMap) return // 没有依赖,直接返回
// 要触发的 effect 集合
const effects = new Set<ReactiveEffect>()
// 添加相关依赖到 effects 集合
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// 1. 处理特定 key 的依赖
if (key !== void 0) {
add(depsMap.get(key))
}
// 2. 处理数组长度变化
if (type === TriggerOpTypes.ADD && isArray(target)) {
add(depsMap.get('length'))
}
// 3. 处理对象属性添加/删除
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
add(depsMap.get(ITERATE_KEY))
}
// 触发收集的 effects
effects.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
})
}
依赖收集(track)分析:
if (!activeEffect) return- 如果没有活动的 effect(例如,不在 computed 或 watch 中),则不需要收集依赖。let depsMap = targetMap.get(target)- 获取与目标对象关联的依赖映射。if (!depsMap) targetMap.set(target, (depsMap = new Map()))- 如果依赖映射不存在,则创建一个新的。let dep = depsMap.get(key)- 获取与特定键关联的依赖集合。if (!dep) depsMap.set(key, (dep = createDep()))- 如果依赖集合不存在,则创建一个新的。trackEffects(dep)- 将当前活动的 effect 添加到依赖集合。
触发更新(trigger)分析:
const depsMap = targetMap.get(target)- 获取目标对象的依赖映射。if (!depsMap) return- 如果没有依赖映射,说明该对象没有被观察,直接返回。const effects = new Set<ReactiveEffect>()- 创建一个 Set 来存储需要触发的 effect。const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {...}- 辅助函数,用于将 effect 添加到集合。if (key !== void 0) add(depsMap.get(key))- 添加特定键的依赖。- 处理数组长度变化和对象迭代相关的依赖。
effects.forEach(effect => {...})- 触发所有收集的 effect。
2. 虚拟DOM(Virtual DOM)
Vue3 的虚拟 DOM 系统是其渲染机制的核心。虚拟 DOM 本质上是真实 DOM 的 JavaScript 对象表示。
VNode 数据结构
首先,让我们看看 VNode(虚拟节点)的数据结构定义:
// packages/runtime-core/src/vnode.ts
export interface VNode {
__v_isVNode: true // 标识这是一个 VNode
type: VNodeTypes // 节点类型(组件、HTML 标签等)
props: VNodeProps | null // 节点属性
key: string | number | null // 节点的 key
ref: VNodeRef | null // 节点的 ref
children: VNodeNormalizedChildren // 子节点
component: ComponentInternalInstance | null // 组件实例(只对组件节点有效)
el: HostNode | null // 对应的真实 DOM 节点
anchor: HostNode | null // 用于 Fragment 的锚点节点
target: HostElement | null // Teleport 目标
targetAnchor: HostNode | null // Teleport 目标锚点
// 其他属性...
}
// createVNode 函数
export function createVNode(
type: VNodeTypes,
props: VNodeProps | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps = null,
isBlockNode = false
): VNode {
// class 和 style 标准化
if (props) {
// 对 class 进行标准化处理
if ('class' in props) {
props.class = normalizeClass(props.class)
}
// 对 style 进行标准化处理
if ('style' in props) {
props.style = normalizeStyle(props.style)
}
}
// 确定节点标志(判断节点类型)
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // HTML 标签
: isObject(type)
? ShapeFlags.COMPONENT // 组件
: 0
// 创建 VNode 对象
const vnode: VNode = {
__v_isVNode: true,
type,
props,
key: props?.key ?? null,
ref: props?.ref ?? null,
children: null,
component: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
shapeFlag,
patchFlag,
dynamicProps,
// 其他属性初始化...
}
// 标准化子节点
normalizeChildren(vnode, children)
return vnode
}
// 标准化子节点
function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
if (children == null) {
children = null
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
// 单个 VNode 或者插槽
if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
// 将单个 VNode 转换为数组
type = ShapeFlags.ARRAY_CHILDREN
children = [children as VNode]
} else {
// 插槽
type = ShapeFlags.SLOTS_CHILDREN
}
} else if (isFunction(children)) {
// 函数子节点(渲染函数)
type = ShapeFlags.SLOTS_CHILDREN
children = { default: children }
} else {
// 字符串或数字,转换为文本节点
children = String(children)
type = ShapeFlags.TEXT_CHILDREN
}
vnode.children = children
vnode.shapeFlag |= type // 使用位运算合并节点类型标志
}
VNode 和 createVNode 函数的分析:
export interface VNode {...}- 定义 VNode 接口,表示虚拟 DOM 节点的属性。__v_isVNode: true- 标识这是一个 VNode 对象。type: VNodeTypes- 节点类型,可以是字符串(HTML 标签名)、组件对象或特殊的符号(Fragment, Teleport 等)。props: VNodeProps | null- 节点的属性,如 id, class, style 等。key: string | number | null- 节点的唯一标识,用于优化 diff 算法。children: VNodeNormalizedChildren- 子节点,可以是文本、VNode 数组或插槽对象。el: HostNode | null- 对应的真实 DOM 节点,初始为 null,在挂载时赋值。
createVNode 函数的关键步骤:
if (props) {...}- 标准化 class 和 style 属性。const shapeFlag = ...- 确定节点的类型标志。const vnode: VNode = {...}- 创建 VNode 对象,初始化各种属性。normalizeChildren(vnode, children)- 标准化子节点。return vnode- 返回创建的 VNode。
子节点标准化(normalizeChildren)的关键步骤:
- 根据子节点的类型(数组、对象、函数或原始值)确定 shapeFlag。
- 对不同类型的子节点进行适当的转换(如将单个 VNode 转换为数组)。
- 设置 vnode.children 和 vnode.shapeFlag。
渲染流程
Vue3 的渲染流程涉及创建 VNode 树、挂载和更新:
// packages/runtime-core/src/renderer.ts
// 创建渲染器
export function createRenderer(options: RendererOptions) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
// 其他平台相关的函数...
} = options
// 挂载组件
const mountComponent = (
initialVNode: VNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 设置组件实例
setupComponent(instance)
// 设置并运行渲染效果
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
// 设置渲染效果
const setupRenderEffect = (
instance: ComponentInternalInstance,
initialVNode: VNode,
container: HostElement,
anchor: HostNode | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
// 创建响应式效果
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 初始渲染
const subTree = (instance.subTree = renderComponentRoot(instance))
// 挂载子树
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
instance.isMounted = true
} else {
// 更新
const prevTree = instance.subTree
const nextTree = renderComponentRoot(instance)
instance.subTree = nextTree
// 更新子树
patch(
prevTree,
nextTree,
hostParentNode(prevTree.el!)!,
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
initialVNode.el = nextTree.el
}
}, prodEffectOptions)
}
// patch 函数 - 核心的 DOM 操作函数
const patch = (
n1: VNode | null, // 旧 VNode
n2: VNode, // 新 VNode
container: HostElement,
anchor: HostNode | null = null,
parentComponent: ComponentInternalInstance | null = null,
parentSuspense: SuspenseBoundary | null = null,
isSVG: boolean = false,
optimized: boolean = false
) => {
// 如果新旧节点类型不同,卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 根据节点类型执行不同的处理
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
processText(n1, n2, container, anchor)
break
case Comment:
// 处理注释节点
processCommentNode(n1, n2, container, anchor)
break
case Fragment:
// 处理 Fragment
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 Teleport
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals)
} // ... 其他类型的处理
}
}
// 处理元素
const processElement = (
n1: VNode | null,
n2: VNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 挂载元素
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
// 更新元素
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
// 挂载元素
const mountElement = (
vnode: VNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: HostElement
const { type, props, shapeFlag } = vnode
// 创建元素
el = vnode.el = hostCreateElement(
type as string,
isSVG,
props?.is,
props
)
// 设置文本内容
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 挂载子节点数组
mountChildren(
vnode.children as VNode[],
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
optimized || !!vnode.dynamicChildren
)
}
// 设置属性
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
// 触发 'beforeMount' 钩子
onBeforeMount(vnode, parentComponent)
// 插入到 DOM
hostInsert(el, container, anchor)
// 触发 'mounted' 钩子
queuePostRenderEffect(
() => onMounted(vnode, parentComponent),
parentSuspense
)
}
// 返回渲染器 API
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
渲染流程分析:
-
createRenderer函数创建一个渲染器,接收平台相关的 DOM 操作函数。 -
mountComponent函数挂载组件:- 创建组件实例
- 设置组件(处理 props, slots 等)
- 设置渲染效果
-
setupRenderEffect函数设置组件的渲染效果:- 创建一个响应式效果,当依赖变化时自动重新渲染
- 处理初始渲染和后续更新
- 调用 patch 函数进行 DOM 操作
-
patch函数是核心的 DOM 操作函数:- 比较新旧 VNode
- 根据节点类型(文本、元素、组件等)分发给不同的处理函数
- 处理 VNode 的挂载和更新
-
processElement函数处理普通 DOM 元素:- 如果没有旧节点,调用 mountElement 挂载新元素
- 如果有旧节点,调用 patchElement 更新元素
-
mountElement函数挂载一个元素:- 创建 DOM
// packages/runtime-core/src/renderer.ts
const mountElement = (
vnode: VNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: HostElement
const { type, props, shapeFlag, transition, dirs } = vnode
// 创建元素
el = vnode.el = hostCreateElement(
type as string,
isSVG,
props?.is,
props
)
// 设置元素内容
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果子节点是文本,直接设置文本内容
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 如果子节点是数组,递归挂载子节点
mountChildren(
vnode.children as VNode[],
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
optimized
)
}
// 处理指令
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 处理 scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
// 设置元素属性
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 如果有 onVnodeBeforeMount 钩子,执行它
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// 指令 beforeMount 钩子
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// 插入到 DOM
hostInsert(el, container, anchor)
// 处理挂载后逻辑
const { transition } = vnode
if (shapeFlag & ShapeFlags.COMPONENT && transition && !transition.persisted) {
transition.beforeEnter(el)
}
// onMounted 钩子
if ((vnodeHook = props && props.onVnodeMounted) || (shapeFlag & ShapeFlags.COMPONENT && transition) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
transition && !transition.persisted && transition.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
详细解析每一行:
const { type, props, shapeFlag, transition, dirs } = vnode- 从 vnode 解构出需要的属性。el = vnode.el = hostCreateElement(...)- 创建真实 DOM 元素并保存到 vnode.el。if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {...}- 如果子节点是文本,直接设置元素的文本内容。else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {...}- 如果子节点是数组,调用 mountChildren 递归挂载。if (dirs) {...}- 如果有指令,调用指令的 created 钩子。setScopeId(...)- 设置 scoped CSS 的 ID,用于样式隔离。if (props) {...}- 遍历 props 设置元素属性,忽略保留的属性。if ((vnodeHook = props.onVnodeBeforeMount)) {...}- 如果有 onVnodeBeforeMount 钩子,执行它。if (dirs) {...}- 执行指令的 beforeMount 钩子。hostInsert(el, container, anchor)- 将元素插入到 DOM 中。if (shapeFlag & ShapeFlags.COMPONENT && transition && !transition.persisted) {...}- 处理组件的过渡效果。if ((vnodeHook = props && props.onVnodeMounted) || ...)- 将 onVnodeMounted 钩子等加入渲染后队列。
patchElement 函数详解
当更新一个已存在的元素时,Vue 使用 patchElement 函数:
// packages/runtime-core/src/renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 处理指令
const dirs = n2.dirs
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// 如果有 onVnodeBeforeUpdate 钩子,执行它
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
// 更新子节点
if (n2.dynamicChildren) {
// 如果有动态子节点(编译器优化),只更新动态子节点
patchBlockChildren(
n1.dynamicChildren!,
n2.dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
optimized
)
} else if (!optimized) {
// 全量 diff 更新子节点
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG
)
}
// 更新属性
if (oldProps !== newProps) {
for (const key in newProps) {
// 忽略保留属性
if (!isReservedProp(key) && newProps[key] !== undefined) {
hostPatchProp(
el,
key,
oldProps[key],
newProps[key],
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 删除不再存在的属性
if (oldProps !== EMPTY_OBJ) {
for (const key in oldProps) {
if (!isReservedProp(key) && !(key in newProps)) {
hostPatchProp(
el,
key,
oldProps[key],
null,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// 处理更新后逻辑
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
patchElement 函数的详细解析:
const el = (n2.el = n1.el!)- 重用旧元素的 DOM 节点。const oldProps = n1.props || EMPTY_OBJ- 获取旧属性或使用空对象。const newProps = n2.props || EMPTY_OBJ- 获取新属性或使用空对象。if (dirs) {...}- 如果有指令,调用指令的 beforeUpdate 钩子。if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {...}- 如果有 onVnodeBeforeUpdate 钩子,执行它。if (n2.dynamicChildren) {...} else if (!optimized) {...}- 更新子节点,优先使用优化路径。if (oldProps !== newProps) {...}- 如果属性发生变化,更新属性。for (const key in newProps) {...}- 设置新属性或更新变化的属性。if (oldProps !== EMPTY_OBJ) {...}- 删除不再存在的属性。if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {...}- 将 onVnodeUpdated 钩子等加入渲染后队列。
mountChildren 和 patchChildren
处理子节点的两个关键函数:
// packages/runtime-core/src/renderer.ts
// 挂载子节点
const mountChildren = (
children: VNode[],
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean,
slotScopeIds: string[] | null = null,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = normalizeVNode(children[i])
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
slotScopeIds
)
}
}
// 更新子节点
const patchChildren = (
n1: VNode | null,
n2: VNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null = null,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { shapeFlag } = n2
// 子节点更新的四种情况
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 1. 新子节点是文本
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧子节点是数组,卸载旧子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
// 设置文本内容
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 2. 旧子节点是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新子节点也是数组,执行 diff 算法
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 新子节点不是数组,卸载旧子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 3. 旧子节点是文本或不存在
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 旧子节点是文本,清空文本
hostSetElementText(container, '')
}
// 4. 新子节点是数组,挂载新子节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
mountChildren 和 patchChildren 的详细解析:
-
mountChildren函数遍历子节点数组,对每个子节点调用 patch 函数,传入 null 作为旧节点(表示挂载)。 -
patchChildren函数根据新旧子节点的类型(文本或数组)进行不同的处理:- 新子节点是文本:
- 如果旧子节点是数组,卸载旧子节点
- 设置容器的文本内容
- 新子节点是数组:
- 如果旧子节点也是数组,执行 diff 算法
- 如果旧子节点是文本,清空文本并挂载新子节点
- 如果旧子节点不存在,直接挂载新子节点
- 新子节点是文本:
3. Diff 算法
Vue3 的 diff 算法主要实现在 patchKeyedChildren 函数中,这是 Vue3 中最复杂也是最核心的部分之一:
// packages/runtime-core/src/renderer.ts
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: HostElement,
parentAnchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // 旧子节点的结束索引
let e2 = l2 - 1 // 新子节点的结束索引
// 1. 从头开始同步
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = normalizeVNode(c2[i])
if (isSameVNodeType(n1, n2)) {
// 如果节点类型相同,递归更新
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 遇到不同的节点,跳出循环
break
}
i++
}
// 2. 从尾开始同步
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = normalizeVNode(c2[e2])
if (isSameVNodeType(n1, n2)) {
// 如果节点类型相同,递归更新
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 遇到不同的节点,跳出循环
break
}
e1--
e2--
}
// 3. 处理公共序列外的节点
if (i > e1) {
// 旧子节点已处理完,但新子节点还有剩余
// 挂载剩余的新子节点
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null,
normalizeVNode(c2[i]),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
} else if (i > e2) {
// 新子节点已处理完,但旧子节点还有剩余
// 卸载剩余的旧子节点
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
} else {
// 4. 处理中间部分未匹配的节点
const s1 = i // 旧子节点开始索引
const s2 = i // 新子节点开始索引
// 4.1 建立新子节点的 key 到索引的映射
const keyToNewIndexMap = new Map<string | number | symbol, number>()
for (i = s2; i <= e2; i++) {
const nextChild = normalizeVNode(c2[i])
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 4.2 更新和移除
let j
let patched = 0 // 已处理的新子节点数量
const toBePatched = e2 - s2 + 1 // 需要处理的新子节点总数
let moved = false // 是否需要移动节点
let maxNewIndexSoFar = 0 // 最大新索引
// 用于存储新子节点在旧子节点中的位置
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
// 遍历旧子节点,在新子节点中找到匹配项并更新
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
// 如果已处理的新子节点数量等于总数,直接卸载剩余的旧子节点
if (patched >= toBePatched) {
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 如果旧子节点有 key,从映射中找到匹配的新子节点
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 如果旧子节点没有 key,在未处理的新子节点中遍历查找匹配的节点
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
// 如果未找到匹配的新子节点,卸载旧子节点
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 标记新子节点对应的旧子节点索引
newIndexToOldIndexMap[newIndex - s2] = i + 1
// 检查是否需要移动节点
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新匹配的节点
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 4.3 移动和挂载
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 倒序处理以便正确移动节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 挂载新子节点
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// 移动节点
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
Vue3 diff 算法的详细分析:
1. 从头开始同步
// 1. 从头开始同步
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = normalizeVNode(c2[i])
if (isSameVNodeType(n1, n2)) {
// 如果节点类型相同,递归更新
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else {
// 遇到不同的节点,跳出循环
break
}
i++
}
这一步从头开始比较新旧子节点列表,直到遇到不同类型的节点。对于类型相同的节点,调用 patch 函数递归更新内容。
2. 从尾开始同步
// 2. 从尾开始同步
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = normalizeVNode(c2[e2])
if (isSameVNodeType(n1, n2)) {
// 如果节点类型相同,递归更新
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else {
// 遇到不同的节点,跳出循环
break
}
e1--
e2--
}
这一步从尾开始比较新旧子节点列表,直到遇到不同类型的节点。同样,对于类型相同的节点,调用 patch 函数递归更新内容。
3. 处理公共序列外的节点
// 3. 处理公共序列外的节点
if (i > e1) {
// 旧子节点已处理完,但新子节点还有剩余,挂载剩余的新子节点
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(null, normalizeVNode(c2[i]), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
i++
}
}
} else if (i > e2) {
// 新子节点已处理完,但旧子节点还有剩余,卸载剩余的旧子节点
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
这一步处理两种情况:
- 如果新子节点比旧子节点多,挂载剩余的新子节点。
- 如果旧子节点比新子节点多,卸载剩余的旧子节点。
4. 处理中间部分未匹配的节点
else {
// 4. 处理中间部分未匹配的节点
const s1 = i // 旧子节点开始索引
const s2 = i // 新子节点开始索引
// 4.1 建立新子节点的 key 到索引的映射
const keyToNewIndexMap = new Map<string | number | symbol, number>()
for (i = s2; i <= e2; i++) {
const nextChild = normalizeVNode(c2[i])
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 4.2 更新和移除
let j
let patched = 0 // 已处理的新子节点数量
const toBePatched = e2 - s2 + 1 // 需要处理的新子节点总数
let moved = false // 是否需要移动节点
let maxNewIndexSoFar = 0 // 最大新索引
// 用于存储新子节点在旧子节点中的位置
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
// 遍历旧子节点
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// 如果已处理的新子节点数量等于总数,直接卸载剩余的旧子节点
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 如果旧子节点有 key,从映射中找到匹配的新子节点
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 如果旧子节点没有 key,在未处理的新子节点中遍历查找匹配的节点
for (j = s2; j <= e2; j++) {
if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode)) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
// 如果未找到匹配的新子节点,卸载旧子节点
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 标记新子节点对应的旧子节点索引
newIndexToOldIndexMap[newIndex - s2] = i + 1
// 检查是否需要移动节点
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新匹配的节点
patch(prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
patched++
}
}
// 4.3 移动和挂载
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 倒序处理以便正确移动节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el
// 倒序处理以便正确移动节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 挂载新子节点
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// 移动节点
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
这部分代码是 diff 算法的最后一步,处理节点的移动和挂载。让我们详细分析每一行:
-
for (i = toBePatched - 1; i >= 0; i--)- 倒序遍历需要处理的新子节点,这样可以确保在移动节点时锚点位置是正确的。 -
const nextIndex = s2 + i- 计算当前处理的新子节点在原数组中的索引。 -
const nextChild = c2[nextIndex] as VNode- 获取当前处理的新子节点。 -
const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor- 确定插入位置的锚点,使用下一个节点的 DOM 元素或父容器的锚点。 -
if (newIndexToOldIndexMap[i] === 0)- 检查当前新子节点是否在旧子节点中存在。值为 0 表示这是一个全新的节点。 -
如果是新节点,调用
patch(null, nextChild, ...)挂载它。 -
else if (moved)- 如果需要移动节点(之前的处理中检测到了顺序变化)。 -
if (j < 0 || i !== increasingNewIndexSequence[j])- 检查当前节点是否需要移动:j < 0表示已经处理完所有最长递增子序列中的节点i !== increasingNewIndexSequence[j]表示当前节点不在最长递增子序列中
-
如果需要移动,调用
move(nextChild, container, anchor, MoveType.REORDER)移动节点。 -
如果不需要移动(即当前节点在最长递增子序列中),则
j--继续处理前一个最长递增子序列中的节点。
最长递增子序列算法
Vue3 的 diff 算法使用了最长递增子序列(Longest Increasing Subsequence,简称 LIS)算法来优化节点移动。这个算法的实现如下:
// 获取最长递增子序列的索引
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]
// 跳过值为 0 的元素(表示这是新增的节点)
if (arrI !== 0) {
j = result[result.length - 1]
// 如果当前元素大于结果数组中最后一个元素对应的值,直接添加到结果数组
if (arr[j] < arrI) {
p[i] = j // 记录前驱节点
result.push(i)
continue
}
// 二分查找,找到第一个大于等于 arrI 的位置
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
}
这个算法的详细分析:
-
const p = arr.slice()- 创建一个数组 p 用于记录前驱节点的索引。 -
const result = [0]- 初始化结果数组,包含第一个元素的索引。 -
主循环遍历输入数组:
- 跳过值为 0 的元素(表示新增节点)
- 如果当前元素大于结果数组最后一个元素对应的值,直接添加到结果数组
- 否则,使用二分查找找到结果数组中第一个大于等于当前元素的位置,并更新该位置
-
最后通过回溯构建最终的最长递增子序列。
move 函数
当需要移动节点时,Vue3 使用 move 函数:
const move = (
vnode: VNode,
container: HostElement,
anchor: HostNode | null,
moveType: MoveType,
parentSuspense: SuspenseBoundary | null = null
) => {
const { el, type, transition, children, shapeFlag } = vnode
// 处理 Fragment
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
// KeepAlive 组件特殊处理
(vnode.component as any).move(vnode, container, anchor, moveType)
return
}
if (shapeFlag & ShapeFlags.TELEPORT) {
// Teleport 组件特殊处理
(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
return
}
if (type === Fragment) {
// Fragment 节点,移动所有子节点
hostInsert(el!, container, anchor)
for (let i = 0; i < (children as VNode[]).length; i++) {
move((children as VNode[])[i], container, anchor, moveType)
}
hostInsert(vnode.anchor!, container, anchor)
return
}
// 处理静态节点
if (type === Static) {
moveStaticNode(vnode, container, anchor)
return
}
// 处理普通节点
const needTransition = moveType !== MoveType.REORDER &&
shapeFlag & ShapeFlags.ELEMENT &&
transition
if (needTransition) {
// 如果需要过渡效果
if (moveType === MoveType.ENTER) {
transition!.beforeEnter(el!)
hostInsert(el!, container, anchor)
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
} else {
const { leave, delayLeave, afterLeave } = transition!
const remove = () => hostInsert(el!, container, anchor)
const performLeave = () => {
leave(el!, () => {
remove()
afterLeave && afterLeave()
})
}
if (delayLeave) {
delayLeave(el!, remove, performLeave)
} else {
performLeave()
}
}
} else {
// 直接移动节点
hostInsert(el!, container, anchor)
}
}
move 函数的详细分析:
-
首先检查节点的类型,对不同类型的节点进行特殊处理:
- KeepAlive 组件
- Teleport 组件
- Fragment 节点
- 静态节点
-
对于普通节点,检查是否需要过渡效果:
- 如果需要过渡效果,根据移动类型(进入或离开)应用相应的过渡
- 如果不需要过渡效果,直接调用 hostInsert 插入节点
Vue3 Diff 算法的优化
Vue3 的 diff 算法相比 Vue2 有以下几个主要优化:
-
静态提升(Static Hoisting):
- 将静态节点提升到渲染函数之外,避免每次渲染时重新创建
- 在编译时完成,不在运行时的 diff 算法中
-
静态树提升(Static Tree Hoisting):
- 将整个静态子树提升,减少虚拟 DOM 的创建和比对
- 同样在编译时完成
-
块树(Block Tree):
- 将模板基于动态节点指令切割为嵌套的区块
- 每个区块内部的节点结构是固定的
- 每个区块只需要追踪自身包含的动态节点
-
更高效的 Diff 算法:
- 双端比较优化
- 最长递增子序列算法优化节点移动
-
PatchFlags 标记:
- 在编译时为动态节点添加标记,指示节点的哪些属性是动态的
- 运行时只需要检查和更新这些动态属性,跳过静态部分
总结
通过深入分析 Vue3 的源码,我们了解了其三个核心部分:
-
响应式系统:基于 ES6 Proxy 实现,相比 Vue2 的 Object.defineProperty 有更好的性能和功能。
-
虚拟 DOM:使用 JavaScript 对象表示 DOM 结构,通过比较新旧虚拟 DOM 树的差异来最小化 DOM 操作。
-
Diff 算法:Vue3 的 diff 算法采用了多种优化策略,包括:
- 从头尾同步处理相同节点
- 使用 key 快速定位节点
- 最长递增子序列算法优化节点移动
- 编译时优化(静态提升、块树等)
Vue3 的这些核心实现使其在性能和开发体验上都有了显著提升。理解这些实现细节不仅有助于更好地使用 Vue3,也能帮助我们在构建自己的应用时做出更优的设计决策。
Vue3 的源码设计非常精巧,每一部分都经过精心设计和优化。通过学习这些源码,我们可以学到很多现代前端框架的设计思想和实现技巧,这对提升我们自身的编程能力非常有帮助。