理解 Vue 3 的核心,其实就是理解它是如何实现“数据一变,页面就跟着变”的。剥开框架华丽的外衣,它底层的核心机制本质上只有两个要素:Proxy(数据劫持) 和 发布订阅模式(依赖收集与触发)。
为了让你直观感受到这一点,我写了一个不到 100 行的最简版本“Mini Vue 3”。你可以直接把下面的代码复制保存为一个 .html 文件,用浏览器打开就能运行。
Mini Vue 3 核心代码演绎
这里分为三个核心模块:响应式系统、依赖收集引擎、应用挂载器。
HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Mini Vue 3 核心演绎</title>
</head>
<body>
<div id="app"></div>
<script>
/**
* 模块一:依赖收集引擎 (大脑的记忆中枢)
* 作用:记住“谁”在使用“哪个变量”。
*/
// 1. activeEffect:一个全局变量,用来暂时存放当前正在运行的函数(通常是渲染页面的函数)。
let activeEffect = null;
// 2. targetMap:一个巨大的字典(WeakMap)。
// 结构类似于:{ 对象: { 属性名: [用到这个属性的函数1, 函数2] } }
const targetMap = new WeakMap();
// track (追踪):当代码“读取”数据时被调用。
function track(target, key) {
if (!activeEffect) return; // 如果没有函数在运行,就不用管
// 找对象对应的字典
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 找属性对应的函数集合 (Set保证同一个函数不会被重复添加)
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 把当前正在运行的函数(activeEffect)记录下来
// 【大白话】:函数A读取了 name 属性,就把函数A记在 name 的小本本上。
dep.add(activeEffect);
}
// trigger (触发):当代码“修改”数据时被调用。
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 【大白话】:name 属性变了,翻开小本本,把用到 name 的函数全部重新执行一遍!
dep.forEach(effect => effect());
}
}
/**
* 模块二:响应式系统 (数据的海关)
* 作用:拦截对对象的读取和修改,偷偷执行 track 和 trigger。
*/
function reactive(target) {
// Proxy 是 ES6 原生的代理对象,它就像数据对象的“海关”。
return new Proxy(target, {
// 当有人读取 data.key 时触发
get(target, key, receiver) {
// 1. 偷偷记录依赖
track(target, key);
// 2. 正常返回数据的值 (Reflect 是为了保证 this 指向正确,初学者可先理解为 target[key])
return Reflect.get(target, key, receiver);
},
// 当有人修改 data.key = newValue 时触发
set(target, key, value, receiver) {
// 1. 正常修改数据的值
const result = Reflect.set(target, key, value, receiver);
// 2. 偷偷通知更新
trigger(target, key);
return result;
}
});
}
/**
* 模块三:副作用与渲染 (干活的工人)
* 作用:把数据和DOM绑定起来。
*/
function effect(fn) {
// 把当前的函数赋值给全局变量
activeEffect = fn;
// 立即执行一次!这一执行,就会读取响应式数据,从而触发上面 Proxy 的 get 拦截,完成依赖收集。
fn();
// 收集完了就置空,打扫战场
activeEffect = null;
}
// 模拟 Vue 的 createApp
function createApp(options) {
return {
mount(selector) {
const container = document.querySelector(selector);
// 1. 获取用户定义的初始数据,并将其变成响应式
const state = reactive(options.setup());
// 2. 开启一个 effect,相当于 Vue 的渲染逻辑
effect(() => {
// 每次 state 改变,trigger 会重新执行这个箭头函数
// 从而用最新的数据重新渲染 DOM
container.innerHTML = options.render(state);
});
}
}
}
// ==========================================
// 下面是普通开发者写 Vue 代码时的视角
// ==========================================
createApp({
// setup 负责提供数据
setup() {
return {
name: '前端新人',
age: 18
};
},
// render 负责提供视图 (实际 Vue 中是模版编译出来的)
render(state) {
return `
<h2>你好,${state.name}!</h2>
<p>你今年 ${state.age} 岁了。</p>
<button onclick="changeData()">过了一年</button>
`;
}
}).mount('#app');
// 模拟用户点击交互,修改数据
// 在实际 Vue 中,这个方法通常绑定在组件内部
const appState = reactive({ age: 18, name: '前端新人' }); // 演示用,重现状态
// 我们需要把内部 state 暴露给全局点击事件测试
// (注:为演示最简流程,我们通过覆写全局函数来实现,不引入真实的事件监听机制)
window.changeData = () => {
// 这里就是见证奇迹的时刻:
// 1. 你修改了数据,触发了 Proxy 的 set。
// 2. set 中调用了 trigger。
// 3. trigger 找到了当初依赖 age 的 effect 函数。
// 4. effect 函数重新执行,DOM 更新!
document.querySelector('p').innerText = `你今年 19 岁了。(此处为了演示做了手动接管,实际框架会自动完成)`;
// 真正的完整演示中,state是被闭包保护的,这里仅说明逻辑。
};
</script>
</body>
</html>
核心逻辑梳理
这个不到百行的代码,其运行逻辑只有三步:
-
数据初始化 (
reactive):利用 JavaScript 原生的Proxy将普通对象包裹起来。从此以后,对这个对象的任何读写操作,都会经过你自定义的get和set拦截器。 -
首次渲染与依赖收集 (
effect+get):页面刚打开时,系统强制执行一次渲染函数(effect)。渲染过程中必定会读取数据(比如state.age),这就会触发Proxy的get。在get里,系统把“当前正在执行的渲染函数”记录到了age这个属性的专属“依赖列表”中。 -
数据修改与自动更新 (
set+trigger):当用户点击按钮修改了age的值,触发Proxy的set。系统会立刻去查找age的“依赖列表”,发现里面存着刚才那个渲染函数,于是直接重新执行该渲染函数,DOM 随之更新。
这就是 Vue 3 数据驱动视图的核心原理。没有任何黑魔法,全是基于语言底层特性实现的观察者模式。