技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

面试官:Vue 的这些原理你了解吗?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/106-面试官 vue的这些原理你了解吗
本文目录11 个章节
  1. 一、Vue 和 React 有什么区别
  2. 二、new Vue() 阶段做了什么
  3. 三、Vue 的响应式原理
  4. 四、计算属性的原理
  5. 五、nextTick 原理
  6. 六、Vue 能不能同步渲染
  7. 七、Vue Router 的核心原理
  8. 八、手写一个简化 Vue Router
  9. 九、Vue 模板编译原理
  10. 十、面试总结
  11. 参考资料

面试官:Vue 的这些原理你了解吗?

原文以 Vue 2.6.11 为背景,串讲了 Vue 与 React 的区别、响应式、computed、nextTick、Vue Router、路由插件和模板编译。本文尽量保留原来的源码阅读路线,修正抓取成一行的代码、拼写错误、Vue 2/3 API 混用和“Vue 3 完美解决所有问题”等绝对表述,并补充 Vue 3.5、Vue Router 4/5 和 Nuxt 4 的现行说法。

一、Vue 和 React 有什么区别

面试时可以先给出一个不绝对化的回答:

Vue 是一个渐进式 JavaScript 框架,提供响应式状态、组件模型、模板/渲染机制和一套官方生态;React 更常被定位为 UI 库,核心负责组件和渲染,路由、状态和数据获取通常由社区生态补充。两者都支持声明式 UI、组件化和单向数据流,也都可以使用 JSX。

维度Vue 3React
定位渐进式框架,官方生态较完整UI 库,生态组合更自由
模板SFC template、render function、JSXJSX 为主,也有其他编译方案
响应式refreactive、computed、watchstate、context、外部 store 和渲染机制
组合逻辑Composition API、composableHooks、custom hooks
官方路由Vue Router 4/5React Router 等社区方案
官方状态方案Pinia(Vue 官方推荐生态)没有唯一官方全局状态库
编译优化patch flags、静态提升、Block TreeJSX/编译器及 React 的渲染调度优化

“Vue 双向绑定、React 单向数据流”也不能作为完整区别。Vue 组件内部同样遵循父传子、子通过事件通知父的单向数据流;v-model 只是对 props 和 update: 事件协议的语法封装。React 也可以通过受控组件、context 和状态库构造不同的数据流。

Vue 3 的 Composition API 与 React Hooks 都能复用逻辑,但两者运行模型、生命周期注册、响应式追踪和规则并不相同。Vue 2.7 也提供了部分 Composition API 兼容能力,但新项目通常选择 Vue 3。

二、new Vue() 阶段做了什么

下面的流程属于 Vue 2.6/2.7 Options API 的源码模型:

new Vue(options)
  ↓
Vue.prototype._init(options)
  ↓
初始化生命周期、事件、渲染能力和依赖注入
  ↓
beforeCreate
  ↓
initState:props、methods、data、computed、watch
  ↓
created
  ↓
$mount
  ↓
beforeMount → render watcher → mounted

Vue 2 中,data 会被 Observer 转换为响应式对象,普通对象属性通过 Object.defineProperty 定义 getter/setter。这个过程不是“给所有数据简单加一个监听器”,还包含依赖收集、数组方法拦截、嵌套对象递归观察和 watcher 调度。

可以用下面的简化代码理解 Dep

class Dep {
  static target = null

  constructor() {
    this.subs = []
  }

  addSub(watcher) {
    this.subs.push(watcher)
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    // 使用快照,避免派发期间修改订阅数组影响遍历。
    for (const watcher of this.subs.slice()) {
      watcher.update()
    }
  }
}

Dep 负责保存依赖,Watcher 是 Vue 2 中执行渲染、computed 或用户 watch 的观察者。真实源码还会去重依赖、维护 target stack,并通过 scheduler 批量更新。

2.1 Vue 3 的入口不同

Vue 3 不再使用 new Vue()

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

createApp 创建应用上下文,插件通过 app.use() 安装,组件在 setup()/<script setup> 中声明响应式状态和生命周期。Vue 3 响应式系统使用 ProxytracktriggerReactiveEffect 等机制,不能直接把 Vue 2 的 Observer/Dep/Watcher 类名套到 Vue 3 源码上。

Vue 初始化、响应式和渲染流程示意图(原文图片已本地化)

三、Vue 的响应式原理

3.1 Vue 2:getter 收集,setter 派发

Vue 2 的简化响应式属性可以写成:

function defineReactive(target, key, value) {
  const dep = new Dep()
  let observedValue = value

  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,

    get() {
      dep.depend()
      return observedValue
    },

    set(nextValue) {
      if (Object.is(nextValue, observedValue)) return
      observedValue = nextValue
      // 真实实现还会 observe 新对象,并触发模块钩子。
      dep.notify()
    },
  })
}

