深入vue2.x源码系列:手写代码来模拟Vue2.x的响应式数据实现

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 深入vue2.x源码系列:手写代码来模拟Vue2.x的响应式数据实现

前言

Vue响应式原理由以下三个部分组成:

  1. 数据劫持:Vue通过Object.defineProperty()方法对data中的每个属性进行拦截,当属性值发生变化时,会触发setter方法,通知依赖更新。
  2. 发布-订阅模式:Vue使用发布-订阅模式来实现数据的响应式更新。当数据发生变化时,会通知依赖进行更新。
  3. 依赖收集:Vue在渲染组件时,会对模板中使用到的数据进行依赖收集,将组件中使用到的数据和对应的Watcher对象建立关联,当数据发生变化时,会通知相关的Watcher对象进行更新。

实现流程

手写Vue响应式原理可以分为以下几个步骤:

  1. 实现Observer类:通过Object.defineProperty()方法对data中的每个属性进行拦截,当属性值发生变化时,会触发setter方法,通知依赖更新。
  2. 实现Dep类:用于管理Watcher对象,包括添加和删除Watcher对象,以及通知Watcher对象进行更新。
  3. 实现Watcher类:用于建立视图与数据之间的联系,当数据发生变化时,会通知Watcher对象进行更新。
  4. 实现Compile类:用于解析模板指令,将指令对应的数据渲染到视图中,并建立视图与数据的联系。
  5. 实现Vue类:将Observer、Watcher、Compile类进行整合,实现Vue的响应式更新机制。总的来说,手写Vue响应式原理主要由Observer、Dep、Watcher、Compile、Vue这几个组成部分构成。其中Observer用于拦截数据变化,Dep用于管理Watcher对象,Watcher用于建立视图与数据之间的联系,Compile用于解析模板指令,Vue将这些类进行整合,实现了Vue的响应式更新机制。

总的来说,手写Vue响应式原理主要由Observer、Dep、Watcher、Compile、Vue这几个组成部分构成。其中Observer用于拦截数据变化,Dep用于管理Watcher对象,Watcher用于建立视图与数据之间的联系,Compile用于解析模板指令,Vue将这些类进行整合,实现了Vue的响应式更新机制。

创建一个Dep类

我们使用递归来遍历数据对象中的所有属性,对每个属性使用Object.defineProperty()方法进行定义。在defineReactive()方法中,我们还创建了一个Dep类,Dep类用于管理所有订阅者(Watcher)和通知它们更新

class Dep {
  constructor() {
    this.subs = []; // 存储依赖的数组
  }
  // 添加依赖
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }
  // 通知依赖更新
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}
Dep.target = null; // 静态属性 target,用于保存当前的 Watcher 对象

上面的代码定义了一个名为Dep的类,它有以下几个方法:


  1. constructor():构造函数,初始化订阅者数组subs为空数组。
  2. addSub(sub):添加订阅者的方法,将传入的订阅者对象sub添加到subs数组中。
  3. notify():通知所有订阅者更新的方法,遍历subs数组,对每个订阅者调用其update()方法。
  4. target:定义一个全局变量target,用于存储当前的订阅者对象。

在Vue中,每个响应式数据(如data中的属性)都会对应一个Dep对象。当这个属性被读取时,会将当前的订阅者对象存储到Dep.target中,然后在属性的getter方法中,将Dep.target添加到当前属性的Dep对象的订阅者数组中;当属性的值被修改时,会调用该属性的Dep对象的notify()方法,通知所有订阅者更新。

创建一个Watcher类

接下来,我们需要创建一个Watcher类,它的主要作用是在数据发生变化时,触发视图的更新操作。在Watcher类中,我们首先需要保存更新视图所需的回调函数,并将Watcher实例添加到数据的订阅列表中。在数据发生变化时,我们遍历订阅列表,并依次调用回调函数来更新视图。

