
一、前言
Vue3 是前端开发中最为常用的框架之一,其简洁的语法和高效的响应式系统深受开发者喜爱。那么,Vue3 究竟是如何实现的?今天,我们就来"拆轮子"------用 XXX 行原生 JavaScript,从零手搓一个简版 Vue3 框架。响应式怎么实现?依赖是如何收集?组件又该怎样渲染?虚拟 DOM 是怎么样的?通过简版 Vue3 框架的实现,深入剖析 Vue3 的核心原理,这些问题都能逐一被解答。
完整版(含注释和示例)源码请移步 Github 👉 Vue-Like
二、代码实现
2.1 响应式系统和依赖追踪
Vue 3 的响应式系统是其核心功能之一,通过 Proxy API 实现了对对象的响应式处理。与 Vue 2 的 defineProperty 方法相比,Proxy 提供了更强大的拦截能力,能够支持对对象的嵌套属性、数组等进行响应式处理。
javascript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发依赖
}
return res;
},
});
}
reactive
函数的作用是将一个普通对象转换为响应式对象。我们可以通过 Proxy API 对对象的读取和设置操作进行拦截,从而实现响应式功能。在获取值的场景下收集依赖,在设置值的场景下触发依赖。
javascript
function track(target, key) {
if (!effectStack.length) return;
let depsMap = reactiveMap.get(target);
if (!depsMap) {
depsMap = new Map();
reactiveMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
const effect = effectStack[effectStack.length - 1];
dep.add(effect);
effect.deps.push(dep);
}
track
函数的作用是收集依赖。当一个响应式对象的属性被读取时,我们需要将当前的副作用函数(effect)与该属性关联起来,以便在属性值发生变化时能够触发更新。⚠️ 注意,所有的响应依赖关系通过 WeakMap
数据结构存储,reactiveMap
的键是响应式对象,值是一个 Map
,其中的键是对象的属性名,值是一个 Set
,存储了与该属性相关的副作用函数。
javascript
function trigger(target, key) {
const depsMap = reactiveMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
// 创建副本避免无限循环
const effects = new Set(dep);
effects.forEach((effect) => {
if (effect.scheduler) {
effect.scheduler();
} else {
effect();
}
});
}
trigger
函数的作用是触发依赖更新。当一个响应式对象的属性值发生变化时,我们需要通知所有与该属性相关的副作用函数重新执行。⚠️ 注意,执行副作用函数遍历前需要创建副本避免无限循环,因为执行副作用函数的时候可能会导致 dep
内容发生变化,导致 forEach
会无限执行下去。
javascript
// 基于 reactive 实现
function ref(value) {
return reactive({
value,
__v_isRef: true,
})
}
// 基于 proxy 实现
function ref(value) {
const r = {
get value() {
track(r, "value");
return value;
},
set value(newVal) {
if (newVal !== value) {
value = newVal;
trigger(r, "value");
}
},
__v_isRef: true,
};
return r;
}
ref
函数的作用是创建一个响应式的引用对象。它通常用于处理基本数据类型的响应式需求。最简单的方式是通过 reactive
来创建,但是这种方式会导致产生很多多余的依赖跟踪,因此也可以单独基于 proxy
实现。
2.2 副作用函数
javascript
function effect(fn, scheduler) {
const effectFn = () => {
try {
effectStack.push(effectFn);
cleanupEffect(effectFn);
fn();
} finally {
effectStack.pop();
}
};
effectFn.scheduler = scheduler;
effectFn.deps = [];
effectFn();
}
副作用函数是响应式系统中的一个重要概念。它通常用于执行与响应式对象相关的操作,例如更新 DOM 或执行计算属性的逻辑等。在 effect
函数中,我们将传入的函数包装为一个副作用函数,并将其压入 effectStack
。在执行副作用函数时,会收集依赖,并在属性值发生变化时重新执行函数触发更新。
2.3 虚拟 DOM
虚拟 DOM( VDOM )是一种轻量级 JS 数据格式,它旨在将渲染逻辑与实际的 DOM 解耦,提高 DOM 操作性能。通过将真实 DOM 转换为虚拟 DOM,我们可以在内存中进行高效的比较和更新操作,从而减少对真实 DOM 的直接操作。虚拟 DOM 由虚拟节点组成,其包含三部分内容,tag
、props
和children
,类似于这样:
JSON
{
"tag": "div",
"props": {
"class": "container"
},
"children": [
{
"tag": "h1",
"props": {},
"children": "Hello World"
},
{
"tag": "p",
"props": {},
"children": "I'm Sherwin."
}
]
}
相当于这个 HTML 代码:
html
<div class="container">
<h1>Hello World</h1>
<p>I'm Sherwin.</p>
</div>
创建和渲染虚拟 DOM 主要涉及以下几个函数:
h
函数:创建一个虚拟节点;render
函数负责将虚拟 DOM 渲染到容器中;mount
函数用于挂载新的虚拟 DOM;patch
函数用于比较新旧虚拟 DOM 的差异并进行更新
代码内容较多不再一一贴出,可移步阅读完整版代码 👉 Vue-Like。注意,代码中 patch
函数仅提供了简易版 diff 算法,实际 patch 中的 diff 算法复杂得多,详见源码,尤其关注最长递增子序列算法的应用,详见源码。
2.4 组件渲染
javascript
function createComponent(vnode) {
const component = vnode.tag;
const props = vnode.props || {};
const ctx = component.setup(props);
const container = document.createElement("div");
const updateComponent = () => {
console.log("子组件渲染");
const combinedCtx = { ...ctx, ...props };
const subVnode = component.render(combinedCtx)();
if (vnode.component) {
patch(vnode.component.vnode, subVnode, container);
} else {
mount(subVnode, container);
vnode.component = { instance: ctx, vnode: subVnode, container };
}
vnode.component.vnode = subVnode;
};
vnode.el = container;
effect(updateComponent);
return container;
}
当虚拟 DOM 对应的 tag 不为字符串,即 DOM 标签,而是一个对象时,则认为是一个组件,需要创建对象对应的组件。子组件渲染是一个副作用函数,当其中的依赖发生改变时就需要重新执行渲染。
2.5 watch 实现
javascript
function watch(source, callback, options = {}) {
let getter;
if (typeof source === "function") {
getter = source;
} else if (source.__v_isRef) {
getter = () => source.value;
}
let oldValue;
// 调度函数
const job = () => {
const newValue = effectFn();
callback(newValue, oldValue);
oldValue = newValue;
};
const effectFn = () => getter();
effect(effectFn, job);
if (options.immediate) {
job();
}
}
watch
函数用于监听响应式对象的变化,并执行指定的回调函数。它通常用于执行副作用操作,例如发送网络请求或更新 DOM。在 watch
函数中,我们通过 effect
函数创建了一个副作用函数,用于监听响应式对象的变化。当响应式对象的值发生变化时,副作用函数会被触发,执行指定的回调函数。⚠️ 注意,在 watch 这种场景副作用函数使用了调度器 scheduler
。getter
函数用于变动依赖收集,但真正遇到变动时执行的副作用函数不再是 getter
函数,而是调度器 scheduler
。
2.6 computed 实现
javascript
function computed(getter) {
let dirty = true; // 脏检查标志
let value;
const effectFn = () => {
value = getter();
dirty = false;
};
effect(effectFn, () => {
dirty = true; // 当依赖变化时,标记为脏值,后续读取则会重新计算
});
const computedObj = {
get value() {
if (dirty) {
effectFn();
console.log("重新计算");
} else {
console.log("读取缓存");
}
return value;
},
};
return computedObj;
}
computed
函数用于创建计算属性。计算属性值是基于其依赖的响应式数据动态计算的。当依赖的响应式数据发生变化时,计算属性的值也会自动更新。在 computed
函数中,通过 effect
函数创建了一个副作用函数,用于计算计算属性的值。当依赖的响应式数据发生变化时,副作用函数会被触发,重新计算计算属性的值。计算属性的一个重要特性是缓存。计算属性的值只有在依赖的响应式数据发生变化时才会重新计算,否则直接返回缓存的值。这可以避免不必要的计算,提高性能。通过 dirty
标志,我们可以判断计算属性的值是否需要重新计算。
三、使用示例

javascript
/**
* 挂载容器
*/
const container = document.getElementById("app");
/**
* 子组件
*/
const Counter = {
setup(props) {
const count = ref(0);
const increment = () => count.value++;
const decrement = () => count.value > 0 && count.value--;
const reset = () => (count.value = 0);
const total = computed(() => Number(props.parentCount.value || 0) + count.value);
return {
count,
total,
increment,
decrement,
reset,
};
},
render(ctx) {
return () =>
h("div", { class: "component-container" }, [
h("h2", null, "Counter Component"),
h("div", { class: "value-display" }, `Parent Count: ${ctx.parentCount.value}`),
h("div", { class: "value-display" }, `Current Count: ${ctx.count.value}`),
h("div", { class: "value-display" }, `Total Count: ${ctx.total.value}`),
h("div", { class: "btn-group" }, [
h("button", { onClick: ctx.increment }, "Increment"),
h("button", { onClick: ctx.decrement }, "Decrement"),
h("button", { onClick: ctx.reset }, "Reset"),
]),
]);
},
};
/**
* 根组件
*/
const App = {
setup() {
// ref 场景
const count = ref(0);
// reactive 场景
const user = reactive({
name: { first: "Sherwin", last: "Williams" },
});
// computed 场景
const double = computed(() => count.value * 2);
const triple = computed(() => count.value * 3);
// 执行方法
const increment = () => count.value++;
const decrement = () => count.value > 0 && count.value--;
const changeName = () => {
const names = [
["Emma", "Smith"],
["Liam", "Johnson"],
];
const randomName = names[Math.floor(Math.random() * names.length)];
user.name.first = randomName[0];
user.name.last = randomName[1];
};
// watch 场景
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
watch(
() => user.name.first,
(newValue, oldValue) => {
console.log(`Name changed from ${oldValue} to ${newValue}`);
},
{ immediate: true }
);
return {
count,
user,
double,
triple,
increment,
decrement,
changeName,
};
},
render(ctx) {
return () =>
h("div", null, [
h("h1", null, "Vue-Like Framework"),
// User information section demonstrating reactive objects
h("div", { class: "card user-card" }, [
h("h2", null, "User Information"),
h("div", { class: "value-display" }, `Full Name: ${ctx.user.name.first} ${ctx.user.name.last}`),
h("div", { class: "btn-group" }, [h("button", { onClick: ctx.changeName }, "Change Random Name")]),
]),
// Counter with computed properties demonstration
h("div", { class: "card" }, [
h("h2", null, "Counter & Computed Properties"),
h("div", { class: "value-display" }, `Count: ${ctx.count.value}`),
h("div", { class: "value-display" }, `Double: ${ctx.double.value}`),
h("div", { class: "value-display" }, `Triple: ${ctx.triple.value}`),
h("div", { class: "btn-group" }, [
h("button", { onClick: ctx.increment }, "Increment"),
h("button", { onClick: ctx.decrement }, "Decrement"),
]),
]),
// Nested component example
// Demonstrates component composition and props passing
h(Counter, { parentCount: ctx.count }),
h("div", { class: "footer" }, "This demo shows a Vue-like framework."),
]);
},
};
createApp(App).mount(container);
四、总结
通过上述代码,我们实现了一个简版 Vue3 框架,虽然功能简单,但它涵盖了 Vue3 的核心原理,包括响应式系统、依赖追踪、副作用函数、虚拟 DOM、computed 和 watch 等内容。通过亲手实现这些功能,我们能够更深入地理解 Vue3 的工作原理,不再对 Vue3 的内部机制感到陌生。当然,这个简版 Vue3 框架距离真正的 Vue3 框架还有缺少非常多内容,如果感兴趣也可以尝试在我们的简版框架源码中进一步扩展和实现,欢迎评论完善,完整源码 👉 Vue-Like。