前言
🔥 面试必看!Vue3 底层原理不用死记硬背,跟着我手写一遍,彻底搞懂 Proxy 响应式、虚拟 DOM、Diff 算法到底是怎么回事!
很多前端同学用 Vue3 开发一年多,却连「响应式为什么用 Proxy」「Diff 算法优化了什么」都答不上来,面试被问直接慌神。
本文不绕弯子、不堆源码,从 0 到 1 手写 Vue3 核心模块,包含 reactive/ref/computed、虚拟 DOM、模板编译、setup/生命周期,更有可直接复制运行的完整代码,全程手把手拆解,看完既能轻松应对面试,也能实打实提升开发深度!
本文核心收获:
- 掌握 Vue3 三大核心模块的工作逻辑
- 手写 watch 监听器,理解其与 computed 的区别和用法
- 手写响应式系统,理解 track/trigger 原理
- 搞懂虚拟 DOM 与 Diff 算法的优化点
- 获得一份可直接运行的迷你 Vue3 完整代码
- 记住面试必背的 Vue3 核心原理总结
- 手写响应式系统,理解 track/trigger 原理
- 搞懂虚拟 DOM 与 Diff 算法的优化点
- 获得一份可直接运行的迷你 Vue3 完整代码
- 记住面试必背的 Vue3 核心原理总结
一、Vue3 整体架构(先抓核心骨架)
Vue3 源码采用模块化设计,整体分为三大核心模块,协同工作实现「数据驱动视图」:
Vue3 = reactivity + runtime + compiler
- reactivity:响应式系统(核心),负责数据劫持、依赖收集、更新派发
- runtime:运行时模块,负责虚拟 DOM 渲染、Diff 算法、更新真实 DOM
- compiler:编译模块,负责将模板编译成渲染函数,做静态提升、靶向更新等优化
整个框架的核心思想只有一句:数据驱动视图,用最小代价更新 DOM。
二、响应式原理:Vue3 最核心的灵魂
1. 为什么弃用 Object.defineProperty,改用 Proxy?
Vue2 使用 Object.defineProperty 实现响应式,存在 3 个致命缺陷:
- 无法监听对象 新增/删除属性(需用 Vue.set/Vue.delete hack)
- 无法监听数组 下标修改、length 变化(需重写数组方法)
- 递归劫持所有属性,性能损耗大(即使属性用不到,也会被劫持)
Vue3 使用 ES6 Proxy + Reflect,完美解决所有问题:
- 监听整个对象,而非单个属性,支持新增/删除属性
- 天然支持数组所有操作(下标修改、push/pop 等),无需 hack
- 惰性代理,只有访问属性时才会劫持,性能大幅提升
2. 响应式核心流程(必记)
响应式的本质就是「监听数据读写,关联视图更新」,完整流程如下:
flowchart TD
A[读取数据] --> B[触发 Proxy get 拦截]
B --> C[track 收集依赖(记录使用该数据的渲染/计算函数)]
D[修改数据] --> E[触发 Proxy set 拦截]
E --> F[trigger 触发更新(执行所有依赖的函数)]
F --> G[重新渲染视图/计算属性]
```
简化记忆:**读数据收集依赖,改数据触发更新**。
### 3\. 手写响应式核心代码(可复制运行)
以下代码包含 reactive、ref、computed、effect、watch 核心实现,注释详细,逐行能看懂(新增 watch 监听器,与 computed 配套,贴合真实开发):
```javascript
// 1. 全局变量:当前激活的副作用(渲染函数 / computed 函数)
let activeEffect = null
// 2. 依赖存储结构:target(响应式对象)→ key(属性)→ Set(依赖列表)
const targetMap = new WeakMap()
// 3. 收集依赖:记录“谁用了这个数据”
function track(target, key) {
// 没有激活的副作用,直接返回(比如非渲染时读取数据)
if (!activeEffect) return
// 从 targetMap 中获取当前对象的依赖表(depsMap)
let depsMap = targetMap.get(target)
if (!depsMap) {
// 没有就创建一个新的依赖表,存入 targetMap
targetMap.set(target, (depsMap = new Map()))
}
// 从 depsMap 中获取当前属性的依赖集合(dep)
let dep = depsMap.get(key)
if (!dep) {
// 没有就创建一个新的依赖集合,存入 depsMap
depsMap.set(key, (dep = new Set()))
}
// 将当前激活的副作用,加入依赖集合
dep.add(activeEffect)
}
// 4. 触发更新:通知“所有用了这个数据的地方”重新执行
function trigger(target, key) {
// 获取当前对象的依赖表
const depsMap = targetMap.get(target)
if (!depsMap) return // 没有依赖,直接返回
// 获取当前属性的依赖集合
const dep = depsMap.get(key)
if (dep) {
// 执行所有依赖的副作用函数(重新渲染/重新计算)
dep.forEach(effect => effect())
}
}
// 5. reactive:创建响应式对象(针对引用类型)
function reactive(target) {
return new Proxy(target, {
// 读取属性时,收集依赖
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return res
},
// 修改属性时,触发更新
set(target, key, value, receiver) {
const oldVal = target[key] // 记录旧值
const ok = Reflect.set(target, key, value, receiver)
// 只有值发生变化,才触发更新(避免无效更新)
if (oldVal !== value) trigger(target, key)
return ok
}
})
}
// 6. ref:创建响应式数据(针对基础类型,如 string/number)
function ref(val) {
// 用一个对象包装基础类型,实现 get/set 拦截
const wrapper = {
get value() {
track(wrapper, 'value') // 收集依赖(依赖的是 wrapper.value)
return val
},
set value(newVal) {
val = newVal // 更新值
trigger(wrapper, 'value') // 触发更新
}
}
return wrapper
}
// 7. computed:计算属性(带缓存,依赖变化才重新计算)
function computed(getter) {
const value = ref() // 用 ref 存储计算结果
// 用 effect 关联依赖,getter 执行时会收集依赖
effect(() => value.value = getter())
return value
}
// 8. effect:副作用函数(关联渲染/计算逻辑)
function effect(fn) {
activeEffect = fn // 标记当前副作用为激活状态
fn() // 执行副作用函数(会触发数据读取,进而收集依赖)
activeEffect = null // 执行完毕,重置激活状态
}
// 9. watch:监听器(监听数据变化,执行自定义回调)
function watch(source, callback, options = {}) {
// 处理 ref 类型(如 watch(count, ...))
const getSource = () => {
if (source.value !== undefined) {
return source.value // ref 类型,取 .value
} else if (typeof source === 'function') {
return source() // 函数类型,执行函数取返回值
}
return source // reactive 类型,直接返回
}
let oldValue = getSource() // 初始值
// 用 effect 监听数据变化,依赖变化时执行回调
effect(() => {
const newValue = getSource()
// 只有值变化,才执行回调(支持 immediate 立即执行)
if (options.immediate || oldValue !== newValue) {
callback(newValue, oldValue) // 回调传递 新值、旧值
oldValue = newValue // 更新旧值,用于下次对比
}
})
}
三、虚拟 DOM + 渲染 + Diff 算法(高性能关键)
1. 虚拟 DOM 是什么?
虚拟 DOM(VNode)是用 轻量 JS 对象 描述真实 DOM 的结构,避免直接操作昂贵的真实 DOM(真实 DOM 包含大量浏览器内置属性,操作耗时)。
示例对比:
真实 DOM:
<div id="app" style="color: blue">Hello Vue3</div>
虚拟 DOM(VNode):
{
type: 'div', // 标签类型
props: { id: 'app', style: 'color: blue' }, // 属性/事件
children: 'Hello Vue3' // 子节点(文本/其他VNode)
}
2. 渲染与更新流程
Vue3 渲染的完整流程,一句话概括:
模板 → 编译 → render 函数 → 虚拟 DOM → 真实 DOM
数据变化 → 新虚拟 DOM → Diff 对比 → 最小化更新真实 DOM
用流程图更直观:
flowchart TD
A[模板] --> B[compiler 编译]
B --> C[render 函数]
C --> D[生成虚拟 DOM]
D --> E[mount 挂载 → 真实 DOM]
F[数据修改] --> G[重新执行 render 函数 → 新虚拟 DOM]
G --> H[patch(Diff)对比新旧虚拟 DOM]
H --> I[只更新变化的真实 DOM]
```
### 3\. 手写 h / mount / patch 核心代码
```javascript
// 1. h 函数:创建虚拟节点(VNode),简化虚拟 DOM 创建
function h(type, props, children) {
return { type, props: props || {}, children }
}
// 2. mount 函数:将虚拟 DOM 挂载到页面(转成真实 DOM)
function mount(vnode, container) {
// 1. 创建真实 DOM 元素
const el = document.createElement(vnode.type)
// 把真实 DOM 挂到 VNode 上,方便后续更新(Diff 时复用)
vnode.el = el
// 2. 处理属性和事件
for (const key in vnode.props) {
if (key.startsWith('on')) {
// 处理事件(如 onClick → click)
const eventName = key.slice(2).toLowerCase()
el.addEventListener(eventName, vnode.props[key])
} else {
// 处理普通属性(如 id、style)
el[key] = vnode.props[key]
}
}
// 3. 处理子节点
if (typeof vnode.children === 'string') {
// 子节点是文本,直接设置 textContent
el.textContent = vnode.children
} else if (Array.isArray(vnode.children)) {
// 子节点是数组(多个 VNode),递归挂载
vnode.children.forEach(child => mount(child, el))
}
// 4. 将真实 DOM 插入容器
container.appendChild(el)
}
// 3. patch 函数:Diff 算法,对比新旧虚拟 DOM,最小化更新真实 DOM
function patch(n1, n2) {
// 复用真实 DOM(避免重新创建,提升性能)
const el = n2.el = n1.el
// 情况1:新节点是文本节点
if (typeof n2.children === 'string') {
// 只有文本内容变化,才更新(避免无效更新)
if (n1.children !== n2.children) {
el.textContent = n2.children
}
return
}
// 情况2:新节点是子节点列表(如 v-for 场景)
const oldCh = n1.children || [] // 旧子节点列表
const newCh = n2.children || [] // 新子节点列表
// 子节点数量变化:新增子节点
if (newCh.length > oldCh.length) {
newCh.slice(oldCh.length).forEach(child => mount(child, el))
}
// 子节点数量变化:删除多余子节点
if (newCh.length < oldCh.length) {
oldCh.slice(newCh.length).forEach(child => el.removeChild(child.el))
}
// 子节点数量不变:递归对比每个子节点(深度 Diff)
for (let i = 0; i < Math.min(oldCh.length, newCh.length); i++) {
patch(oldCh[i], newCh[i])
}
}
四、模板编译与指令原理(语法糖底层)
1. 模板编译的核心作用
我们写的 Vue 模板(如 <div>{{ msg }}</div>),浏览器无法直接识别,Vue3 的 compiler 模块会将其 编译成 render 函数,再由 render 函数生成虚拟 DOM。
Vue3 编译时的 3 大优化(重点,面试常问):
- 静态提升:不变的节点(如静态文本)提升到 render 函数外,只创建一次,避免每次渲染都重新创建
- PatchFlags:给动态节点打标记(如文本、属性),Diff 时只更新带标记的节点,跳过静态节点
- 事件缓存:缓存事件处理函数,避免每次渲染都创建新函数,导致子组件不必要更新
2. 常用指令底层实现(面试高频)
Vue 的指令(v-if、v-for、v-model)本质是「语法糖」,底层都是编译成普通的 JS 逻辑,我们用极简代码模拟编译过程:
// 极简编译函数:模拟模板 → render 函数(真实 Vue3 编译器更复杂,核心逻辑一致)
function compile(template) {
// 编译后返回 render 函数,接收响应式上下文(ctx)
return function render(ctx) {
return h('div', null, [
// 1. v-if 底层:三元表达式(条件成立渲染节点,否则渲染其他节点)
ctx.show ? h('h2', null, ctx.msg) : h('h2', null, 'v-if 隐藏状态'),
// 2. v-for 底层:数组 map 方法(遍历数组生成多个 VNode)
...ctx.list.map(item => h('p', null, `v-for 遍历:${item}`)),
// 3. v-model 底层:value 属性 + onInput 事件(双向绑定)
h('input', {
value: ctx.inputVal, // 绑定值
onInput: e => ctx.inputVal = e.target.value // 输入时更新数据
}),
// 显示 v-model 绑定的值
h('p', null, `v-model 绑定结果:${ctx.inputVal}`)
])
}
}
五、createApp + setup + 生命周期(整合所有模块)
createApp 是 Vue3 的入口函数,负责整合响应式、渲染、编译、生命周期所有逻辑,手写实现如下(和真实 Vue3 用法完全一致):
function createApp(options) {
return {
// 挂载函数:将组件挂载到指定容器
mount(selector) {
// 1. 获取挂载容器
const container = document.querySelector(selector)
// 2. 处理响应式数据(优先执行 setup,没有则执行 data)
const ctx = reactive(options.setup ? options.setup() : options.data())
// 3. 执行生命周期:挂载前(onBeforeMount)
options.onBeforeMount?.()
// 4. 编译模板,生成 render 函数
const render = compile(options.template)
// 5. 存储旧虚拟 DOM(用于后续 Diff 更新)
let oldVNode = null
// 6. 核心:用 effect 关联渲染逻辑,实现数据驱动视图
effect(() => {
// 生成新虚拟 DOM
const newVNode = render(ctx)
if (!oldVNode) {
// 首次渲染:挂载虚拟 DOM 到页面
mount(newVNode, container)
// 执行生命周期:挂载完成(onMounted)
options.onMounted?.()
} else {
// 数据更新:Diff 对比新旧虚拟 DOM,更新页面
patch(oldVNode, newVNode)
// 执行生命周期:更新完成(onUpdated)
options.onUpdated?.()
}
// 保存当前虚拟 DOM,作为下一次 Diff 的旧虚拟 DOM
oldVNode = newVNode
})
}
}
}
六、终极完整版:可直接运行的迷你 Vue3
将以上所有模块整合,就是一个功能完整的迷你 Vue3,支持响应式、虚拟 DOM、模板指令、setup、生命周期,复制到 HTML 文件即可直接运行:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>手写迷你 Vue3 完整版</title>
</head>
<body>
<div id="app"></div>
<script>
// ==================== 1. 响应式模块 ====================
let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
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 = new Set()))
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(effect => effect())
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const oldVal = target[key]
const ok = Reflect.set(target, key, value, receiver)
if (oldVal !== value) trigger(target, key)
return ok
}
})
}
function ref(val) {
const wrapper = {
get value() { track(wrapper, 'value'); return val },
set value(newVal) { val = newVal; trigger(wrapper, 'value') }
}
return wrapper
}
function computed(getter) {
const value = ref()
effect(() => value.value = getter())
return value
}
function effect(fn) { activeEffect = fn; fn(); activeEffect = null }
// 新增:watch 监听器
function watch(source, callback, options = {}) {
const getSource = () => {
if (source.value !== undefined) return source.value;
if (typeof source === 'function') return source();
return source;
}
let oldValue = getSource()
effect(() => {
const newValue = getSource()
if (options.immediate || oldValue !== newValue) {
callback(newValue, oldValue)
oldValue = newValue
}
})
}
// ==================== 2. 虚拟 DOM + 渲染模块 ====================
function h(type, props, children) { return { type, props: props || {}, children } }
function mount(vnode, container) {
const el = document.createElement(vnode.type)
vnode.el = el
for (const key in vnode.props) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
} else {
el[key] = vnode.props[key]
}
}
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => mount(child, el))
}
container.appendChild(el)
}
function patch(n1, n2) {
const el = n2.el = n1.el
if (typeof n2.children === 'string') {
if (n1.children !== n2.children) el.textContent = n2.children
return
}
const oldCh = n1.children || []
const newCh = n2.children || []
if (newCh.length > oldCh.length) {
newCh.slice(oldCh.length).forEach(child => mount(child, el))
}
if (newCh.length < oldCh.length) {
oldCh.slice(newCh.length).forEach(child => el.removeChild(child.el))
}
for (let i = 0; i < Math.min(oldCh.length, newCh.length); i++) {
patch(oldCh[i], newCh[i])
}
}
// ==================== 3. 编译 + 入口模块 ====================
function compile(template) {
return function render(ctx) {
return h('div', null, [
ctx.show ? h('h2', null, ctx.msg) : h('h2', null, 'v-if 隐藏'),
...ctx.list.map(item => h('p', null, `v-for:${item}`)),
h('input', { value: ctx.inputVal, onInput: e => ctx.inputVal = e.target.value }),
h('p', null, `v-model:${ctx.inputVal}`),
h('p', null, `computed 计算:${ctx.double.value}`),
h('p', null, `watch 监听count:${ctx.watchTip}`)
])
}
}
function createApp(options) {
return {
mount(selector) {
const container = document.querySelector(selector)
const ctx = reactive(options.setup ? options.setup() : options.data())
options.onBeforeMount?.()
const render = compile(options.template)
let oldVNode = null
effect(() => {
const newVNode = render(ctx)
if (!oldVNode) {
mount(newVNode, container)
options.onMounted?.()
} else {
patch(oldVNode, newVNode)
options.onUpdated?.()
}
oldVNode = newVNode
})
}
}
}
// ==================== 4. 使用示例(新增 watch 用法) ====================
createApp({
setup() {
const msg = ref('Hello 手写 Vue3!')
const show = ref(true)
const list = reactive(['Vue', 'React', 'Angular'])
const inputVal = ref('双向绑定测试')
const count = ref(1)
const double = computed(() => count.value * 2)
const watchTip = ref('初始值:1')
// 新增:使用 watch 监听 count 变化
watch(count, (newVal, oldVal) => {
watchTip.value = `count从${oldVal}变成${newVal},double同步变为${newVal*2}`
console.log('watch 监听触发:', newVal, oldVal)
})
return { msg, show, list, inputVal, count, double, watchTip }
},
// 生命周期钩子
onBeforeMount: () => console.log('🚀 生命周期:挂载前'),
onMounted: () => console.log('✅ 生命周期:挂载完成'),
onUpdated: () => console.log('🔄 生命周期:视图更新'),
// 模板(新增 watch 展示)
template: `<div>{{ msg }}</div>`
}).mount('#app')
// 测试:修改数据,视图自动更新,watch 同步触发
setTimeout(() => {
const vm = document.querySelector('#app').__vue__
vm.msg = '数据更新啦!'
vm.count = 2
vm.list.push('Svelte')
}, 1500)
</script>
</body>
</html
七、Vue3 核心原理总结(面试必背)
这部分直接背,面试问 Vue3 原理,直接答这 5 点,清晰又全面:
- 响应式:基于 Proxy + Reflect 实现,通过 track 收集依赖、trigger 触发更新,配套 computed(计算属性,带缓存)和 watch(监听器,自定义回调),解决 Vue2 响应式缺陷。
- 虚拟 DOM:用 JS 对象描述真实 DOM,减少直接操作真实 DOM 的开销,提升渲染性能。
- Diff 算法:对比新旧虚拟 DOM,优先复用节点,只更新变化的部分(文本、子节点),最小化 DOM 操作。
- 模板编译:将模板编译成 render 函数,编译时做静态提升、PatchFlags 等优化,进一步提升更新性能。
- 整体流程:createApp → 响应式数据 → 模板编译 → 虚拟 DOM → 挂载真实 DOM;数据变化 → 触发更新 → Diff → 视图更新。
一句话总结 Vue3 核心:用 Proxy 做响应式劫持,用 track/trigger 管理依赖,用 computed 做缓存计算、watch 做自定义监听,用虚拟 DOM + Diff 最小化更新视图,用编译优化进一步提速。
八、结尾语
看到这里,你已经掌握了 Vue3 最核心的底层原理!
其实 Vue3 原理并不复杂,核心就是「响应式 + 虚拟 DOM + 编译优化」这三件事,跟着本文手写一遍代码,你会发现:那些面试中看似高深的问题,本质都是这些基础逻辑的延伸。