依赖收集和派发更新的过程是:

渲染 watcher 执行 render
  ↓ 读取响应式属性
getter → dep.depend() → 当前 watcher 加入 dep
  ↓
修改属性
setter → dep.notify()
  ↓
watcher.update() → 加入异步更新队列
  ↓
重新 render 和 patch

Vue 2 中常见三类 watcher:

  • render watcher:负责组件渲染;
  • computed watcher:惰性求值和缓存计算属性;
  • user watcher:由 watch 创建,执行用户回调。

响应式 API 的标准名称是 Object.defineProperty,代码中应保持拼写一致。

3.2 Vue 2 的限制

Object.defineProperty 可以拦截已经存在属性的读取和写入,但不能自动知道未来会新增哪些属性,因此 Vue 2 需要:

this.$set(this.form, 'newField', value)
// 或 Vue.set(this.form, 'newField', value)

数组直接通过下标赋值或修改 length 也不能触发 Vue 2 的响应式更新:

this.items[3] = item       // 可能无法触发更新
this.items.length = 0      // 不建议依赖响应式

Vue 2 通过改写 pushpopshiftunshiftsplicesortreverse 七个变更方法来通知更新;它不是因为 JavaScript 完全无法观察数组,而是 Vue 2 选择了这种兼顾成本的实现。Vue 2 可以观察已经存在的数组元素内部对象属性,但不会自动为越界新索引建立响应式定义。

3.3 Vue 3:Proxy 和 Reflect

Vue 3 使用代理拦截对象级别的操作:

const state = reactive({
  count: 0,
  list: [],
})

state.count++
state.list[0] = 'new item'
state.list.length = 0

教学版的依赖追踪可以简化为:

const targetMap = new WeakMap()
let activeEffect

function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap?.get(key)
  dep?.forEach(effect => effect())
}

实际 Vue 3 还要处理数组 length、迭代器、Map/Set、嵌套代理、只读对象、浅层代理、scheduler 和递归触发等情况。

Reflect 常用于在 Proxy handler 中正确转发默认行为:

const raw = { count: 0 }
const proxy = new Proxy(raw, {
  get(target, key, receiver) {
    track(target, key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if (!Object.is(oldValue, value)) trigger(target, key)
    return result
  },
})

Proxy 也不是“完美解决所有响应式问题”:它无法代理已经取出的原始值,解构会改变依赖追踪方式,第三方非响应式对象仍然需要手动处理,SSR 和副作用也要遵守对应环境规则。

Vue 2 defineProperty 与 Vue 3 Proxy 的响应式对比(原文图片已本地化)

四、计算属性的原理

4.1 Vue 2 computed watcher

Vue 2 的 computed watcher 通常是 lazy 的:

  1. 创建 computed watcher,但初始不立即计算;
  2. 第一次读取 computed 时执行 getter;
  3. getter 读取的响应式数据成为它的依赖;
  4. 依赖变化时把 dirty 设为 true
  5. 下一次读取时重新计算并缓存;
  6. 其他渲染 watcher 读取 computed 时,computed 还会把依赖通知给外层 watcher。

因此计算属性适合纯派生值:

computed: {
  fullName() {
    return `${this.firstName} ${this.lastName}`
  },
}

4.2 Vue 3 computed

Vue 3 的 computed() 仍然是惰性和缓存的 ref:

import { computed, ref } from 'vue'

const price = ref(10)
const count = ref(2)
const total = computed(() => price.value * count.value)

只有依赖变化且再次读取时才重新计算。不要把有副作用的请求、修改其他状态或随机值放进 computed getter;需要副作用时使用 watchwatchEffect

五、nextTick 原理

5.1 它解决什么问题

Vue 的响应式状态变化不会同步地把 DOM 立即改好:

<script setup>
import { nextTick, ref } from 'vue'

const name = ref('before')
const element = ref<HTMLElement | null>(null)

async function update() {
  name.value = 'after'
  console.log(element.value?.textContent) // 可能还是 before

  await nextTick()
  console.log(element.value?.textContent) // after
}
</script>

<template>
  <div ref="element">{{ name }}</div>
  <button @click="update">更新</button>
</template>

nextTick 的作用是等待当前这轮 DOM 更新 flush 完成后再执行回调或继续 Promise。它不是“等待任意异步请求”,也不是浏览器原生 API。

5.2 Vue 2 的队列模型

Vue 2 中修改数据大致经过:

setter
  → dep.notify()
  → watcher.update()
  → queueWatcher()
  → nextTick(flushSchedulerQueue)
  → watcher.run()
  → vm._render()
  → vm._update()
  → patch

queueWatcher 会按 watcher id 去重和排序,避免同一个同步执行段中重复渲染。原文把“异步渲染”理解为 setTimeout,这不够准确:Vue 2 使用异步 scheduler 批处理,具体的回调调度会根据环境选择 Promise、MutationObserver、setImmediate 或 setTimeout。

Vue 2 中 nextTick 的核心结构可以简化为:

const callbacks = []
let pending = false

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice()
  callbacks.length = 0
  for (const callback of copies) callback()
}

