Logo个人技术学习笔记

从 0 到 1 手写 Vue3 核心原理:吃透响应式、虚拟 DOM、编译与 Diff

2026-05-24T19:33:03
191 阅读
vue

前言

🔥 面试必看!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 点,清晰又全面:

  1. 响应式:基于 Proxy + Reflect 实现,通过 track 收集依赖、trigger 触发更新,配套 computed(计算属性,带缓存)和 watch(监听器,自定义回调),解决 Vue2 响应式缺陷。
  2. 虚拟 DOM:用 JS 对象描述真实 DOM,减少直接操作真实 DOM 的开销,提升渲染性能。
  3. Diff 算法:对比新旧虚拟 DOM,优先复用节点,只更新变化的部分(文本、子节点),最小化 DOM 操作。
  4. 模板编译:将模板编译成 render 函数,编译时做静态提升、PatchFlags 等优化,进一步提升更新性能。
  5. 整体流程:createApp → 响应式数据 → 模板编译 → 虚拟 DOM → 挂载真实 DOM;数据变化 → 触发更新 → Diff → 视图更新。

一句话总结 Vue3 核心:用 Proxy 做响应式劫持,用 track/trigger 管理依赖,用 computed 做缓存计算、watch 做自定义监听,用虚拟 DOM + Diff 最小化更新视图,用编译优化进一步提速

八、结尾语

看到这里,你已经掌握了 Vue3 最核心的底层原理!

其实 Vue3 原理并不复杂,核心就是「响应式 + 虚拟 DOM + 编译优化」这三件事,跟着本文手写一遍代码,你会发现:那些面试中看似高深的问题,本质都是这些基础逻辑的延伸。