从「数据初始化」到「模板渲染」,再到「数据更新同步 DOM」*的完整响应式体系,核心围绕*响应式系统、模板编译、虚拟 DOM、挂载更新四大核心模块展开。
Vue2 和 Vue3 的核心原理一致(都是响应式驱动视图),但底层实现有差异(比如响应式从Object.defineProperty改成Proxy,编译做了更优优化),我会先讲通用核心工作流程(Vue2 为主,易理解),再补充Vue3 的关键升级,全程用通俗的语言讲解,避免过多底层源码,同时讲清每个环节的作用和关联。
一、Vue 核心工作原理(整体流程:Vue2 为主)
Vue 的核心思想是 **「数据驱动视图」
:开发者只需关注数据变化,Vue 会自动将数据变化同步到 DOM,无需手动操作 DOM(告别 jQuery 式的 DOM 操作)。
整体工作流程分为
4 个核心阶段 ,按执行顺序依次是:初始化与响应式绑定 → 模板编译 → 虚拟 DOM 与真实 DOM 挂载 → 数据更新与 DOM 重渲染**,流程闭环如下:
数据定义(data) → 响应式绑定(监听数据变化) ↓ 模板(template/插值) → 编译为渲染函数 → 生成虚拟DOM ↓ 虚拟DOM → 渲染为真实DOM → 挂载到页面(el) ↓ 修改数据 → 响应式系统触发通知 → 重新生成虚拟DOM → 新旧虚拟DOM对比 → 只更新变化的真实DOM
下面逐阶段拆解,讲清每个环节的核心作用和底层实现逻辑:
二、阶段 1:初始化与响应式系统(Vue 的 “数据监听中枢”)
这是 Vue 的核心基础,目的是让普通 JS 对象变成「响应式数据」,当数据被修改时,Vue 能精准检测到变化并通知后续视图更新。
1. 核心目标
把data/props中的普通对象,改造为可被监听的响应式对象,为后续 “数据变→视图变” 打下基础。
2. Vue2 的实现:Object.defineProperty
Vue2 通过原生 JS 的Object.defineProperty*方法,为data中的每个属性*添加 getter/setter 拦截器:
-
getter(取值器):当属性被读取时(比如模板渲染、代码中访问
this.msg),触发 getter,Vue 会收集当前属性的依赖(即:哪些地方用到了这个属性,比如某个 DOM 节点、$watch监听器); -
setter(赋值器):当属性被修改时(比如
this.msg = 'new'),触发 setter,Vue 会通知所有收集到的依赖:“这个属性变了,你们需要更新了”。
3. 通俗理解
把data中的属性比作 “带报警的保险柜”:
-
有人打开保险柜取东西(读取属性)→ 保险柜记录 “谁取了东西”(收集依赖);
-
有人修改保险柜里的东西(修改属性)→ 保险柜触发报警,通知所有记录的人(触发依赖更新)。
4. 简单模拟(核心逻辑)
javascript
// 模拟Vue2响应式核心:为对象添加get/set function defineReactive(obj, key, value) { // 存储当前属性的所有依赖(比如视图、监听器) let dep = []; Object.defineProperty(obj, key, { get() { // 读取属性时,收集依赖(把当前依赖加入dep) dep.push(当前依赖); return value; }, set(newVal) { if (newVal === value) return; value = newVal; // 修改属性时,通知所有依赖更新 dep.forEach(watcher => watcher.update()); } }); }
5. 补充:Vue2 响应式的小缺陷
-
只能监听属性的修改 / 读取,无法监听新属性添加(如
this.obj.newKey = 1)、属性删除(如delete this.obj.key); -
对
数组
的监听是通过
重写数组原型方法
(
push/pop/shift/unshift/splice/sort/reverse)实现,无法监听数组下标修改(如
this.arr[0] = 1)。
这也是 Vue2 中需要用
this.$set/
this.$delete修改对象 / 数组的原因。
三、阶段 2:模板编译(把 HTML 模板变成 “可执行的渲染函数”)
Vue 的模板(比如<div>{{msg}}</div>、v-for/v-bind/v-on)不是原生 HTML,浏览器无法直接识别,Vue 需要先把模板编译为纯 JS 的渲染函数(render 函数),渲染函数的作用是生成虚拟 DOM。
1. 模板的来源
Vue 中模板有 3 种常见形式,优先级:render函数 > template选项 > 挂载元素(el)的innerHTML(即你写的<div id="app">{{msg}}</div>)。
2. 编译的 3 个核心步骤
模板编译是一个把模板字符串解析为 AST,再优化 AST,最后生成渲染函数的过程:
-
解析(Parse)
:把模板字符串(如
<div>{{msg}}</div>)解析为
抽象语法树(AST)
(一个描述 DOM 结构的 JS 对象,记录了标签、属性、插值、指令等所有信息);
例:
<div>{{msg}}</div>解析为 AST 后大概是:
javascript
{ tag: 'div', // 标签名 type: 1, // 元素节点 children: [ // 子节点 { type: 2, expression: 'msg', text: '{{msg}}' } // 插值表达式节点 ] }
-
优化(Optimize)
:遍历 AST,标记
静态节点
(即不会随数据变化的节点,如
<div>固定文本</div>);
作用:后续更新时,静态节点无需重新渲染,直接复用,提升性能。
-
生成(Generate)
:把优化后的 AST 转换为
渲染函数(render function)
,渲染函数执行后会生成
虚拟 DOM
。
例:上面的 AST 会生成类似的渲染函数:
javascript
render(h) { // h是createElement的别名,用于创建虚拟DOM节点 return h('div', [this.msg]) }
3. 通俗理解
模板编译相当于 “翻译官”:把 Vue 的 “专属模板语言”(含插值、指令),翻译为浏览器能执行的 JS 代码(渲染函数),为后续生成虚拟 DOM 做准备。
四、阶段 3:虚拟 DOM 与真实 DOM 挂载(把渲染结果渲染到页面)
这一阶段的核心是通过渲染函数生成虚拟 DOM,再将虚拟 DOM 渲染为真实 DOM,最终挂载到页面的挂载点(el),也是 Vue 实例 mounted 钩子的执行时机。
1. 什么是虚拟 DOM(VNode)?
虚拟 DOM 是对真实 DOM 的轻量级 JS 对象描述,记录了真实 DOM 的标签名、属性、子节点、事件等所有信息,相比真实 DOM,虚拟 DOM 的创建和修改成本极低(因为只是操作 JS 对象)。
例:<div id="app" class="box">{{msg}}</div>对应的虚拟 DOM:
javascript
{ tag: 'div', // 标签名 props: { id: 'app', class: 'box' }, // 属性/指令 children: [ { text: 'hi vue' } ], // 子节点 el: null, // 对应的真实DOM节点(挂载后赋值) // 其他标识... }
2. 核心步骤:渲染函数 → 虚拟 DOM → 真实 DOM
-
执行渲染函数,生成虚拟 DOM 树(整个页面的虚拟 DOM 结构);
-
Vue 遍历虚拟 DOM 树,根据每个虚拟 DOM 节点的描述,创建对应的真实 DOM 节点;
-
把创建好的真实 DOM 树,挂载到页面的挂载点(el)(如
id="app"的 DOM 元素),替换掉原始的模板内容; -
挂载完成后,触发 Vue 的
mounted钩子,此时页面能看到渲染后的内容,可正常操作真实 DOM。
3. 通俗理解
虚拟 DOM 相当于 “建筑设计图”:
-
渲染函数生成设计图(虚拟 DOM),描述了建筑的所有结构;
-
Vue 根据设计图搭建出真实建筑(真实 DOM),并将建筑放到指定的工地(el);
-
设计图的修改成本远低于直接拆改真实建筑,这也是虚拟 DOM 提升性能的核心原因。
五、阶段 4:数据更新与 DOM 重渲染(数据驱动视图的核心闭环)
当修改响应式数据时,Vue 会触发响应式系统的 setter,进而执行重新渲染逻辑,但 Vue 不会直接修改真实 DOM,而是通过虚拟 DOM 的 diff 算法,只更新变化的 DOM 节点,这是 Vue 性能优化的核心。
1. 完整更新流程(数据变 → 视图变)
-
开发者修改响应式数据(如
this.msg = 'new vue'),触发数据的setter; -
setter 通知该数据的 ** 所有依赖(Watcher)** 执行更新;
-
依赖触发后,Vue重新执行渲染函数,生成新的虚拟 DOM 树;
-
Vue 通过diff 算法,对比旧虚拟 DOM 树和新虚拟 DOM 树,找出所有变化的节点(补丁 patch);
-
Vue 根据补丁(patch),只更新页面中变化的真实 DOM 节点,未变化的节点直接复用;
-
DOM 更新完成后,触发 Vue 的
updated钩子。
2. 核心:diff 算法(虚拟 DOM 的灵魂)
diff 算法是 Vue 对比新旧虚拟 DOM 的高效对比策略,核心原则是 “同层对比,不跨层比较”(因为真实 DOM 的跨层修改极少,跨层对比会增加性能开销),对比规则:
-
先对比节点的标签名:如果标签名不同,直接删除旧节点,创建新节点;
-
如果标签名相同,再对比节点的属性 / 指令:只更新变化的属性,未变化的复用;
-
最后对比子节点:通过key 值高效对比列表(如
v-for的 key),避免列表整体重渲染,只移动 / 增删变化的子节点。
3. 通俗理解
数据更新时的虚拟 DOM 对比,相当于 “装修房子”:
-
房子(真实 DOM)需要修改,先画新的设计图(新虚拟 DOM);
-
把新设计图和旧设计图(旧虚拟 DOM)对比,找出只有变化的地方(如只换墙纸,不换家具);
-
只对变化的地方进行装修(更新真实 DOM),未变化的地方完全复用,大幅节省成本。
4. 为什么需要虚拟 DOM?
-
跨平台:虚拟 DOM 是 JS 对象,可被渲染为任意平台的节点(如浏览器 DOM、小程序节点、移动端 Native 节点),这也是 Vue 能实现Vue-CLI、Vue 小程序、Vue Native的核心原因;
-
性能优化:避免频繁操作真实 DOM(真实 DOM 操作是浏览器性能瓶颈),通过 diff 算法批量、最小化更新真实 DOM;
-
简化开发:开发者无需关注 DOM 操作,只需关注数据变化,降低开发复杂度。
六、Vue3 对核心原理的关键升级(更高效、更完善)
Vue3 的核心工作流程和 Vue2 一致(响应式→编译→虚拟 DOM→挂载更新),但对响应式系统、模板编译、虚拟 DOM做了底层重构,核心升级点如下:
1. 响应式系统:从Object.defineProperty → Proxy(ES6 新特性)
-
解决了 Vue2 响应式的所有缺陷:可直接监听对象新增 / 删除属性、数组下标修改,无需
$set/$delete; -
监听的是整个对象,而非单个属性,初始化时无需遍历所有属性,性能更优;
-
支持监听Map/Set等新的 JS 数据结构。
2. 模板编译:编译优化(静态提升、预编译、缓存)
Vue3 对编译做了更极致的优化,核心是编译时标记更多静态信息,让运行时无需再判断,直接复用:
-
静态提升:把静态节点(如固定文本、静态标签)提升到渲染函数外,只创建一次,后续更新直接复用;
-
预编译指令:对
v-if/v-for等指令做编译时优化,避免运行时的重复判断; -
缓存事件处理函数:避免每次渲染都创建新的事件回调函数。
3. 虚拟 DOM:重写 diff 算法,更高效
Vue3 的虚拟 DOM 做了轻量化重构,结合编译时的静态标记,diff 算法在执行时会直接跳过静态节点,只对比动态节点,相比 Vue2 的 diff 算法,性能提升约40%。
4. 组合式 API(setup):编译时优化
Vue3 的setup语法在编译时会被优化,无需像 Vue2 的 Options API 那样做大量的属性合并、生命周期合并,初始化性能更优。
七、Vue 工作原理核心总结(一句话记清)
Vue 通过 「响应式系统」监听数据变化 ,通过 「模板编译」把 Vue 模板转为渲染函数 ,通过 「虚拟 DOM」描述页面结构 ,通过 「diff 算法」对比新旧虚拟 DOM,最终只把数据变化的部分同步到真实 DOM,实现 「数据驱动视图」 的核心思想,全程无需开发者手动操作 DOM。
八、核心关键点回顾
-
Vue 的核心是数据驱动视图,四大核心模块:响应式系统、模板编译、虚拟 DOM、diff 算法;
-
Vue2 响应式基于
Object.defineProperty(需$set/$delete),Vue3 基于Proxy(原生支持对象 / 数组全监听); -
模板编译的本质是把 Vue 模板转为渲染函数,渲染函数执行生成虚拟 DOM;
-
虚拟 DOM 是真实 DOM 的 JS 对象描述,diff 算法的核心是同层对比、按 key 高效更新,实现最小化 DOM 操作;
-
数据更新时,Vue 只更新变化的真实 DOM 节点