let timerFunc
if (typeof Promise !== 'undefined') {
  const resolved = Promise.resolve()
  timerFunc = () => resolved.then(flushCallbacks)
} else if (typeof MutationObserver !== 'undefined') {
  // 真实源码通过修改文本节点触发 MutationObserver。
  timerFunc = () => setTimeout(flushCallbacks, 0)
} else {
  timerFunc = () => setTimeout(flushCallbacks, 0)
}

function nextTick(callback) {
  callbacks.push(callback)
  if (!pending) {
    pending = true
    timerFunc()
  }
}

这是帮助理解的伪代码,不是完整 Vue 2 源码;真实实现还处理异常、Promise 返回值、iOS 兼容和环境判断。

5.3 Vue 3 的 nextTick 和 scheduler

Vue 3 的实现核心更接近:

const resolvedPromise = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

function nextTick(callback?: () => void) {
  const promise = currentFlushPromise || resolvedPromise
  return callback ? promise.then(callback) : promise
}

组件更新 job 会进入 scheduler 队列,执行过程中还会处理 pre-flush 和 post-flush 回调。nextTick 等待的是当前 flush promise;如果当前没有待处理更新,它通常只是等待一个已解决 Promise 的后续微任务。

Vue 3 watcher 还支持:

watch(source, callback, { flush: 'pre' })   // 默认,组件更新前
watch(source, callback, { flush: 'post' })  // DOM 更新后
watch(source, callback, { flush: 'sync' })  // 同步触发,谨慎使用

flush: 'post' 对“状态变化后读取 DOM”的 watcher 往往比在回调中再手动调用 nextTick 更直接。

六、Vue 能不能同步渲染

Vue 2 有 Vue.config.async = false 和 watcher 内部 sync 等历史调试/配置路径,但它们不是普通业务代码的推荐方案,可能破坏批处理、增加重复渲染和引入难以定位的更新顺序问题。

Vue 3 没有对应的全局 Vue.config.async 公共 API。若确实需要同步执行某个 watcher,可以使用 flush: 'sync',但它容易在循环、批量数组变更和递归更新中造成大量执行:

watch(source, callback, { flush: 'sync' })

大多数场景应该依赖默认批处理,并在需要访问 DOM 时使用 nextTickflush: 'post'

七、Vue Router 的核心原理

路由可以理解为 URL 状态与组件树之间的映射。SPA 中切换路由通常不会重新请求整个 HTML 文档,而是由路由器监听 URL 变化、匹配路由记录、加载组件并更新当前路由状态。

7.1 Vue Router 3、4 和 5 的 API

Vue 2 常见写法:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
  ],
})

Vue 3 及 Vue Router 4/5 使用工厂函数:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./Home.vue') },
  ],
})

app.use(router)

Vue Router 4/5 需要把 history 实现作为 history 传入;Nuxt 4 通常使用文件系统路由,不需要在页面应用里手写完整 createRouter 配置。

7.2 路由模式

  • hash:URL 中 # 后的部分变化,通常不会发送给服务器,部署简单;
  • history:使用 pushStatereplaceStatepopstate,URL 更自然,但服务器必须把未知路径回退到应用入口;
  • memory:在测试、SSR 或没有浏览器地址栏的环境使用内存 history。

改变 history URL 不会自动请求完整页面,但刷新、直接访问深层路径和服务端回退配置仍然需要考虑。

7.3 导航守卫

现代 Vue Router 推荐用返回值决定导航:

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

router.beforeResolve(async (to) => {
  if (to.meta.requiresCamera) {
    await askForCameraPermission()
  }
})

router.afterEach((to, from, failure) => {
  if (!failure) sendPageView(to.fullPath)
})

beforeResolve 在异步路由组件和组件内守卫解析后、导航确认前调用,适合执行只有在确认能进入页面时才需要的操作。afterEach 不能取消导航,但可以接收导航失败信息。

Vue Router 3/4 仍支持带 next 的旧式守卫,但现代代码更推荐每条控制路径返回一次结果:

router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) return '/login'
  return true
})

7.4 完整导航过程

