手写一个迷你 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 整体架构

响应式系统

运行时阶段

编译阶段

模板字符串

Compiler
parse + generate

渲染函数 render

effect 包裹
建立响应式关联

生成 VNode 树
h 函数

patch 对比
diff 算法

更新真实 DOM

数据变更

trigger

通知所有依赖
的 effect

响应式系统数据流

effect targetMap Proxy 用户代码 effect targetMap Proxy 用户代码 创建 Proxy 代理 将 effect 存入 target ->> count ->> Set<effect> reactive({ count: 0 }) effect(() => console.log(state.count)) 读取 state.count track(target, 'count') 执行 effect 函数 state.count++ trigger(target, 'count') 遍历 Set,重新执行 effect 输出新值

虚拟 DOM Patch 流程

接收新旧 VNode

oldVNode
是否存在?

createElement
创建新 DOM

type 是否相同?

创建新 DOM
replaceChild 替换

是文本节点?

更新 textContent

patchProps
更新属性

patchChildren
递归对比子节点

完成更新

八、完整示例与测试

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 文件后,页面会显示:

  1. 一个标题 “MiniVue 计数器”
  2. 当前计数值和两个按钮(+1 / -1)
  3. 用户信息区域,显示用户名和年龄
  4. 点击按钮时,响应式系统会自动追踪数据变化并更新 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 有以下优势:

  1. 监听范围更广:Proxy 可以拦截更多操作(如 in 运算符、delete、属性遍历等)
  2. 嵌套对象自动代理:Proxy 可以在 getter 中递归创建代理,而 defineProperty 需要预先遍历所有属性
  3. 数组操作友好:Proxy 可以直接拦截数组的索引设置和 pushpop 等方法
  4. 性能更好: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 的核心优势:

  1. 跨平台:虚拟 DOM 是平台无关的抽象,可以渲染到浏览器 DOM、Native 视图、Canvas 等不同平台
  2. 批量更新:将多次数据变化合并为一次 DOM 更新,减少重排重绘
  3. diff 优化:通过对比新旧虚拟 DOM 树,只更新变化的部分,而非全量替换
  4. 开发体验:开发者只需关注数据状态,框架自动处理 DOM 操作

Q5:编译器如何将模板转换为渲染函数?

Vue3 编译器的核心流程:

模板字符串
  → 词法分析(Tokenizer)生成 Token 流
  → 语法分析(Parser)生成 AST
  → 转换(Transform)优化 AST(静态提升等)
  → 代码生成(Codegen)输出渲染函数代码

MiniVue 的编译器简化了这个过程,直接用正则表达式解析生成简化的 AST,然后遍历 AST 生成 h() 函数调用代码。

十、总结

通过手写 MiniVue,我们深入理解了 Vue 框架的四大核心机制:

  1. 响应式系统:基于 Proxy 的依赖收集与触发机制,通过 tracktrigger 实现数据与视图的自动同步
  2. 虚拟 DOM:用 JavaScript 对象描述真实 DOM 结构,通过 diff 算法高效更新页面
  3. 编译器:将声明式模板编译为命令式渲染函数,架起了模板语法与虚拟 DOM 之间的桥梁
  4. 组件系统:封装组件实例、setup 函数和渲染代理,实现组件化开发模式

这四个模块相互协作,形成了 Vue 的完整数据流:

模板编译 → 渲染函数 → 虚拟 DOM → 真实 DOM
                ↑                    ↓
           响应式系统 ←←←←←←← 用户交互/数据变更

理解这些底层原理,不仅有助于更好地使用 Vue 进行开发,也为阅读 Vue 源码、进行性能优化和框架扩展打下了坚实基础。

十一、思考题

  1. 在 MiniVue 的响应式系统中,如果 effect 嵌套调用(一个 effect 内部触发另一个 effect),会出现什么问题?如何修复?

  2. MiniVue 的 diff 算法只实现了最简单的版本。请尝试实现基于 key 属性的 diff 算法,支持列表节点的复用和移动。

  3. 当前的编译器只支持简单的插值表达式。如果要支持 v-ifv-for 等指令,编译器需要做哪些扩展?

  4. 在 MiniVue 中,如果组件的 render 函数访问了未在 setup 中返回的数据,会发生什么?对比 Vue3 的真实行为,思考其差异原因。

  5. 尝试为 MiniVue 添加 computedwatch 的实现,思考它们与 effect 的关系:computed 是基于 effect 的衍生状态,watch 是基于 effect 的副作用监听。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