// 创建一个Watcher类,用于管理依赖与视图的更新
class Watcher {
  constructor(vm, key, cb) { 
      this.vm = vm; 
      this.key = key; 
      this.cb = cb;
      // 将当前Watcher实例指定为Dep.target 
      Dep.target = this; 
      // 获取数据的值,触发数据的get方法,从而将当前Watcher实例添加到Dep中 
      this.oldValue = vm[key]; 
      Dep.target = null;
  }
// 更新视图 
update() {
    const newValue = this.vm[this.key]; 
    if (this.oldValue === newValue) { 
        return; 
    } 
    this.cb(newValue); 
    this.oldValue = newValue; 
    }
}

创建一个Observer类

Observer 类:该类用于对数据进行监听和响应式处理,主要实现了 walk 和 defineReactive 两个方法。walk 方法遍历对象中所有属性,对每个属性调用 defineReactive 方法进行响应式处理;defineReactive 方法利用 Object.defineProperty 给每个属性添加 getter 和 setter,当属性被访问或修改时,会触发相应的依赖更新。

class Observer {
  constructor(data) {
    this.walk(data);
  }
  // 对数据对象进行递归遍历,为每个属性添加getter和setter
  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }
  defineReactive(obj, key, val) {
    const dep = new Dep(); // 创建一个依赖收集器
    Object.defineProperty(obj, key, {
      enumerable: true, // 可枚举
      configurable: true, // 可配置
      get() {
        // 添加依赖
        if (Dep.target) {
          dep.depend();
        }
        return val;
      },
      set(newVal) {
        if (val === newVal) {
          return;
        }
        val = newVal;
        // 触发依赖更新
        dep.notify();
      }
    });
  }
}

创建一个Compile类

Compile类的代码。在Compile类中,我们首先需要遍历模板中的节点,并根据节点的类型来处理它们。对于普通节点,我们将对它们的文本内容进行处理,对于包含指令的节点,我们将创建Watcher实例,并将它们添加到订阅列表中。

class Compile {
  constructor(el, vm) {
    this.el = document.querySelector(el); // 获取根节点
    this.vm = vm; // 保存 Vue 实例
    this.compile(this.el); // 编译模板
  }
  compile(el) {
    const childNodes = el.childNodes; // 获取根节点的子节点列表
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === 1) { // 元素节点
        this.compileElement(node);
      } else if (this.isInterpolation(node)) { // 文本节点且包含插值语法
        this.compileText(node);
      }
      // 递归编译子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }
  compileElement(node) {
    const attrs = node.attributes; // 获取元素节点的属性列表
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value;
      if (attrName.startsWith("v-")) { // 匹配指令
        const dir = attrName.substring(2); // 获取指令名称
        this[dir] && this[dir](node, exp); // 调用对应的指令函数
      }
    });
  }
  compileText(node) {
    const exp = node.textContent; // 获取插值语法中的表达式
    node.textContent = this.getVMValue(exp); // 将插值语法替换为表达式的值
  }
  isInterpolation(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); // 文本节点且包含插值语法
  }
  getVMValue(exp) {
    let value = this.vm;
    exp.split(".").forEach(key => {
      value = value[key];
    });
    return value;
  }
  // v-model 指令
  model(node, exp) {
    this.bind(node, exp, "model");
    node.addEventListener("input", e => {
      const newValue = e.target.value;
      this.setVMValue(exp, newValue);
    });
  }
  // v-bind 指令
  bind(node, exp, dir) {
    const updaterFn = this[dir + "Updater"];
    updaterFn && updaterFn(node, this.getVMValue(exp));
    new Watcher(this.vm, exp, value => {
      updaterFn && updaterFn(node, value);
    });
  }
  // model 指令更新视图
  modelUpdater(node, value) {
    node.value = value;
  }
  // v-text 指令
  text(node, exp) {
    this.bind(node, exp, "text");
  }
  // text 指令更新视图
  textUpdater(node, value) {
    node.textContent = value;
  }
  setVMValue(exp, value) {
    let vm = this.vm;
    const keys = exp.split(".");
    keys.forEach((key, index) => {
      if (index < keys.length - 1) {
        vm = vm[key];
      } else {
        vm[key] = value;
      }
    });
  }
}

