抛弃脚手架!手写极简Vue2实现原理

最近想学一下vue2源码,于是兴致冲冲的跑到[vue官网](深入响应式原理 --- Vue.js)了解响应式原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

大概理解了一下,vue通过Object.defineProperty劫持属性,然后通过setter触发watcher来进行组件更新,然后去翻一下vue2的源码,上万行的代码,亚麻呆住了!

于是我想开始实现一个极简的vue,由简入繁,方便理解,回忆一下平时怎么用vue

js 复制代码
    const app = new Vue({
      el: '#app',
      data: {
      
      },
      methods: {
       
      }
    });

首先想到是要创建一个Vue类,构造函数中挂载属性和方法,同时还要进行模板编译,工作流程是这样的

  • 1.初始化 :Vue实例创建并将数据转换为响应式
  • 2.依赖收集 :首次访问数据时,收集依赖的Watcher
  • 3.数据更新 :用户交互或其他操作修改数据
  • 4.通知更新 :数据变化触发setter,Dep通知所有相关的Watcher
  • 5.视图更新 :Watcher执行回调函数,更新DOM展示

先给出大概流程图,暂时不考虑边界情况

graph TD Vue[Vue实例] Data[Observer] Watcher[Watcher] DOM[视图] User[数据] Vue -->|初始化| Data Vue -->|挂载| DOM DOM -->|读取| Data Data -->|依赖收集| Watcher Watcher -->|更新| DOM User -->|修改| Data Data -->|通知| Watcher Watcher -->|重新渲染| DOM style Vue fill:#42b883 style Data fill:#4299e1 style Watcher fill:#9f7aea style DOM fill:#ed8936 style User fill:#e53e3e
js 复制代码
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods || {};

    // 将data数据转换为响应式
    this._observe(this.$data);

    // 将methods绑定到this上下文
    this._bindMethods();

    // 编译模板
    this._compile(this.$el);
  }

  _observe(data) {
   
  }

  _bindMethods() {
   
  }

  _compile(el) {
   
  }
}

接下来实现_observe,

  • 1.遍历data,将属性用Object.defineProperty进行劫持,转换为getter/setter
  • 2.为每个属性创建一个 Dep 实例(依赖收集器)
  • 3.数据变化时通知所有依赖该属性的Watcher进行更新
_observe 复制代码
 _observe(data) {
    const self = this;
    Object.keys(data).forEach(key => {
      let value = data[key];
      
      // 为每个属性创建一个依赖收集器
      const dep = new Dep();
      
      Object.defineProperty(data, key, {
        get() {
          // 如果有Watcher,就将其添加到依赖中
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newValue) {
          if (value !== newValue) {
            value = newValue;
            // 通知所有依赖更新
            dep.notify();
          }
        }
      });

      // 将data属性代理到Vue实例上
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newValue) {
          this.$data[key] = newValue;
        }
      });
    });
  }

Dep:用于添加观察者,当数据更新的时候通知观察者

  • 1.收集依赖,添加观察者
  • 2.通知所有观察者
Dep 复制代码
// 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}

watcher:关联数据和视图

  • 1.将自身设置为 Dep.target ,然后读取数据属性触发 getter ,从而被添加到对应数据的依赖收集器( Dep 实例)中
  • 2.Watcher 负责监听特定数据的变化。当数据发生变化时, Dep 会通知所有订阅的 Watcher 调用 update 方法
watcher 复制代码
// 观察者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    
    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this;
    // 触发getter,进行依赖收集
    this.oldValue = vm[key];
    // 清除标记
    Dep.target = null;
  }
  
  update() {
    const newValue = this.vm[this.key];
    if (this.oldValue !== newValue) {
      this.callback(newValue);
      this.oldValue = newValue;
    }
  }
}

_compile:解析插值表达式和数据更新视图

_compile 复制代码
_compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (node.nodeType === 3) {
        const text = node.textContent;
        const reg = /\{\{(.*?)\}\}/g;
        
        if (reg.test(text)) {
          const key = RegExp.$1.trim();
          // 创建Watcher实例
          new Watcher(this, key, (newValue) => {
            node.textContent = text.replace(reg, newValue);
          });
          
          // 初始渲染数据
          node.textContent = text.replace(reg, this.$data[key]);
        }
      } 
      // 递归处理子节点
      else if (node.nodeType === 1) {
        this._compile(node);
      }
    });
  }

_bindMethods:绑定方法执行

_bindMethods 复制代码
  _bindMethods() {
    const self = this;
    Object.keys(this.$methods).forEach(key => {
      self[key] = this.$methods[key].bind(self);
    });
  }

让我们梳理一下过程

  • 1.Vue实例创建后调用 _compile 方法,遍历DOM节点,检测文本节点中的 {{}} 语法。
  • 2.Watcher 构造函数执行时,会将自身设置为 Dep.target ,然后访问 vm[key] 触发数据属性的 getter,这样当前创建的watcher实例会被添加到依赖Dep中,data里面每个属性都对应一个Dep实例
  • 3.当数据发生改变时,触发 setter ,调用 dep.notify() 通知所有依赖的 Watcher ,然后 Watcher 调用 update 方法更新视图,而this.subs其实存放的是观察者,这样通过观察者watcher的callback来进行视图更新。

下面是完整的代码:

Vue极简框架 复制代码
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods || {};

    // 将data数据转换为响应式
    this._observe(this.$data);

    // 将methods绑定到this上下文
    this._bindMethods();

    // 编译模板
    this._compile(this.$el);
  }

  _observe(data) {
    const self = this;
    Object.keys(data).forEach(key => {
      let value = data[key];
      
      // 为每个属性创建一个依赖收集器
      const dep = new Dep();
      
      Object.defineProperty(data, key, {
        get() {
          // 如果有Watcher,就将其添加到依赖中
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newValue) {
          if (value !== newValue) {
            value = newValue;
            // 通知所有依赖更新
            dep.notify();
          }
        }
      });

      // 将data属性代理到Vue实例上
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newValue) {
          this.$data[key] = newValue;
        }
      });
    });
  }

  _bindMethods() {
    const self = this;
    Object.keys(this.$methods).forEach(key => {
      self[key] = this.$methods[key].bind(self);
    });
  }

  _compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (node.nodeType === 3) {
        const text = node.textContent;
        const reg = /\{\{(.*?)\}\}/g;
        
        if (reg.test(text)) {
          const key = RegExp.$1.trim();
          // 创建Watcher实例
          new Watcher(this, key, (newValue) => {
            node.textContent = text.replace(reg, newValue);
          });
          
          // 初始渲染数据
          node.textContent = text.replace(reg, this.$data[key]);
        }
      } 
      // 递归处理子节点
      else if (node.nodeType === 1) {
        this._compile(node);
      }
    });
  }
}

// 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}

// 观察者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    
    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this;
    // 触发getter,进行依赖收集
    this.oldValue = vm[key];
    // 清除标记
    Dep.target = null;
  }
  
  update() {
    const newValue = this.vm[this.key];
    if (this.oldValue !== newValue) {
      this.callback(newValue);
      this.oldValue = newValue;
    }
  }
}

页面测试:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>简易Vue实现</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .container {
      border: 1px solid #ddd;
      padding: 20px;
      border-radius: 5px;
    }
    button {
      background-color: #4CAF50;
      color: white;
      border: none;
      padding: 8px 12px;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 10px;
      margin-right: 10px;
    }
    button:hover {
      background-color: #45a049;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>简易Vue实现</h1>
    <div id="app">
      <h2>数据绑定演示</h2>
      <p>姓名: {{ name }}</p>
      <p>年龄: {{ age }}</p>
      <p>欢迎信息: {{ greeting }}</p>
      <button onclick="app.incrementAge()">增加年龄</button>
      <button onclick="app.changeName()">改变姓名</button>
    </div>
  </div>

  <script src="./simple_vue.js"></script>
  <script>
    // 创建Vue实例
    const app = new Vue({
      el: '#app',
      data: {
        name: '张三',
        age: 25,
        greeting: '欢迎使用简易Vue实现!'
      },
      methods: {
        incrementAge() {
          this.age++;
        },
        changeName() {
          this.name = '李四';
          this.greeting = '姓名已更新为: ' + this.name;
        }
      }
    });
  </script>
</body>
</html>

成功发生了改变!

目前框架存在问题:

1. 数据响应式的局限性

  • 嵌套对象处理 : _observe 方法只处理了对象的第一层属性,嵌套对象的属性不会被转换为响应式。
  • 数组支持 :无法检测数组的变化(如 push 、 pop 、 splice 等操作),数组元素的修改不会触发视图更新。
  • 新增属性 :无法检测对象新增属性的变化,只能监听初始化时已存在的属性。

2. 模板编译功能有限

  • 仅支持文本插值 :只实现了 {{}} 文本插值,不支持 Vue 的指令系统(如 v-model 、 v-for 、 v-if 等)。
  • 无优化策略 :编译过程是简单的字符串替换,没有实现模板缓存或虚拟 DOM 等优化手段。
  • 静态内容未优化 :所有内容都会被视为动态内容,即使是不会变化的静态文本。

3. 事件处理机制原始

  • 直接绑定 DOM 事件 :示例中使用原生 onclick 绑定事件,而非 Vue 的事件系统。
  • 无事件修饰符 :没有实现 .stop 、 .prevent 、 .capture 等事件修饰符。
  • 无自定义事件 :不支持组件间的自定义事件通信。

4. 性能优化缺失

  • 无批量更新 :每次数据变化都会立即触发视图更新,没有实现批量更新策略。
  • 无虚拟 DOM :直接操作真实 DOM,频繁更新可能导致性能问题。
  • Watcher 粒度问题 :每个数据属性对应一个 Watcher,当数据量大时可能造成性能开销。

5. 功能不完整

  • 无计算属性 :没有实现计算属性(computed)功能。
  • 无侦听器 :没有实现 watch 功能来监听特定数据的变化。
  • 无组件系统 :不支持组件化开发,无法拆分复杂应用。
  • 无生命周期钩子 :缺少 created、mounted 等生命周期钩子函数。
相关推荐
JuneXcy9 分钟前
11.Layout-Pinia优化重复请求
前端·javascript·css
子洋19 分钟前
快速目录跳转工具 zoxide 使用指南
前端·后端·shell
天下无贼!20 分钟前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
CF14年老兵40 分钟前
✅ Next.js 渲染速查表
前端·react.js·next.js
司宸1 小时前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树1 小时前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔1 小时前
总结一下Vue的组件通信
前端
dyb1 小时前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js
前端的日常1 小时前
Vite 如何处理静态资源?
前端
前端的日常1 小时前
如何在 Vite 中配置路由?
前端