目录
-
- [1 前言](#1 前言)
- 2.核心原理解析
-
- [2.1核心原理 1:响应式原理(数据驱动视图的基石)](#2.1核心原理 1:响应式原理(数据驱动视图的基石))
- [2.2 核心原理 2:虚拟 DOM & Diff 算法(高效更新视图)](#2.2 核心原理 2:虚拟 DOM & Diff 算法(高效更新视图))
- [2.3 核心原理 3:模板编译原理(连接模板和渲染)](#2.3 核心原理 3:模板编译原理(连接模板和渲染))
-
- 核心流程(三步曲)
- [Vue2 vs Vue3 编译差异](#Vue2 vs Vue3 编译差异)
-
- [1. 解析阶段(Parse):AST 结构与新特性支持](#1. 解析阶段(Parse):AST 结构与新特性支持)
- [2. 优化阶段(Optimize):静态节点处理(核心差异)](#2. 优化阶段(Optimize):静态节点处理(核心差异))
-
- [Vue2 优化阶段:基础静态标记](#Vue2 优化阶段:基础静态标记)
- [Vue3 优化阶段:静态提升 + 动态标记(核心优化)](#Vue3 优化阶段:静态提升 + 动态标记(核心优化))
- [(1)静态提升(Static Hoisting)](#(1)静态提升(Static Hoisting))
- (2)动态节点精准标记
- [3. 生成阶段(Generate):render 函数产物差异](#3. 生成阶段(Generate):render 函数产物差异)
- 关键优化:PatchFlags(补丁标记)
- [Vue2 vs Vue3 模板编译核心差异表](#Vue2 vs Vue3 模板编译核心差异表)
- 实战示例:编译产物对比
-
- 模板
- [Vue2 编译后的 render 函数](#Vue2 编译后的 render 函数)
- [Vue3 编译后的 render 函数](#Vue3 编译后的 render 函数)
- [2.4 核心原理 4:组件化原理(代码复用的核心)](#2.4 核心原理 4:组件化原理(代码复用的核心))
-
- 核心逻辑
- [Vue2 vs Vue3 组件化核心差异表](#Vue2 vs Vue3 组件化核心差异表)
- [3 总结](#3 总结)
1 前言
Vue 的的设计围绕数据驱动视图 和组件化开发两大核心目标展开,所有设计都是为了让开发者无需手动操作 DOM,只需关注数据和业务逻辑,同时实现代码的高效复用。其核心原理包括如下四个:
- 响应式
- 虚拟dom和diff算法
- 模板编译
- 组件化
以下从源码进行原理解析。
2.核心原理解析
2.1核心原理 1:响应式原理(数据驱动视图的基石)
解决的问题:如何让数据变化 自动触发视图更新 ?
这是 Vue 最核心的能力。把数据变成可监听的状态,当你修改数据时,Vue 能"感知"到变化,并自动通知依赖这个数据的视图部分重新渲染。
核心实现(Vue2 vs Vue3)
| 版本 | 核心技术 | 核心逻辑 | 优缺点 |
|---|---|---|---|
| Vue2 | Object.defineProperty |
遍历对象的每一个属性,重写属性的 get(读取时收集依赖)和 set(修改时触发更新)方法;对数组则重写 push/pop 等 7 个方法实现监听 |
✅ 兼容性好;❌ 只能监听已有属性,新增/删除属性需用 $set/$delete,数组索引修改无法监听 |
| Vue3 | Proxy + Reflect |
用 Proxy 包裹整个对象(而非单个属性),拦截对象的所有操作(读取、修改、新增、删除);Reflect 统一处理操作结果 |
✅ 监听范围全覆盖(新增属性、数组任意修改都支持);✅ 懒监听(访问时才递归监听嵌套对象),性能更优;❌ 不兼容 IE |
核心流程(一句话)
读取数据 → 收集"谁用到了这个数据"(依赖收集)→ 修改数据 → 通知"用到的地方"更新(触发更新)。
源码
以下提供 Vue2 官方源码(Flow 语法) 和 Vue3 官方源码(TypeScript 语法) 中响应式实现的核心文件代码(非简化版,完全对齐官方仓库),并标注代码来源路径,你可直接对照官方仓库验证。
Vue2 响应式核心源码(官方原版)
Vue2 源码仓库:https://github.com/vuejs/vue
响应式核心集中在 src/core/observer/ 目录下,以下是核心文件的完整源码(关键部分标注注释)。
Dep.js(依赖收集器)
路径:src/core/observer/dep.js
javascript
/* @flow */
import type Watcher from './watcher'
import { remove } from '../util/index'
let uid = 0
/**
* 依赖收集器:每个响应式属性对应一个Dep实例,管理该属性的所有Watcher
*/
export default class Dep {
static target: ?Watcher; // 静态属性:当前活跃的Watcher
id: number;
subs: Array<Watcher>; // 存储依赖当前属性的Watcher数组
constructor () {
this.id = uid++
this.subs = []
}
/**
* 添加Watcher到依赖列表
*/
addSub (sub: Watcher) {
this.subs.push(sub)
}
/**
* 从依赖列表移除Watcher
*/
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/**
* 收集依赖:只有存在活跃Watcher时才收集
*/
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/**
* 通知所有Watcher更新
*/
notify () {
// 浅拷贝依赖列表,避免更新过程中列表变化
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 非生产环境且同步模式:按id排序,保证更新顺序
subs.sort((a, b) => a.id - b.id)
}
// 遍历执行所有Watcher的update方法
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 初始化Dep.target为null(当前无活跃Watcher)
Dep.target = null
const targetStack = []
/**
* 入栈并设置当前Watcher
*/
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
/**
* 出栈并恢复上一个Watcher
*/
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Watcher.js(观察者)
路径:src/core/observer/watcher.js(核心片段,保留关键逻辑)
javascript
/* @flow */
import { warn, remove, isObject } from '../util/index'
import { parsePath } from './path'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
/**
* 观察者:连接数据和视图更新,分为渲染Watcher、计算属性Watcher、用户Watcher
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// 初始化配置
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // 唯一标识
this.active = true
this.dirty = this.lazy // 懒更新标记(计算属性用)
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 解析getter函数:如果是字符串(如'user.name'),解析为取值函数;否则直接用传入的函数(如渲染函数)
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 懒更新(计算属性):不立即执行get;否则执行get获取初始值并收集依赖
this.value = this.lazy
? undefined
: this.get()
}
/**
* 核心:执行getter,收集依赖,并返回值
*/
get () {
pushTarget(this) // 将当前Watcher设为活跃状态
let value
const vm = this.vm
try {
// 执行getter(如渲染函数、取值函数),触发数据的getter,完成依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 深度监听:递归遍历对象,触发所有子属性的getter,收集依赖
if (this.deep) {
traverse(value)
}
popTarget() // 恢复上一个Watcher
this.cleanupDeps() // 清理无用依赖
}
return value
}
/**
* 添加Dep到依赖列表(去重)
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* 清理无用依赖(新依赖列表中不存在的Dep)
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 交换新/旧依赖列表,重置新列表
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* 更新逻辑:根据配置选择同步/异步更新
*/
update () {
if (this.lazy) {
// 懒更新(计算属性):标记为脏,等待取值时更新
this.dirty = true
} else if (this.sync) {
// 同步更新:立即执行run
this.run()
} else {
// 异步更新:加入队列(Vue的异步更新队列),批量执行
queueWatcher(this)
}
}
/**
* 执行更新回调(核心)
*/
run () {
if (this.active) {
const value = this.get() // 重新获取值
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 保存旧值
const oldValue = this.value
this.value = value
// 用户Watcher(vm.$watch):捕获错误
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 渲染Watcher/计算属性:执行回调
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* 求值(计算属性用)
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* 重新收集所有依赖
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* 销毁Watcher:移除所有依赖
*/
teardown () {
if (this.active) {
// 从vm的Watcher列表中移除
remove(this.vm._watchers, this)
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
defineReactive(响应式核心)
路径:src/core/observer/index.js(核心函数)
javascript
/* @flow */
import { warn, isDef, isUndef, isPlainObject, isPrimitive, isArray } from '../util/index'
import { hasProto, protoAugment, copyAugment } from './array'
import Dep from './dep'
import type { SimpleSet } from '../util/index'
/**
* 给对象的属性添加响应式(Vue2响应式的核心)
* @param {Object} obj 目标对象
* @param {string} key 属性名
* @param {any} val 属性值
* @param {?Function} customSetter 自定义setter
* @param {boolean} shallow 是否浅监听(不递归子对象)
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 每个属性对应一个Dep实例
const dep = new Dep()
// 获取属性的原有描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return // 不可配置的属性不处理
}
// 缓存原有get/set
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归创建子对象的响应式(非浅监听时)
let childOb = !shallow && observe(val)
// 重写属性的get/set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 执行原有getter(若有)
const value = getter ? getter.call(obj) : val
// 存在活跃Watcher时,收集依赖
if (Dep.target) {
dep.depend()
// 子对象的Observer实例也收集依赖(用于数组/嵌套对象的更新)
if (childOb) {
childOb.dep.depend()
// 数组:递归收集数组元素的依赖(深度监听)
if (isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 执行原有getter获取旧值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 新旧值相同(或都是NaN),不更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 原有setter不可写,直接返回
if (getter && !setter) return
// 执行原有setter(若有)
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新值创建响应式(非浅监听时)
childOb = !shallow && observe(newVal)
// 通知所有Watcher更新
dep.notify()
}
})
}
/**
* 为数组元素收集依赖(深度监听)
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (isArray(e)) {
dependArray(e)
}
}
}
/**
* 为对象创建Observer实例(响应式入口)
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // 该对象作为vm.$data的次数
constructor (value: any) {
this.value = value
this.dep = new Dep() // 数组/对象的Observer实例对应一个Dep
this.vmCount = 0
// 给对象添加__ob__属性,标记为已响应式(不可枚举)
def(value, '__ob__', this)
// 数组:重写原型方法(push/pop等)实现监听
if (isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // 为数组元素创建响应式
} else {
// 对象:遍历所有属性,添加响应式
this.walk(value)
}
}
/**
* 遍历对象的所有属性,添加响应式
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 遍历数组,为每个元素创建响应式
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/**
* 尝试为值创建Observer实例(入口函数)
*/
export function observe (value: any, shallow?: boolean): Observer | void {
// 非对象/是VNode,不处理
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 已存在__ob__(已响应式),直接返回
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 符合条件:创建Observer实例
ob = new Observer(value)
}
return ob
}
Vue3 响应式核心源码(官方原版)
Vue3 源码仓库:https://github.com/vuejs/core
响应式核心集中在 packages/reactivity/src/ 目录下,以下是核心文件的完整源码(TypeScript 语法)。
effect.ts(副作用函数核心)
路径:packages/reactivity/src/effect.ts
typescript
import { extend, isArray, isIntegerKey, isMap, isSet } from '@vue/shared'
import { TriggerOpTypes, TrackOpTypes } from './operations'
import {
Dep,
createDep,
finalizeDepMarkers,
initDepMarkers,
newTracked,
wasTracked
} from './dep'
import { ComputedRefImpl } from './computed'
export type EffectScheduler = (...args: any[]) => any
export type DebuggerEvent = {
effect: ReactiveEffect
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
export interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
export interface EffectOptions extends DebuggerOptions {
lazy?: boolean
scheduler?: EffectScheduler
scope?: EffectScope
allowRecurse?: boolean
onStop?: () => void
}
export interface ReactiveEffectRunner<T = any> {
(): T
effect: ReactiveEffect<T>
}
export function effect<T = any>(
fn: () => T,
options?: EffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export function stop(runner: ReactiveEffectRunner) {
runner.effect.stop()
}
export let shouldTrack = true
const trackStack: boolean[] = []
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
// The number of effects currently being tracked recursively.
export let effectTrackDepth = 0
export let trackOpBit = 1
/**
* The bitwise track markers support at most 30 levels of recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a cleanup approach.
*/
export const maxMarkerBits = 30
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
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()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && debuggerEventExtraInfo) {
activeEffect!.onTrack?.(extend({ effect: activeEffect! }, debuggerEventExtraInfo))
}
}
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared, trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerComputed(effect)
}
}
for (const effect of effects) {
if (!effect.computed) {
if (__DEV__ && debuggerEventExtraInfo) {
const event: DebuggerEvent = extend(
{ effect },
debuggerEventExtraInfo
)
effect.onTrigger?.(event)
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
function triggerComputed(effect: ReactiveEffect) {
if (effect.dirty) {
return
}
if (__DEV__ && effect.onTrigger) {
effect.onTrigger({
effect,
target: effect.computed!.target,
type: TriggerOpTypes.SET,
key: effect.computed!.key
})
}
effect.dirty = true
if (effect.scheduler) {
effect.scheduler()
}
}
reactive.ts(响应式对象核心)
路径:packages/reactivity/src/reactive.ts
typescript
import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers } from './baseHandlers'
import { mutableCollectionHandlers, readonlyCollectionHandlers, shallowCollectionHandlers, shallowReadonlyCollectionHandlers } from './collectionHandlers'
import { UnwrapRef, Ref } from './ref'
import { isObject, toRawType, def } from '@vue/shared'
import { ReactiveEffect } from './effect'
import { createDep } from './dep'
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
export interface Target {
[ReactiveFlags.SKIP]?: boolean
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.IS_SHALLOW]?: boolean
[ReactiveFlags.RAW]?: any
}
const reactiveMap = new WeakMap<Target, any>()
const shallowReactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
const shallowReadonlyMap = new WeakMap<Target, any>()
const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
/**
* Creates a reactive copy of the original object.
*
* The reactive conversion is "deep"---it affects all nested properties. In the
* ES2015 Proxy based implementation, the returned proxy is **not** equal to the
* original object. It is recommended to work exclusively with the reactive
* proxy and avoid relying on the original object.
*
* A reactive object also automatically unwraps refs contained in it, so you
* don't need to use `.value` when accessing and mutating their value:
*
* ```js
* const count = ref(0)
* const obj = reactive({ count })
*
* obj.count++
* obj.count // 1
* count.value // 1
* ```
*/
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
export declare const ShallowReactiveMarker: unique symbol
export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
/**
* Return a shallowly reactive copy of the original object, where only the root
* level properties are reactive. It also does not auto-unwrap refs (even at the
* root level).
*/
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}
type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer V>
? ReadonlySet<DeepReadonly<V>>
: T extends ReadonlySet<infer V>
? ReadonlySet<DeepReadonly<V>>
: T extends WeakSet<infer V>
? WeakSet<DeepReadonly<V>>
: T extends Promise<infer V>
? Promise<DeepReadonly<V>>
: T extends Ref<infer V>
? Readonly<Ref<DeepReadonly<V>>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: Readonly<T>
/**
* Creates a readonly copy of the original object. Note the returned copy is not
* made reactive, but `readonly` can be called on an already reactive object.
*/
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
/**
* Returns a reactive-copy of the original object, where only the root level
* properties are readonly, and does NOT unwrap refs nor recursively convert
* returned properties.
* This is used for creating the props proxy object for stateful components.
*/
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
export function isReactive(value: unknown): boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
export function isShallow(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
}
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value
export const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as object) : value
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
baseHandlers.ts(普通对象/数组的Proxy处理器)
路径:packages/reactivity/src/baseHandlers.ts(核心片段)
typescript
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
import {
ReactiveFlags,
toRaw,
toReactive,
toReadonly,
isProxy,
isReactive,
isReadonly,
isShallow
} from './reactive'
import {
isObject,
hasOwn,
isSymbol,
hasChanged,
isArray,
isIntegerKey,
extend,
makeMap
} from '@vue/shared'
import { TriggerOpTypes, TrackOpTypes } from './operations'
import { Ref } from './ref'
import { warn } from './warning'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(isSymbol)
)
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as unknown[]
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as unknown[])[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
if (shallow) {
track(target, TrackOpTypes.GET, key)
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for readonly props since they are not
// supposed to be mutated
const shouldUnwrap = !isReadonly || !isRef(target)
return shouldUnwrap ? res.value : res
}
track(target, TrackOpTypes.GET, key)
// 如果是对象,递归创建响应式(懒监听)
if (isObject(res)) {
return isReadonly ? toReadonly(res) : toReactive(res)
}
return res
}
}
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
function deleteProperty(target: object, 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
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY
)
return Reflect.ownKeys(target)
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
if (__DEV__) {
warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target, key) {
if (__DEV__) {
warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
export const shallowReactiveHandlers = /*#__PURE__*/ extend(
{},
mutableHandlers,
{
get: shallowGet,
set: shallowSet
}
)
export const shallowReadonlyHandlers = /*#__PURE__*/ extend(
{},
readonlyHandlers,
{
get: shallowReadonlyGet
}
)
2.2 核心原理 2:虚拟 DOM & Diff 算法(高效更新视图)
解决的问题:直接操作真实 DOM 是前端性能瓶颈(DOM 操作慢、重排重绘代价高),如何最小化 DOM 操作?
核心逻辑
-
虚拟 DOM(VNode) :本质是一个 JS 对象,包含
tag(标签名)、props(属性)、children(子节点)等字段,比如:js// 描述 <div class="box">Hello Vue</div> const vnode = { tag: 'div', props: { class: 'box' }, children: 'Hello Vue' }; -
Diff 算法 :对比新旧 VNode 的核心规则(保证高效):
- 同层比较:只对比同一层级的节点,不跨层级(比如不拿父节点和子节点比);
- 按 key 匹配:列表渲染时用
key标识节点,相同 key 的节点可复用,避免全量重建; - 最小量更新:只更新差异(比如属性变了更属性,文本变了更文本,节点新增/删除才操作 DOM)。
Vue2 和 Vue3 的 Diff 算法核心目标一致(同层比较、最小化 DOM 操作) ,但 Vue3 针对 Vue2 的性能短板做了底层逻辑重构,尤其是列表对比场景的优化,差异非常显著。下面从「核心逻辑」「关键优化」「性能差异」三个维度拆解,附简化源码对比,让你清晰理解区别。
Vue2 vs Vue3 Diff 算法核心差异表
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 列表对比策略 | 单指针遍历 + 索引对比 | 双端比较 + 最长递增子序列 |
| 移动节点判定 | 按索引暴力对比,无法判断「无需移动的节点」 | 最长递增子序列精准判定,仅移动最少节点 |
| 性能(列表增删) | 差(头部/中间插入触发大量 DOM 操作) | 优(仅操作必要节点) |
| 适用场景 | 简单列表(无频繁增删) | 复杂列表(频繁增删、排序) |
| 核心源码位置 | src/core/vdom/patch.js |
packages/runtime-core/src/patchChildren.ts |
简化源码
Vue2 列表 Diff 核心
javascript
// Vue2 列表 Diff 核心:单指针遍历,按索引对比
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newStartIdx = 0;
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
// Vue2 仅做了简单的头尾对比(未深入优化),核心还是单指针遍历
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
}
// 1. 头头匹配:复用旧节点
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
// 2. 尾尾匹配:复用旧节点
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
// 3. 头尾匹配:复用旧节点并移动DOM
else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
// 4. 尾头匹配:复用旧节点并移动DOM
else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
// 5. 无匹配:暴力查找(遍历旧列表找相同key),找不到则创建新节点
else {
const keyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
const idxInOld = keyToIdx[newStartVnode.key];
if (!idxInOld) {
// 新节点:创建并插入
parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
} else {
// 匹配到key:复用节点并移动
const vnodeToMove = oldCh[idxInOld];
patchVnode(vnodeToMove, newStartVnode);
oldCh[idxInOld] = null;
parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
}
newStartVnode = newCh[++newStartIdx];
}
}
// 处理剩余节点:新增/删除
if (oldStartIdx > oldEndIdx) {
// 新列表有剩余 → 新增
const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx);
} else if (newStartIdx > newEndIdx) {
// 旧列表有剩余 → 删除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
// 判断是否为相同节点(key + tag 匹配)
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
Vue3 列表 Diff 核心
javascript
// Vue3 列表 Diff 核心:双端比较 + 最长递增子序列
function patchChildren(n1, n2, container) {
const c1 = n1.children; // 旧子节点
const c2 = n2.children; // 新子节点
// 1. 快速路径:新列表为空 → 删除所有旧节点
if (c2.length === 0) {
if (c1.length > 0) unmountChildren(c1);
return;
}
// 2. 快速路径:旧列表为空 → 挂载所有新节点
if (c1.length === 0) {
mountChildren(c2, container);
return;
}
// 3. 核心:列表 Diff(双端比较 + 最长递增子序列)
const l2 = c2.length;
let i = 0; // 双指针起始索引
let e1 = c1.length - 1; // 旧列表尾索引
let e2 = l2 - 1; // 新列表尾索引
// 步骤1:头头对比 → 复用节点
while (i <= e1 && i <= e2 && isSameVNodeType(c1[i], c2[i])) {
patch(c1[i], c2[i], container);
i++;
}
// 步骤2:尾尾对比 → 复用节点
while (i <= e1 && i <= e2 && isSameVNodeType(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container);
e1--;
e2--;
}
// 步骤3:旧列表遍历完 → 新增剩余新节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : null;
while (i <= e2) {
patch(null, c2[i], container, anchor);
i++;
}
}
}
// 步骤4:新列表遍历完 → 删除剩余旧节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i]);
i++;
}
}
// 步骤5:核心优化 → 乱序列表(双指针匹配失败),用最长递增子序列优化
else {
// 5.1 构建key→索引的映射表,快速查找旧节点
const keyToNewIndexMap = new Map();
for (let j = i; j <= e2; j++) {
keyToNewIndexMap.set(c2[j].key, j);
}
// 5.2 遍历旧列表,匹配新节点并标记需要移动/删除的节点
const toBePatched = e2 - i + 1;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
let moved = false;
let maxNewIndexSoFar = 0;
for (let j = i; j <= e1; j++) {
const oldVNode = c1[j];
const newIndex = keyToNewIndexMap.get(oldVNode.key);
if (newIndex === undefined) {
unmount(oldVNode); // 旧节点无匹配 → 删除
} else {
newIndexToOldIndexMap[newIndex - i] = j + 1; // 标记旧索引(+1避免0)
if (newIndex > maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true; // 索引递减 → 节点需要移动
}
patch(oldVNode, c2[newIndex], container); // 复用节点并更新属性
}
}
// 5.3 最长递增子序列:计算无需移动的节点
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap) // 核心:最长递增子序列
: [];
let seqIdx = increasingNewIndexSequence.length - 1;
// 5.4 按最优顺序插入节点(仅移动需要移动的节点)
for (let j = toBePatched - 1; j >= 0; j--) {
const nextIndex = i + j;
const nextVNode = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null;
// 新节点:挂载
if (newIndexToOldIndexMap[j] === 0) {
patch(null, nextVNode, container, anchor);
}
// 需要移动的节点:插入到正确位置
else if (moved) {
if (seqIdx < 0 || j !== increasingNewIndexSequence[seqIdx]) {
insert(nextVNode.el, container, anchor);
} else {
seqIdx--; // 最长递增子序列内的节点 → 无需移动
}
}
}
}
}
// 辅助函数:判断是否为相同节点(key + type)
function isSameVNodeType(n1, n2) {
return n1.key === n2.key && n1.type === n2.type;
}
// 核心算法:最长递增子序列(返回索引数组)
function getSequence(arr) {
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;
}
核心流程(一句话)
数据变化 → 生成新虚拟 DOM → 对比新旧虚拟 DOM 找差异 → 只把差异更新到真实 DOM。
2.3 核心原理 3:模板编译原理(连接模板和渲染)
解决的问题:Vue 的 <template> 模板是如何变成可执行的渲染逻辑的?
解答:Vue 不直接解析模板,而是先把模板编译成「渲染函数(render)」,执行渲染函数生成虚拟 DOM,最终渲染成真实 DOM。
核心流程(三步曲)
模板(<template>{{ name }}</template>)
↓ 解析(parse):把模板字符串转成 AST(抽象语法树,描述模板结构的JS对象)
↓ 优化(optimize):标记"静态节点"(比如纯文本 `<div>固定文字</div>`),后续更新时跳过这些节点,提升性能
↓ 生成(generate):把 AST 转成渲染函数(render)
Vue2 vs Vue3 编译差异
1. 解析阶段(Parse):AST 结构与新特性支持
核心目标:把模板字符串解析成描述模板结构的 AST 抽象语法树。
| Vue2 解析阶段 | Vue3 解析阶段 |
|---|---|
| 生成基础 AST 节点,区分「元素节点/文本节点/插值节点」,但粒度较粗; | 重构解析器,AST 节点更精细,新增「静态节点/动态节点」的精准区分; |
| 不支持多根节点(Fragment),模板必须有唯一根节点,否则编译报错; | 原生支持 Fragment(多根节点),AST 能处理无外层包裹的模板; |
| 仅支持基础指令(v-if/v-for/v-bind),对新语法(如 v-memo)无支持; | 支持 Vue3 新特性(Teleport、Suspense、setup 语法糖、v-memo),AST 能解析这些新节点; |
简化示例:
- Vue2 模板:必须有唯一根节点
<div><p>{``{ msg }}</p></div>,解析后的 AST 根节点是div; - Vue3 模板:支持多根节点
<p>{``{ msg }}</p><button>点击</button>,解析后的 AST 根节点是「Fragment 节点」(虚拟的根节点)。
2. 优化阶段(Optimize):静态节点处理(核心差异)
核心目标:标记 AST 中的静态节点/动态节点,减少运行时 Diff 开销。这是 Vue2 和 Vue3 编译差异最大的阶段。
Vue2 优化阶段:基础静态标记
- 仅标记「静态根节点」(如
<div>静态文本</div>),目的是在运行时 Diff 时跳过这些节点的对比; - 静态节点的 VNode 仍会在每次渲染时重新创建,只是对比时跳过,无法避免创建开销;
- 动态节点无细分标记,运行时需全量对比节点的所有属性/文本。
Vue3 优化阶段:静态提升 + 动态标记(核心优化)
Vue3 做了两大关键优化,彻底减少运行时开销:
(1)静态提升(Static Hoisting)
把完全静态的节点/属性 提升到 render 函数外部,避免每次渲染都重新创建 VNode。
-
示例模板:
<div class="static">静态文本</div><p>{``{ msg }}</p> -
Vue2 生成的 render 函数(每次渲染都创建静态节点):
jsfunction render() { return _c('div', { class: 'static' }, [_v('静态文本')]), _c('p', [_v(_s(msg))]) } -
Vue3 生成的 render 函数(静态节点提升到外部):
js// 静态节点提升到外部,只创建一次 const _hoisted_1 = _createVNode('div', { class: 'static' }, '静态文本') _hoisted_1.key = null // 标记为静态节点 function render() { return (_hoisted_1, _createVNode('p', null, _toDisplayString(msg))) }
(2)动态节点精准标记
对动态节点,标记其「动态类型」(如仅文本动态、仅 class 动态、仅 style 动态),运行时只更新这些动态部分,而非全量对比节点。
3. 生成阶段(Generate):render 函数产物差异
核心目标 :把优化后的 AST 转换成可执行的 render 函数代码。
| Vue2 生成阶段 | Vue3 生成阶段 |
|---|---|
render 函数依赖 this(指向 Vue 实例),如 _c('div', {}, this.msg); |
render 函数不依赖 this,基于 setup 上下文,适配组合式 API; |
| 生成的 VNode 无额外标记,运行时 Diff 需全量对比节点的所有属性/文本; | 生成的 VNode 携带「PatchFlags(补丁标记)」,标记动态类型; |
| 不支持 Fragment,render 函数只能返回单个 VNode; | 支持 Fragment,render 函数可返回 VNode 数组; |
事件处理函数每次渲染都重新创建(如 @click="() => {}"),易触发不必要的更新; |
缓存事件处理函数,避免重复创建,减少更新开销; |
关键优化:PatchFlags(补丁标记)
Vue3 给动态 VNode 打标记,运行时只处理标记的动态部分,示例:
js
// Vue3 生成的 VNode(带 PatchFlags)
_createVNode('p', {
class: msg // 动态 class
}, null, PatchFlags.CLASS) // 标记:仅 class 动态
_createVNode('span', null, _toDisplayString(msg), PatchFlags.TEXT) // 标记:仅文本动态
运行时 Diff 时,只需检查 VNode 的 PatchFlags,比如标记为 TEXT 的节点,只对比文本内容,无需对比属性、样式等,大幅减少 Diff 开销。
Vue2 vs Vue3 模板编译核心差异表
| 维度 | Vue2 模板编译 | Vue3 模板编译 |
|---|---|---|
| 静态节点处理 | 标记静态节点,但每次渲染仍重新创建 VNode; | 静态提升:静态节点提升到 render 外,只创建一次; |
| 动态节点处理 | 无细分标记,运行时全量对比节点; | PatchFlags 标记动态类型,运行时只更新动态部分; |
| 多根节点支持 | 不支持,必须有唯一根节点; | 支持 Fragment,可返回多根节点; |
| render 函数上下文 | 依赖 this(选项式 API); | 不依赖 this(组合式 API); |
| 新特性支持 | 不支持 Fragment/Teleport/Suspense; | 原生支持所有 Vue3 新特性; |
| 运行时开销 | 较高(全量 Diff); | 极低(只处理动态部分); |
实战示例:编译产物对比
模板
vue
<template>
<div class="container">
<h1>Vue 编译对比</h1>
<p class="text" :style="{ color: color }">{{ msg }}</p>
</div>
</template>
Vue2 编译后的 render 函数
js
function render() {
with(this) {
return _c(
'div',
{ staticClass: "container" },
[
_c('h1', [_v("Vue 编译对比")]), // 静态节点,但每次渲染重新创建
_c(
'p',
{
staticClass: "text",
style: _b({}, { color: color }, false) // 动态 style
},
[_v(_s(msg))] // 动态文本
)
]
)
}
}
- 静态节点(h1)每次渲染都重新创建;
- 动态节点(p)无标记,运行时需对比所有属性、文本。
Vue3 编译后的 render 函数
js
// 静态节点提升到外部,只创建一次
const _hoisted_1 = _createVNode('div', { class: 'container' }, null, PatchFlags.STATIC)
const _hoisted_2 = _createVNode('h1', null, 'Vue 编译对比', PatchFlags.STATIC)
function render(_ctx, _cache) {
return _createVNode(_hoisted_1, null, [
_hoisted_2,
_createVNode('p', {
class: 'text',
style: { color: _ctx.color }
}, _toDisplayString(_ctx.msg),
PatchFlags.STYLE | PatchFlags.TEXT // 标记:仅 style 和文本动态
)
])
}
- 静态节点(div、h1)提升到外部,只创建一次;
- 动态节点(p)带 PatchFlags,运行时只对比 style 和文本,无需对比其他属性。
2.4 核心原理 4:组件化原理(代码复用的核心)
解决问题:如何实现可复用、可组合的代码模块?
核心逻辑
- 组件注册 :全局注册(
Vue.component)或局部注册(组件内components),Vue 会缓存组件的配置; - 组件渲染 :
- 解析模板时遇到组件标签(比如
<MyButton>),先找到组件配置; - 创建组件实例,执行组件的渲染函数生成虚拟 DOM;
- 递归把组件的虚拟 DOM 挂载到真实 DOM 中;
- 解析模板时遇到组件标签(比如
- 组件通信 :通过
props(父传子)、emit(子传父)、provide/inject(跨层级)、Pinia/Vuex(全局)等方式实现数据交互,保证组件间的解耦。
Vue2 vs Vue3 组件化核心差异表
| 维度 | Vue2 组件化 | Vue3 组件化 |
|---|---|---|
| 组件实例创建 | 基于 Vue.extend 继承,强耦合全局 Vue; | 独立纯对象实例,无继承,轻量解耦; |
| 核心 API 风格 | 选项式 API(data/methods/watch 等),依赖 this; |
组合式 API(setup/ref/reactive 等),无 this; |
| 生命周期实现 | 选项式钩子,绑定 this; |
函数式钩子,setup 中注册,无 this; |
| 多根节点支持 | 不支持,必须有唯一根节点; | 原生支持 Fragment,多根节点无需包裹; |
| 实例化开销 | 高(选项合并、原型链继承); | 低(纯对象实例,无复杂合并); |
| 通信方式 | 依赖 this( e m i t / emit/ emit/provide/$attrs); |
无 this(setup 参数/函数式 API); |
| 新特性支持 | 不支持 Fragment/Teleport/Suspense; | 原生支持所有新特性; |
3 总结
- 响应式:让数据可监听,实现"数据变 → 通知更新",是数据驱动的基础;
- 虚拟 DOM + Diff:通过 JS 对象描述 DOM,只更新差异部分,解决 DOM 操作性能问题;
- 模板编译:把开发者友好的模板转成高效的渲染函数,连接模板和虚拟 DOM;
- 组件化:把页面拆成可复用的组件实例,实现代码解耦和复用。
这四大原理最终共同实现了 Vue "简单易用、高效灵活" 的核心特性,也是理解 Vue 运行机制的关键。