常见流程可以概括为:

  1. 导航被触发;
  2. 调用失活组件的 beforeRouteLeave
  3. 调用全局 beforeEach
  4. 调用复用组件的 beforeRouteUpdate
  5. 调用路由记录的 beforeEnter
  6. 解析异步路由组件;
  7. 调用被激活组件的 beforeRouteEnter
  8. 调用全局 beforeResolve
  9. 导航确认;
  10. 调用全局 afterEach
  11. 触发 DOM 更新;
  12. Vue Router 3 的 beforeRouteEnter 回调在组件实例创建后执行。

Vue Router 4/5 中还可以在 Composition API 中使用 onBeforeRouteUpdateonBeforeRouteLeave,并不要求组件必须是路由组件。

八、手写一个简化 Vue Router

手写路由只能帮助理解,不应替代 Vue Router 的完整实现。它至少需要保存当前 URL、匹配路由、监听浏览器事件和渲染组件。

8.1 Vue 2 插件安装

Vue 2 插件通过 Vue.use() 调用 install

let Vue

class MiniRouter {
  constructor(options = {}) {
    this.mode = options.mode || 'hash'
    this.routes = options.routes || []
    this.routeMap = this.createMap(this.routes)
    this.current = this.getCurrentPath()
  }

  createMap(routes) {
    return routes.reduce((map, route) => {
      map[route.path] = route.component
      return map
    }, Object.create(null))
  }

  getCurrentPath() {
    if (this.mode === 'history') {
      return window.location.pathname
    }
    return window.location.hash.slice(1) || '/'
  }
}

MiniRouter.install = function (VueConstructor) {
  Vue = VueConstructor

  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this
        this._router = this.$options.router
      } else {
        this._routerRoot = this.$parent?._routerRoot
      }
    },
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot?._router
    },
  })
}

真实 Vue Router 还要实现响应式 current route、router-linkrouter-view、导航守卫、异步组件、滚动行为和错误处理。原文只给出了固定渲染“首页”的组件,这可以作为插件注入的第一步,但还不是可用路由器。

8.2 Vue 3 插件安装

Vue 3 不再修改全局 Vue 构造函数,而是通过应用实例安装:

const router = {
  install(app) {
    app.provide('router', router)
    app.config.globalProperties.$router = router
  },
}

app.use(router)

实际实现通常使用 shallowRef 保存 currentRoute,并在 pushState/popstate 后更新它;router-view 根据匹配记录渲染组件,router-link 使用 router.push 并阻止默认跳转。

九、Vue 模板编译原理

浏览器不能直接理解 Vue 的 {{ }}v-ifv-for 等模板语法。使用构建工具时,SFC 通常在构建阶段被编译成 render function;运行时构建版本则可能包含模板编译器。

9.1 Vue 2 的经典三阶段

parse:模板字符串 → AST
optimize:标记静态节点
generate:AST → render 函数字符串

Vue 2 的静态节点优化可以让后续更新跳过确定不会改变的部分,但组件、插槽和动态指令仍然需要按运行时规则处理。

9.2 Vue 3 的编译流程

Vue 3 compiler-core 大致包含:

baseParse
  → transform / directive transforms
  → generate
  → render function

编译器会为运行时提供更多信息:

  • 静态节点提升;
  • patch flags 标记动态文本、class、style、props 等;
  • Block Tree 收集动态子节点;
  • 事件处理器和静态属性缓存;
  • v-ifv-for、插槽和组件生成对应运行时代码。

这就是为什么 Vue 3 的 VDOM 不能简单理解为“每次把完整树拿来做通用 Diff”:编译器会告诉 runtime 哪些地方可能变化。

十、面试总结

回答 Vue 原理时,建议先说清版本和边界:

  1. Vue 2 的响应式核心是 ObserverDepWatcherObject.defineProperty
  2. Vue 3 使用 Proxytrack/triggerReactiveEffect 和 scheduler;
  3. computed 基于响应式依赖缓存,watch 用于副作用;
  4. nextTick 等待当前 DOM 更新 flush,不是等待任意异步任务;
  5. Vue 2/3 的更新都经过 render 和 patch,但 Vue 3 有 compiler-informed 优化;
  6. Vue Router 3 使用 new VueRouter,4/5 使用 createRouter
  7. hash/history 是 URL 与服务器回退策略不同的两种模式;
  8. 模板编译大致经历解析、转换/优化和代码生成;
  9. Vue 3 组件推荐 Composition API、Pinia、Vue Router 4/5,Nuxt 4 还提供服务端渲染和数据获取能力;
  10. 不要把“Vue 是双向绑定”“Proxy 解决一切问题”“VDOM 一定更快”当成完整答案。

参考资料

原文作者:程序员 Better。原文关于 Vue/React 对比、Vue 2 响应式、computed、nextTick、Vue Router 插件、导航守卫和模板编译的主线予以保留;抓取成行的源码、错误拼写、无效示例、Vue 2/3 混用和不准确的绝对结论已整理或修正。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS