实现一个迷你版的Vue

手写实现Vue的功能,包括响应式系统、模板编译、虚拟DOM和组件系统

问题

实现一个简化版的Vue框架,需要包含以下功能:

  1. 响应式数据系统(数据劫持)
  2. 模板编译(解析指令和插值表达式)
  3. 依赖收集和派发更新
  4. 视图更新机制

解答

// 依赖收集器
class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  // 添加订阅者
  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }

  // 通知所有订阅者
  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

// 观察者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    this.value = this.get();
  }

  get() {
    Dep.target = this;
    // 触发getter,进行依赖收集
    const value = this.vm.$data[this.key];
    Dep.target = null;
    return value;
  }

  update() {
    const oldValue = this.value;
    const newValue = this.vm.$data[this.key];
    if (oldValue !== newValue) {
      this.value = newValue;
      this.callback.call(this.vm, newValue, oldValue);
    }
  }
}

// 响应式处理
class Observer {
  constructor(data) {
    this.data = data;
    this.walk(data);
  }

  walk(data) {
    if (!data || typeof data !== 'object') {
      return;
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }

  defineReactive(obj, key, value) {
    const dep = new Dep();
    // 递归处理嵌套对象
    this.walk(value);

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 依赖收集
        dep.depend();
        return value;
      },
      set(newValue) {
        if (newValue === value) {
          return;
        }
        value = newValue;
        // 派发更新
        dep.notify();
      }
    });
  }
}

// 编译器
class Compiler {
  constructor(el, vm) {
    this.el = typeof el === 'string' ? document.querySelector(el) : el;
    this.vm = vm;
    
    if (this.el) {
      // 将节点移入文档片段,减少回流
      this.fragment = this.nodeToFragment(this.el);
      // 编译模板
      this.compile(this.fragment);
      // 将编译后的内容放回页面
      this.el.appendChild(this.fragment);
    }
  }

  nodeToFragment(node) {
    const fragment = document.createDocumentFragment();
    let child;
    while (child = node.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  }

  compile(node) {
    const childNodes = node.childNodes;
    Array.from(childNodes).forEach(child => {
      if (this.isElementNode(child)) {
        // 编译元素节点
        this.compileElement(child);
      } else if (this.isTextNode(child)) {
        // 编译文本节点
        this.compileText(child);
      }

      // 递归编译子节点
      if (child.childNodes && child.childNodes.length) {
        this.compile(child);
      }
    });
  }

  compileElement(node) {
    const attributes = node.attributes;
    Array.from(attributes).forEach(attr => {
      const { name, value } = attr;
      
      // 处理 v-model 指令
      if (name === 'v-model') {
        this.modelUpdater(node, value);
      }
      // 处理 v-text 指令
      else if (name === 'v-text') {
        this.textUpdater(node, value);
      }
      // 处理 v-html 指令
      else if (name === 'v-html') {
        this.htmlUpdater(node, value);
      }
      // 处理 @click 事件
      else if (name.startsWith('@')) {
        const eventType = name.substring(1);
        this.eventHandler(node, value, eventType);
      }
    });
  }

  compileText(node) {
    const text = node.textContent;
    const reg = /\{\{(.+?)\}\}/g;
    
    if (reg.test(text)) {
      const key = RegExp.$1.trim();
      node.textContent = text.replace(reg, this.vm.$data[key]);
      
      // 创建观察者,数据变化时更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = text.replace(reg, newValue);
      });
    }
  }

  modelUpdater(node, key) {
    node.value = this.vm.$data[key];
    
    // 数据变化更新视图
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });

    // 视图变化更新数据
    node.addEventListener('input', (e) => {
      this.vm.$data[key] = e.target.value;
    });
  }

  textUpdater(node, key) {
    node.textContent = this.vm.$data[key];
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }

  htmlUpdater(node, key) {
    node.innerHTML = this.vm.$data[key];
    new Watcher(this.vm, key, (newValue) => {
      node.innerHTML = newValue;
    });
  }

  eventHandler(node, method, eventType) {
    const fn = this.vm.$methods && this.vm.$methods[method];
    if (fn) {
      node.addEventListener(eventType, fn.bind(this.vm));
    }
  }

  isElementNode(node) {
    return node.nodeType === 1;
  }

  isTextNode(node) {
    return node.nodeType === 3;
  }
}

// MiniVue 主类
class MiniVue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    this.$methods = options.methods;

    // 将data代理到实例上
    this.proxyData(this.$data);

    // 数据响应式处理
    new Observer(this.$data);

    // 编译模板
    if (this.$el) {
      new Compiler(this.$el, this);
    }
  }

  proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          data[key] = newValue;
        }
      });
    });
  }
}

使用示例

// HTML 模板
/*
<div id="app">
  <h1>{{ title }}</h1>
  <p v-text="message"></p>
  <input type="text" v-model="inputValue">
  <p>输入的值:{{ inputValue }}</p>
  <button @click="handleClick">点击我</button>
  <div v-html="htmlContent"></div>
</div>
*/

// 创建 MiniVue 实例
const vm = new MiniVue({
  el: '#app',
  data: {
    title: '迷你版Vue',
    message: '这是一个简化版的Vue实现',
    inputValue: 'Hello',
    htmlContent: '<strong>这是HTML内容</strong>'
  },
  methods: {
    handleClick() {
      this.message = '按钮被点击了!';
      this.title = '标题已更新';
    }
  }
});

// 可以直接访问和修改数据
console.log(vm.title); // '迷你版Vue'
vm.inputValue = 'World'; // 视图会自动更新

关键点

  • 响应式原理:使用 Object.defineProperty 劫持数据的 getter 和 setter,在 getter 中收集依赖,在 setter 中触发更新

  • 发布订阅模式:通过 Dep 类管理依赖,Watcher 类作为订阅者,实现数据变化时自动更新视图

  • 依赖收集:通过全局变量 Dep.target 标记当前正在执行的 Watcher,在数据被访问时建立依赖关系

  • 模板编译:解析 DOM 节点,识别指令(v-model、v-text、v-html)和插值表达式({{ }}),建立数据与视图的绑定关系

  • 双向绑定:v-model 指令通过监听 input 事件实现视图到数据的更新,通过 Watcher 实现数据到视图的更新

  • 文档片段优化:使用 DocumentFragment 减少 DOM 操作次数,提高性能

  • 数据代理:将 $data 中的属性代理到 Vue 实例上,可以通过 this.xxx 直接访问数据

  • 递归处理:对嵌套对象进行递归响应式处理,对子节点进行递归编译