Compile 类的实例化需要传入两个参数:el 和 vm。其中,el 是根节点的选择器,vm 是 Vue 实例。


Compile 类主要实现了以下功能:


  1. 遍历根节点及其子节点,对每个元素节点和包含插值语法的文本节点进行编译。
  2. 对于元素节点,遍历其属性列表,匹配指令并调用对应的指令函数进行处理。
  3. 对于包含插值语法的文本节点,替换为表达式的值。
  4. 实现了 v-model、v-bind 和 v-text 指令的处理。
  5. 实现了响应式数据的处理,通过 Watcher 对数据进行监听,数据发生变化时自动更新视图。

创建一个完整的Vue实例

创建一个Vue类,将Observer、Watcher和Compile类组合在一起,以创建一个完整的Vue实例

class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // 将 Vue 实例的属性代理到 $data 对象上
    this._proxyData(this.$data);
    // 创建Observer实例,监听数据变化
    new Observer(this.$data);
    // 创建Compile实例,解析模板指令
    new Compile(this.$el, this);
  }
  //使用_proxyData()方法来将数据代理到Vue实例中,就可以在Vue实例中通过this.key的方式来访问数据
  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          if (newValue === data[key]) {
            return;
          }
          data[key] = newValue;
        },
      });
    });
  }
}

到此为止,我们已经完成了手写代码来模拟Vue2.0的响应式数据实现的过程。我们可以通过这个过程来深入理解Vue2.0的响应式数据原理,从而更好地应用Vue2.0开发应用程序。


后续会继续更新vue2.0其他源码系列,包括目前在学习vue3.0源码也会后续更新出来,喜欢的点点关注。


相关文章
|
2月前
|
JavaScript 前端开发 开发者
Vue.js 框架大揭秘:响应式系统、组件化与路由管理,震撼你的前端世界!
【8月更文挑战第27天】Vue.js是一款备受欢迎的前端JavaScript框架,以简洁、灵活和高效著称。本文将从三个方面深入探讨Vue.js:响应式系统、组件化及路由管理。响应式系统为Vue.js的核心特性,能自动追踪数据变动并更新视图。例如,通过简单示例代码展示其响应式特性:`{{ message }}`,当`message`值改变,页面随之自动更新。此外,Vue.js支持组件化设计,允许将复杂界面拆分为独立且可复用的组件,提高代码可维护性和扩展性。如创建一个包含标题与内容的简单组件,并在其他页面中重复利用。
57 3
|
2月前
|
JavaScript 开发者
vue学习之响应式数据绑定
响应式数据绑定
28 0
|
7天前
|
JavaScript
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
本文讨论了在使用Vue和Element UI实现树形数据和懒加载时遇到的“Maximum call stack size exceeded”错误,指出问题的原因通常是因为数据中的唯一标识符`id`不唯一,导致递归渲染造成调用栈溢出。
19 1
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
|
6天前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
在 Vue3 中,如何使用 setup 函数创建响应式数据?
在 Vue3 中,如何使用 setup 函数创建响应式数据?
|
21天前
|
JavaScript
vue学习(8)数据代理
vue学习(8)数据代理
29 1
|
2月前
|
JavaScript
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
这篇文章通过一个实际的Vue项目案例,演示了如何在Vuex中实现多组件间共享数据。文章内容包括在Vuex的state中新增用户数组,创建Person.vue组件用于展示和添加用户信息,以及在Count组件中使用Person组件操作的数据。通过测试效果展示了组件间数据共享和状态更新的流程。
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
|
1月前
|
存储 JavaScript 前端开发
Vue 3的响应式系统是如何工作的呢
【9月更文挑战第3天】Vue 3的响应式系统是如何工作的呢
27 4
|
6天前
|
JavaScript 前端开发 UED
组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList
该文章详细介绍了如何使用Vue3结合TypeScript来开发全局Header组件和列表数据渲染组件ColumnList,并提供了从设计到实现的完整步骤指导。
|
6天前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
下一篇
无影云桌面