14-手写一个迷你 Vue 框架
手写一个迷你 Vue 框架
从 0 到 1 实现一个包含响应式系统、虚拟 DOM、编译器和组件系统的迷你 Vue 框架,深入理解 Vue 的核心原理。
一、前言
Vue.js 作为一个渐进式 JavaScript 框架,其核心设计包含响应式系统、虚拟 DOM、模板编译器和组件系统四大模块。通过手写一个迷你版本的 Vue 框架,我们可以深入理解这些核心机制的实现原理,以及它们之间是如何协同工作的。
本文将带领读者从零开始,逐步实现一个功能完整的迷你 Vue(命名为 MiniVue),支持响应式数据绑定、模板编译、虚拟 DOM diff 和组件化开发。
二、框架整体设计
2.1 架构概览
MiniVue 的整体架构分为四个核心层:
| 层级 | 职责 | 核心模块 |
|---|---|---|
| 编译层 | 将模板字符串编译为渲染函数 | Compiler |
| 响应式层 | 追踪依赖、触发更新 | Reactivity(reactive/ref/effect) |
| 运行时层 | 虚拟 DOM、diff 算法、patch | Runtime(h/patch) |
| 组件层 | 组件定义、生命周期、props/emit | Component |
2.2 模块依赖关系
Compiler(编译器)
↓ 输出 render 函数
Runtime(运行时)
← 调用 Reactivity(响应式系统)
↓ 输出真实 DOM
Component(组件系统)
← 组合 Compiler + Runtime + Reactivity
2.3 使用方式预览
最终实现的 MiniVue 使用方式如下:
const { createApp, reactive, ref, h } = MiniVue;
createApp({
setup() {
const state = reactive({ count: 0 });
const msg = ref('Hello MiniVue!');
const increment = () => state.count++;
return { state, msg, increment };
},
render() {
return h('div', null, [
h('h1', null, this.msg),
h('p', null, `Count: ${this.state.count}`),
h('button', { onClick: this.increment }, '+1')
]);
}
}).mount('#app');
三、实现响应式系统
3.1 核心设计思路
Vue3 的响应式系统基于 Proxy 实现,核心概念包括:
- target:被代理的原始对象
- depsMap:存储每个属性对应的依赖集合
- activeEffect:当前正在执行的副作用函数
- track:收集依赖(读取属性时触发)
- trigger:触发更新(修改属性时触发)
3.2 实现 effect 与依赖收集
// 当前活跃的 effect
let activeEffect = null;
const effectStack = [];
// 存储依赖关系: WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap();
/**
* 创建响应式 effect
* @param {Function} fn - 副作用函数
*/
function effect(fn) {
const effectFn = () => {
// 清理之前的依赖
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
try {
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
}
};
// 存储该 effect 依赖的所有 dep 集合
effectFn.deps = [];
effectFn();
return effectFn;
}
/**
* 清理 effect 的依赖关系
*/
function cleanup(effectFn) {
const deps = effectFn.deps;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn);
}
effectFn.deps.length = 0;
}
/**
* 追踪依赖 - 在 getter 中调用
*/
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);
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
/**
* 触发更新 - 在 setter 中调用
*/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 复制集合避免无限循环
const effectsToRun = new Set(dep);
effectsToRun.forEach(effectFn => effectFn());
}
}
3.3 实现 reactive
/**
* 创建响应式对象
* @param {Object} target - 原始对象
*/
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
// 递归处理嵌套对象
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
}
});
}
3.4 实现 ref
/**
* Ref 类 - 包装基本类型值
*/
class RefImpl {
constructor(value) {
this._value = value;
this.__v_isRef = true;
}
get value() {
track(this, 'value');
return this._value;
}
set value(newVal) {
if (newVal !== this._value) {
this._value = newVal;
trigger(this, 'value');
}
}
}
function ref(value) {
return new RefImpl(value);
}
/**
* 自动解包 ref
*/
function unref(val) {
return isRef(val) ? val.value : val;
}
function isRef(val) {
return val && val.__v_isRef === true;
}
3.5 响应式系统测试
// 测试 reactive
const state = reactive({ count: 0, nested: { value: 1 } });
effect(() => {
console.log('count:', state.count);
});
state.count++; // 输出: count: 1
state.count++; // 输出: count: 2
// 测试 ref
const num = ref(10);
effect(() => {
console.log('num:', num.value);
});
num.value = 20; // 输出: num: 20
四、实现虚拟 DOM
4.1 虚拟节点(VNode)设计
/**
* 创建虚拟节点
* @param {String|Object} type - 标签名或组件
* @param {Object|null} props - 属性对象
* @param {Array|String|null} children - 子节点
*/
function h(type, props = null, children = null) {
return {
type,
props,
children: children !== null
? Array.isArray(children)
? children
: [children]
: null,
el: null, // 对应的真实 DOM 元素
key: props?.key || null
};
}
4.2 创建真实 DOM
/**
* 根据 VNode 创建真实 DOM
*/
function createElement(vnode) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(String(vnode));
}
const el = document.createElement(vnode.type);
vnode.el = el;
// 设置属性
if (vnode.props) {
patchProps(el, {}, vnode.props);
}
// 递归创建子节点
if (vnode.children) {
vnode.children.forEach(child => {
el.appendChild(createElement(child));
});
}
return el;
}
/**
* 更新元素属性
*/
function patchProps(el, oldProps, newProps) {
// 移除旧属性
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith('on')) {
const event = key.slice(2).toLowerCase();
el.removeEventListener(event, oldProps[key]);
} else {
el.removeAttribute(key);
}
}
}
// 设置新属性
for (const key in newProps) {
const oldVal = oldProps[key];
const newVal = newProps[key];
if (oldVal !== newVal) {
if (key.startsWith('on') && typeof newVal === 'function') {
const event = key.slice(2).toLowerCase();
if (oldVal) {
el.removeEventListener(event, oldVal);
}
el.addEventListener(event, newVal);
} else if (key === 'class') {
el.className = newVal;
} else if (key === 'style' && typeof newVal === 'object') {
Object.assign(el.style, newVal);
} else {
el.setAttribute(key, newVal);
}
}
}
}
4.3 Diff 算法与 Patch
采用简单的双端比较 diff 算法:
/**
* 对比新旧 VNode 树,更新真实 DOM
*/
function patch(oldVNode, newVNode, container) {
// 首次渲染
if (!oldVNode) {
container.appendChild(createElement(newVNode));
return;
}
// 类型不同,直接替换
if (oldVNode.type !== newVNode.type) {
const newEl = createElement(newVNode);
container.replaceChild(newEl, oldVNode.el);
return;
}
// 文本节点
if (typeof newVNode === 'string' || typeof newVNode === 'number') {
if (oldVNode !== newVNode) {
oldVNode.el.textContent = String(newVNode);
}
newVNode.el = oldVNode.el;
return;
}
// 同类型元素,复用 DOM
const el = (newVNode.el = oldVNode.el);
// 更新属性
patchProps(el, oldVNode.props || {}, newVNode.props || {});
// 对比子节点
patchChildren(el, oldVNode.children, newVNode.children);
}
/**
* 对比子节点列表
*/
function patchChildren(parentEl, oldChildren, newChildren) {
if (!oldChildren) {
// 旧节点为空,直接添加所有新节点
if (newChildren) {
newChildren.forEach(child => {
parentEl.appendChild(createElement(child));
});
}
return;
}
if (!newChildren) {
// 新节点为空,移除所有旧节点
parentEl.innerHTML = '';
return;
}
// 简单 diff:遍历较短的数组
const len = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < len; i++) {
patch(oldChildren[i], newChildren[i], parentEl);
}
// 添加多余的新节点
if (newChildren.length > oldChildren.length) {
for (let i = len; i < newChildren.length; i++) {
parentEl.appendChild(createElement(newChildren[i]));
}
}
// 移除多余的旧节点
if (oldChildren.length > newChildren.length) {
for (let i = len; i < oldChildren.length; i++) {
parentEl.removeChild(oldChildren[i].el);
}
}
}
五、实现编译器
5.1 编译器设计
MiniVue 的编译器将 HTML 模板字符串编译为渲染函数。为简化实现,采用正则表达式解析:
/**
* 简单模板编译器
* 将模板字符串编译为渲染函数代码
*/
function compile(template) {
// 解析模板为 AST(简化版)
const ast = parseTemplate(template.trim());
// 生成渲染函数代码
const code = generate(ast);
// 创建渲染函数
return new Function('ctx', `with(ctx) { return ${code} }`);
}
5.2 模板解析
/**
* 解析模板为 AST 节点
*/
function parseTemplate(template) {
// 匹配开始标签: <div class="foo">
const startTagReg = /^<([a-zA-Z][a-zA-Z0-9-]*)(?:\s+([^>]*))?>/;
// 匹配结束标签: </div>
const endTagReg = /^<\/([a-zA-Z][a-zA-Z0-9-]*)>/;
// 匹配文本节点
const textReg = /^([^<]+)/;
// 匹配插值表达式: {{ variable }}
const interpolationReg = /\{\{\s*(.+?)\s*\}\}/g;
const stack = [];
let root = null;
let currentParent = null;
while (template.length > 0) {
// 处理开始标签
let match = template.match(startTagReg);
if (match) {
const tag = match[1];
const attrStr = match[2] || '';
const props = parseAttrs(attrStr);
const node = {
type: tag,
props,
children: []
};
if (!root) {
root = node;
}
if (currentParent) {
currentParent.children.push(node);
}
// 自闭合标签不需要入栈
if (!isSelfClosing(tag)) {
stack.push(node);
currentParent = node;
}
template = template.slice(match[0].length);
continue;
}
// 处理结束标签
match = template.match(endTagReg);
if (match) {
stack.pop();
currentParent = stack[stack.length - 1] || null;
template = template.slice(match[0].length);
continue;
}
// 处理文本节点
match = template.match(textReg);
if (match) {
const text = match[1].trim();
if (text) {
// 解析插值表达式
const tokens = parseInterpolation(text);
if (currentParent) {
currentParent.children.push(...tokens);
}
}
template = template.slice(match[0].length);
continue;
}
// 移除空白字符
template = template.trim();
}
return root;
}
/**
* 解析属性字符串
*/
function parseAttrs(attrStr) {
const props = {};
const attrReg = /([a-zA-Z-:]+)\s*=\s*["']([^"']*)["']/g;
let match;
while ((match = attrReg.exec(attrStr)) !== null) {
let key = match[1];
const val = match[2];
// 处理事件绑定: @click -> onClick
if (key.startsWith('@')) {
key = 'on' + key[1].toUpperCase() + key.slice(2);
}
// 处理属性绑定: :class -> class
else if (key.startsWith(':')) {
key = key.slice(1);
}
props[key] = val;
}
return Object.keys(props).length > 0 ? props : null;
}
/**
* 解析插值表达式
*/
function parseInterpolation(text) {
const tokens = [];
const reg = /\{\{\s*(.+?)\s*\}\}/g;
let lastIndex = 0;
let match;
while ((match = reg.exec(text)) !== null) {
if (match.index > lastIndex) {
tokens.push(text.slice(lastIndex, match.index));
}
tokens.push({ type: 'interpolation', expr: match[1] });
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(text.slice(lastIndex));
}
return tokens.length > 0 ? tokens : [text];
}
function isSelfClosing(tag) {
return ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tag);
}
5.3 代码生成
/**
* 根据 AST 生成渲染函数代码
*/
function generate(ast) {
if (!ast) return 'null';
if (typeof ast === 'string') {
return JSON.stringify(ast);
}
if (ast.type === 'interpolation') {
return `String(${ast.expr})`;
}
// 生成 h() 调用
const tag = JSON.stringify(ast.type);
const props = ast.props ? generateProps(ast.props) : 'null';
const children = generateChildren(ast.children);
return `h(${tag}, ${props}, ${children})`;
}
function generateProps(props) {
const entries = Object.entries(props).map(([key, val]) => {
// 事件处理
if (key.startsWith('on')) {
return `${JSON.stringify(key)}: ${val}`;
}
// 绑定属性
if (val.startsWith && val.startsWith('{{') && val.endsWith('}}')) {
const expr = val.slice(2, -2).trim();
return `${JSON.stringify(key)}: ${expr}`;
}
return `${JSON.stringify(key)}: ${JSON.stringify(val)}`;
});
return `{ ${entries.join(', ')} }`;
}
function generateChildren(children) {
if (!children || children.length === 0) return 'null';
const childCodes = children.map(child => generate(child));
if (childCodes.length === 1) {
return childCodes[0];
}
return `[${childCodes.join(', ')}]`;
}
六、实现组件系统
6.1 组件实例与生命周期
/**
* 创建组件实例
*/
function createComponentInstance(options) {
const instance = {
options,
props: {},
setupState: {},
render: null,
mounted: false,
isMounted: false,
subTree: null,
update: null
};
return instance;
}
/**
* 设置组件
*/
function setupComponent(instance) {
const { options } = instance;
// 执行 setup 函数
if (options.setup) {
const setupResult = options.setup();
if (typeof setupResult === 'object' && setupResult !== null) {
instance.setupState = setupResult;
}
}
// 编译模板为渲染函数
if (options.template && !options.render) {
instance.render = compile(options.template);
} else if (options.render) {
instance.render = options.render;
}
}
6.2 创建应用实例
/**
* 创建应用
*/
function createApp(rootComponent) {
const app = {
mount(selector) {
const container = typeof selector === 'string'
? document.querySelector(selector)
: selector;
const instance = createComponentInstance(rootComponent);
setupComponent(instance);
// 创建响应式更新函数
instance.update = effect(() => {
const proxy = createRenderProxy(instance);
const vnode = instance.render.call(proxy, proxy);
if (!instance.isMounted) {
// 首次挂载
patch(null, vnode, container);
instance.isMounted = true;
instance.subTree = vnode;
} else {
// 更新
patch(instance.subTree, vnode, container);
instance.subTree = vnode;
}
});
return app;
}
};
return app;
}
/**
* 创建渲染代理对象
* 用于在渲染函数中访问 setup 返回的数据
*/
function createRenderProxy(instance) {
return new Proxy(instance.setupState, {
get(target, key) {
// 优先从 setupState 获取
if (key in target) {
const val = target[key];
return isRef(val) ? val.value : val;
}
// 然后查找实例属性
if (key === '$props') return instance.props;
return undefined;
},
set(target, key, value) {
if (key in target) {
const val = target[key];
if (isRef(val)) {
val.value = value;
} else {
target[key] = value;
}
return true;
}
return false;
}
});
}
七、Mermaid 图表
MiniVue 整体架构
响应式系统数据流
虚拟 DOM Patch 流程
八、完整示例与测试
8.1 完整框架代码整合
// ============================================
// MiniVue - 迷你 Vue 框架
// ============================================
// ----- 响应式系统 -----
let activeEffect = null;
const effectStack = [];
const targetMap = new WeakMap();
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
try {
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
}
};
effectFn.deps = [];
effectFn();
return effectFn;
}
function cleanup(effectFn) {
const deps = effectFn.deps;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn);
}
effectFn.deps.length = 0;
}
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);
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
const effectsToRun = new Set(dep);
effectsToRun.forEach(effectFn => effectFn());
}
}
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
});
}
class RefImpl {
constructor(value) {
this._value = value;
this.__v_isRef = true;
}
get value() {
track(this, 'value');
return this._value;
}
set value(newVal) {
if (newVal !== this._value) {
this._value = newVal;
trigger(this, 'value');
}
}
}
function ref(value) {
return new RefImpl(value);
}
function isRef(val) {
return val && val.__v_isRef === true;
}
// ----- 虚拟 DOM -----
function h(type, props = null, children = null) {
return {
type,
props,
children: children !== null
? Array.isArray(children) ? children : [children]
: null,
el: null,
key: props?.key || null
};
}
function createElement(vnode) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(String(vnode));
}
const el = document.createElement(vnode.type);
vnode.el = el;
if (vnode.props) {
patchProps(el, {}, vnode.props);
}
if (vnode.children) {
vnode.children.forEach(child => {
el.appendChild(createElement(child));
});
}
return el;
}
function patchProps(el, oldProps, newProps) {
oldProps = oldProps || {};
newProps = newProps || {};
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
} else {
el.removeAttribute(key);
}
}
}
for (const key in newProps) {
const oldVal = oldProps[key];
const newVal = newProps[key];
if (oldVal !== newVal) {
if (key.startsWith('on') && typeof newVal === 'function') {
const event = key.slice(2).toLowerCase();
if (oldVal) el.removeEventListener(event, oldVal);
el.addEventListener(event, newVal);
} else {
el.setAttribute(key, newVal);
}
}
}
}
function patch(oldVNode, newVNode, container) {
if (!oldVNode) {
container.appendChild(createElement(newVNode));
return;
}
if (oldVNode.type !== newVNode.type) {
container.replaceChild(createElement(newVNode), oldVNode.el);
return;
}
if (typeof newVNode === 'string') {
if (oldVNode !== newVNode) {
oldVNode.el.textContent = String(newVNode);
}
newVNode.el = oldVNode.el;
return;
}
const el = (newVNode.el = oldVNode.el);
patchProps(el, oldVNode.props || {}, newVNode.props || {});
patchChildren(el, oldVNode.children, newVNode.children);
}
function patchChildren(parentEl, oldChildren, newChildren) {
if (!oldChildren) {
if (newChildren) {
newChildren.forEach(child => parentEl.appendChild(createElement(child)));
}
return;
}
if (!newChildren) {
parentEl.innerHTML = '';
return;
}
const len = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < len; i++) {
patch(oldChildren[i], newChildren[i], parentEl);
}
if (newChildren.length > oldChildren.length) {
for (let i = len; i < newChildren.length; i++) {
parentEl.appendChild(createElement(newChildren[i]));
}
}
if (oldChildren.length > newChildren.length) {
for (let i = len; i < oldChildren.length; i++) {
parentEl.removeChild(oldChildren[i].el);
}
}
}
// ----- 编译器(简化版)-----
function compile(template) {
const ast = parseTemplate(template.trim());
const code = generate(ast);
return new Function('ctx', `with(ctx) { return ${code}; }`);
}
function parseTemplate(template) {
const startTagReg = /^<([a-zA-Z][a-zA-Z0-9-]*)(?:\s+([^>]*))?>/;
const endTagReg = /^<\/([a-zA-Z][a-zA-Z0-9-]*)>/;
const textReg = /^([^<]+)/;
const stack = [];
let root = null;
let currentParent = null;
while (template.length > 0) {
let match = template.match(startTagReg);
if (match) {
const tag = match[1];
const props = parseAttrs(match[2] || '');
const node = { type: tag, props, children: [] };
if (!root) root = node;
if (currentParent) currentParent.children.push(node);
if (!['input', 'img', 'br'].includes(tag)) {
stack.push(node);
currentParent = node;
}
template = template.slice(match[0].length);
continue;
}
match = template.match(endTagReg);
if (match) {
stack.pop();
currentParent = stack[stack.length - 1] || null;
template = template.slice(match[0].length);
continue;
}
match = template.match(textReg);
if (match) {
const text = match[1].trim();
if (text && currentParent) {
const tokens = parseInterpolation(text);
currentParent.children.push(...tokens);
}
template = template.slice(match[0].length);
continue;
}
template = template.trim();
}
return root;
}
function parseAttrs(attrStr) {
const props = {};
const attrReg = /([a-zA-Z@:.-]+)\s*=\s*["']([^"']*)["']/g;
let match;
while ((match = attrReg.exec(attrStr)) !== null) {
let key = match[1];
const val = match[2];
if (key.startsWith('@')) {
key = 'on' + key[1].toUpperCase() + key.slice(2);
props[key] = val;
} else if (key.startsWith(':')) {
props[key.slice(1)] = val;
} else {
props[key] = val;
}
}
return Object.keys(props).length > 0 ? props : null;
}
function parseInterpolation(text) {
const tokens = [];
const reg = /\{\{\s*(.+?)\s*\}\}/g;
let lastIndex = 0;
let match;
while ((match = reg.exec(text)) !== null) {
if (match.index > lastIndex) {
tokens.push(text.slice(lastIndex, match.index));
}
tokens.push({ type: 'interpolation', expr: match[1] });
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(text.slice(lastIndex));
}
return tokens.length > 0 ? tokens : [text];
}
function generate(ast) {
if (!ast) return 'null';
if (typeof ast === 'string') return JSON.stringify(ast);
if (ast.type === 'interpolation') return `String(${ast.expr})`;
const tag = JSON.stringify(ast.type);
const props = ast.props ? generateProps(ast.props) : 'null';
const children = generateChildren(ast.children);
return `h(${tag}, ${props}, ${children})`;
}
function generateProps(props) {
const entries = Object.entries(props).map(([key, val]) => {
if (key.startsWith('on')) return `${JSON.stringify(key)}: ${val}`;
return `${JSON.stringify(key)}: ${JSON.stringify(val)}`;
});
return `{ ${entries.join(', ')} }`;
}
function generateChildren(children) {
if (!children || children.length === 0) return 'null';
const childCodes = children.map(child => generate(child));
return childCodes.length === 1 ? childCodes[0] : `[${childCodes.join(', ')}]`;
}
// ----- 组件系统 -----
function createComponentInstance(options) {
return {
options,
props: {},
setupState: {},
render: null,
isMounted: false,
subTree: null,
update: null
};
}
function setupComponent(instance) {
const { options } = instance;
if (options.setup) {
const setupResult = options.setup();
if (typeof setupResult === 'object' && setupResult !== null) {
instance.setupState = setupResult;
}
}
if (options.template && !options.render) {
instance.render = compile(options.template);
} else if (options.render) {
instance.render = options.render;
}
}
function createRenderProxy(instance) {
return new Proxy(instance.setupState, {
get(target, key) {
if (key in target) {
const val = target[key];
return isRef(val) ? val.value : val;
}
return undefined;
},
set(target, key, value) {
if (key in target) {
const val = target[key];
if (isRef(val)) {
val.value = value;
} else {
target[key] = value;
}
return true;
}
return false;
}
});
}
function createApp(rootComponent) {
const app = {
mount(selector) {
const container = typeof selector === 'string'
? document.querySelector(selector)
: selector;
const instance = createComponentInstance(rootComponent);
setupComponent(instance);
instance.update = effect(() => {
const proxy = createRenderProxy(instance);
const vnode = instance.render.call(proxy, proxy);
if (!instance.isMounted) {
patch(null, vnode, container);
instance.isMounted = true;
instance.subTree = vnode;
} else {
patch(instance.subTree, vnode, container);
instance.subTree = vnode;
}
});
return app;
}
};
return app;
}
// 导出 MiniVue API
const MiniVue = {
reactive,
ref,
isRef,
effect,
h,
createApp,
compile
};
8.2 使用示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>MiniVue Demo</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.counter { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
button { padding: 8px 16px; margin: 0 4px; cursor: pointer; }
.user-info { background: #f5f5f5; padding: 15px; margin-top: 10px; }
</style>
</head>
<body>
<div id="app"></div>
<script src="./minivue.js"></script>
<script>
const { createApp, reactive, ref, h } = MiniVue;
// 示例 1:使用模板编译
createApp({
setup() {
const state = reactive({
count: 0,
user: { name: '张三', age: 25 }
});
const title = ref('MiniVue 计数器');
const increment = () => state.count++;
const decrement = () => state.count--;
const updateName = () => state.user.name = state.user.name === '张三' ? '李四' : '张三';
return { state, title, increment, decrement, updateName };
},
template: `
<div class="counter">
<h1>{{ title }}</h1>
<p>当前计数: {{ state.count }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<div class="user-info">
<p>用户名: {{ state.user.name }}</p>
<p>年龄: {{ state.user.age }}</p>
<button @click="updateName">切换用户</button>
</div>
</div>
`
}).mount('#app');
// 示例 2:使用渲染函数
// createApp({
// setup() {
// const count = ref(0);
// return { count };
// },
// render() {
// return h('div', null, [
// h('h1', null, '渲染函数示例'),
// h('p', null, `Count: ${this.count}`),
// h('button', {
// onClick: () => this.count++
// }, 'Increment')
// ]);
// }
// }).mount('#app');
</script>
</body>
</html>
8.3 运行效果
打开 HTML 文件后,页面会显示:
- 一个标题 “MiniVue 计数器”
- 当前计数值和两个按钮(+1 / -1)
- 用户信息区域,显示用户名和年龄
- 点击按钮时,响应式系统会自动追踪数据变化并更新 DOM
8.4 单元测试
// 测试响应式系统
function testReactivity() {
console.log('=== 测试 reactive ===');
const state = reactive({ count: 0 });
let effectRunCount = 0;
effect(() => {
console.log('effect 执行:', state.count);
effectRunCount++;
});
state.count++; // 应触发 effect,输出 1
state.count++; // 应触发 effect,输出 2
console.assert(effectRunCount === 3, 'effect 应执行 3 次');
console.log('=== 测试 ref ===');
const num = ref(10);
let refEffectCount = 0;
effect(() => {
console.log('ref effect:', num.value);
refEffectCount++;
});
num.value = 20; // 应触发 effect
num.value = 20; // 值未变,不应触发
console.assert(refEffectCount === 2, 'ref effect 应执行 2 次');
console.log('=== 测试嵌套对象 ===');
const nested = reactive({ user: { name: 'test' } });
effect(() => {
console.log('user name:', nested.user.name);
});
nested.user.name = 'changed'; // 应触发 effect
}
// 测试虚拟 DOM
function testVNode() {
console.log('=== 测试 h 函数 ===');
const vnode = h('div', { id: 'app' }, [
h('h1', null, 'Hello'),
h('p', null, 'World')
]);
console.assert(vnode.type === 'div', '类型应为 div');
console.assert(vnode.props.id === 'app', 'id 应为 app');
console.assert(vnode.children.length === 2, '应有 2 个子节点');
console.log('=== 测试 createElement ===');
const el = createElement(vnode);
console.assert(el.tagName === 'DIV', '元素应为 DIV');
console.assert(el.children.length === 2, '应有 2 个子元素');
}
// 运行测试
testReactivity();
testVNode();
console.log('所有测试通过!');
九、常见问题
Q1:MiniVue 与真实 Vue3 的主要差异是什么?
MiniVue 是一个教学演示框架,与 Vue3 的主要差异包括:
| 方面 | MiniVue | Vue3 |
|---|---|---|
| 编译器 | 正则表达式解析,功能有限 | 完整的词法/语法分析器 |
| Diff 算法 | 简单双端比较 | 双端 diff + key 优化 |
| 组件系统 | 无 props/emit/插槽 | 完整的组件通信机制 |
| 生命周期 | 仅支持挂载 | 完整的生命周期钩子 |
| 性能优化 | 无 | 静态提升、PatchFlag 等 |
| TypeScript | 无 | 完整类型支持 |
Q2:为什么 Vue3 使用 Proxy 而不是 Object.defineProperty?
Proxy 相比 Object.defineProperty 有以下优势:
- 监听范围更广:Proxy 可以拦截更多操作(如
in运算符、delete、属性遍历等) - 嵌套对象自动代理:Proxy 可以在 getter 中递归创建代理,而
defineProperty需要预先遍历所有属性 - 数组操作友好:Proxy 可以直接拦截数组的索引设置和
push、pop等方法 - 性能更好:Proxy 是原生支持的代理机制,不需要对每个属性单独定义 getter/setter
Q3:effect 中的 cleanup 机制有什么作用?
cleanup 机制解决了条件分支中的依赖收集问题:
const state = reactive({ ok: true, text: 'hello' });
effect(() => {
console.log(state.ok ? state.text : 'no');
});
// 初始依赖: ok, text
state.ok = false;
// 现在只依赖 ok,但如果没有 cleanup,text 的修改仍会触发 effect
cleanup 在每次 effect 执行前清理旧的依赖关系,确保依赖集合始终与当前执行路径一致。
Q4:虚拟 DOM 的优势是什么?为什么不直接操作真实 DOM?
虚拟 DOM 的核心优势:
- 跨平台:虚拟 DOM 是平台无关的抽象,可以渲染到浏览器 DOM、Native 视图、Canvas 等不同平台
- 批量更新:将多次数据变化合并为一次 DOM 更新,减少重排重绘
- diff 优化:通过对比新旧虚拟 DOM 树,只更新变化的部分,而非全量替换
- 开发体验:开发者只需关注数据状态,框架自动处理 DOM 操作
Q5:编译器如何将模板转换为渲染函数?
Vue3 编译器的核心流程:
模板字符串
→ 词法分析(Tokenizer)生成 Token 流
→ 语法分析(Parser)生成 AST
→ 转换(Transform)优化 AST(静态提升等)
→ 代码生成(Codegen)输出渲染函数代码
MiniVue 的编译器简化了这个过程,直接用正则表达式解析生成简化的 AST,然后遍历 AST 生成 h() 函数调用代码。
十、总结
通过手写 MiniVue,我们深入理解了 Vue 框架的四大核心机制:
- 响应式系统:基于 Proxy 的依赖收集与触发机制,通过
track和trigger实现数据与视图的自动同步 - 虚拟 DOM:用 JavaScript 对象描述真实 DOM 结构,通过 diff 算法高效更新页面
- 编译器:将声明式模板编译为命令式渲染函数,架起了模板语法与虚拟 DOM 之间的桥梁
- 组件系统:封装组件实例、setup 函数和渲染代理,实现组件化开发模式
这四个模块相互协作,形成了 Vue 的完整数据流:
模板编译 → 渲染函数 → 虚拟 DOM → 真实 DOM
↑ ↓
响应式系统 ←←←←←←← 用户交互/数据变更
理解这些底层原理,不仅有助于更好地使用 Vue 进行开发,也为阅读 Vue 源码、进行性能优化和框架扩展打下了坚实基础。
十一、思考题
-
在 MiniVue 的响应式系统中,如果 effect 嵌套调用(一个 effect 内部触发另一个 effect),会出现什么问题?如何修复?
-
MiniVue 的 diff 算法只实现了最简单的版本。请尝试实现基于 key 属性的 diff 算法,支持列表节点的复用和移动。
-
当前的编译器只支持简单的插值表达式。如果要支持
v-if、v-for等指令,编译器需要做哪些扩展? -
在 MiniVue 中,如果组件的 render 函数访问了未在 setup 中返回的数据,会发生什么?对比 Vue3 的真实行为,思考其差异原因。
-
尝试为 MiniVue 添加
computed和watch的实现,思考它们与effect的关系:computed 是基于 effect 的衍生状态,watch 是基于 effect 的副作用监听。
更多推荐



所有评论(0)