Vue响应式原理全解析

news2024/11/25 12:14:50

前言

大家好,我是程序员蒿里行。浅浅记录一下面试中的高频问题,请你谈一下Vue响应式原理。 必备前置知识,​​Vue2​​官方文档中​​深入响应式原理​​​及​​Vue3​​官方文档中​​深入响应式系统​​。

什么是响应式

响应式本质是当数据变化的时候,会自动执行一些相关函数。

const apple = {
  price: 2,
  amount: 3
}
const totalPrice = () => apple.price * apple.amount;

假设去水果店买苹果,价格为两元,买三个,总价是六元。但是苹果价格调整后,我还得重新计算一遍总价,即调用totalPrice函数。针对我们前端场景,数据的变动,无法直接响应页面变化,还需要做额外操作,如获取DOM,将数据设置到对应节点上。为了减少程序员的开发负担,响应式也就诞生了。

下面的例子实现了极简的响应式,数据变动,会自动更新页面。

<div class="card">
  <p id="firstName"></p>
  <p id="lastName"></p>
  <p id="age"></p>
</div>
<form>
  <input oninput="user.name = this.value"/>
  <input type="date" onchange="user.birthday = this.value">
</form>

var user = {
  name: '张三',
  birthday: '2000-1-1'
};

observe(user) // 观察对象

// 显示姓氏
function showFirstName () {
  var firstName = document.getElementById('firstName');
  firstName.textContent = '姓: ' + (user.name[0] || '');
}

// 显示名字
function showLastName () {
  var lastName = document.getElementById('lastName');
  lastName.textContent = '名: ' + user.name.slice(1);
}

// 显示年龄
function showAge () {
  var age = document.getElementById('age');
  var birth = new Date(user.birthday);
  var now = new Date();
  age.textContent = '年龄: ' + (now.getFullYear() - birth.getFullYear());
}


autoRun(showFirstName)
autoRun(showLastName)
autoRun(showAge)

/**
 * 观察某个对象的所有属性
 * @param {Object} obj
 */
function observe(obj) {
  for(const key in obj) {
    let internalValue = obj[key];
    let funcs = new Set();
    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集,记录:是哪个函数在用我
        if (window.__func && !funcs.has(window.__func)) {
          funcs.add(window.__func);
        }
        return internalValue;
      },
      set(val) {
        internalValue = val;
        // 派发更新,运行:执行用我的函数
        for (const func of funcs) {
          func();
        }
      }
    });
  }
}

function autoRun(fn){
  window.__func = fn
  fn()
  window.__func = null;
}

为何需要响应式

没有响应式系统时开发者要更新页面,通常需要手动更新DOM,抑或使用模板引擎。下面是最古老的模板引擎mastche.js的用法:

const Mustache = require('mustache');

const view = {
  title: "Joe",
  calc: () => ( 2 + 4 )
};

const template = `<div>{{title}} spends {{calc}}</div>`

const output = Mustache.render(template, view);
// <div>Joe spends 6</div>

Vue有自己的模板引擎,它的文本插值使用的也是“Mustache”语法 (即双大括号)。

上面两种方式不仅无法自动更新视图,而且含有大量的节点操作,代码可维护性和可读性差。 响应式不仅使得数据变化能够自动更新视图,无需手动操作DOM,而且解耦了数据和视图逻辑,简化了代码结构,提高了代码可维护性和可读性。 说了这么多响应式的好处,下面让我们来简单探究下Vue是如何实现一个响应式。

实现响应式

Vue的数据响应式原理是通过数据劫持结合发布-订阅者模式实现的。大概原理如下:

  1. 数据劫持:Vue通过Object.defineProperty或Proxy对数据进行劫持。
  2. 发布-订阅者模式:Vue使用发布-订阅者模式来实现数据变动时的通知和更新。

Object.defineProperty与Proxy

Object.defineProperty会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。它接收三个参数,分别为

/**
 * obj: 要定义属性的对象。
 * prop: 一个字符串或 Symbol,指定了要定义或修改的属性键。
 * descriptor: 要定义或修改的属性的描述符。
 */
Object.defineProperty(obj, prop, descriptor)

Object.defineProperty自定义 setter 和 getter

function Archiver() {
  let temperature = null;
  const archive = [];

  Object.defineProperty(this, "temperature", {
    get() {
      console.log("get!");
      return temperature;
    },
    set(value) {
      temperature = value;
      archive.push({ val: temperature });
    },
  });

  this.getArchive = () => archive;
}

const arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。它接收两个参数:

/**
 * target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
 * handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
 */
const p = new Proxy(target, handler)

Proxy自定义 setter 和 getter

// 目标对象
const target = {
  name: 'John',
  age: 30
};

// 处理器对象
const handler = {
  // 自定义 getter
  get: function(target, property) {
    console.log(`Getting ${property}`);
    return target[property];
  },
  
  // 自定义 setter
  set: function(target, property, value) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
  }
};

// 创建代理
const p = new Proxy(target, handler);

// 访问属性
console.log(p.name); // 输出:Getting name
                     // 输出:John

// 修改属性
p.age = 40; // 输出:Setting age to 40

发布-订阅模式

发布-订阅模式(Publish-Subscribe Pattern)是一种设计模式,用于构建对象之间的解耦和通信。在这种模式中,发布者(Publisher)和订阅者(Subscriber)之间通过一个中介者(或称为主题、事件通道等)进行通信,而不直接相互关联。 简而言之,发布者发布事件,而订阅者订阅这些事件。当事件发生时,发布者会将事件发送给所有订阅者。这样,订阅者就能够接收到事件并做出相应的响应。 举个例子,比如DOM 事件处理:

<script>
  // 发布者(Publisher): HTML 元素。
  const body = document.body;
  // 订阅者(Subscriber): JavaScript 事件处理函数。
  const handleClick = () => {
    alert('body clicked!');
  }
  const handleDBClick = () => {
    alert('body dbclicked');
  }
  // 订阅(Subscribe)
  body.addEventListener('click', handleClick);
  body.addEventListener('dbclick', handleDBClick);
  // 发布(Publish): HTML 元素触发了特定事件,所有订阅者的处理函数将被调用
</script>

Vue2响应式代码实现

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

  // 添加依赖
  depend() {
    if (Dep.target) {
      // 将当前Watcher添加到依赖列表
      this.subscribers.push(Dep.target);
    }
  }

  // 通知依赖更新
  notify() {
    this.subscribers.forEach(subscriber => {
      subscriber();
    });
  }
}

// 全局变量,当前正在计算的Watcher
Dep.target = null;

// Watcher
class Watcher {
  constructor(vm, update) {
    this.vm = vm;
    this.update = update;

    // 设置当前Watcher为Dep的target
    Dep.target = this.update;
    // 初始化时触发getter,收集依赖
    this.update();
    // 清空Dep的target,防止notify触发时,重复绑定Watcher与Dep
    Dep.target = null;
  }
}

// 数据响应式
function defineReactive(obj, key) {
  const dep = new Dep();
  let val = obj[key];

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('Getter:', key);
      dep.depend(); // 添加依赖
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      console.log('Setter:', key);
      dep.notify(); // 通知依赖更新
    }
  });
}

// 递归遍历对象,使其所有属性都变成响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key);
    observe(obj[key]);
  });
}

// 创建响应式对象
function reactive(obj) {
  observe(obj);
  return obj;
}

// 示例
const data = reactive({
  message: 'Hello, Vue!',
});

// Watcher监听message属性
new Watcher(data, () => {
  console.log('Message Updated:', data.message);
});

// 模拟视图中读取数据
console.log('Initial Message:', data.message);

// 模拟视图中修改数据
data.message = 'Hello, Reactive!';

// 可以在不同地方添加Watcher
new Watcher(data, () => {
  console.log('Another Watcher:', data.message);
});

// 修改数据,触发所有相关Watcher更新
data.message = 'New Message!';

上述代码中Dep 类可以被视为发布者(Publisher),而 Watcher 类则是订阅者(Subscriber),具体来说:

  • Dep 类中的 depend() 方法用于将当前的 Watcher 添加到依赖列表中,notify() 方法用于通知所有订阅者进行更新。
  • Watcher 类则在初始化时将自身作为当前的 Dep.target,并在初始化过程中触发响应式数据的读取,从而将自身添加到依赖列表中。当数据变化时,被依赖的属性会触发** Watcher **的更新操作。

在这里插入图片描述

为什么要使用 Dep 和 Watcher 呢?

主要是为了实现数据响应式的机制,具体原因如下:

  1. 数据依赖追踪:Dep 负责收集数据与依赖的关系,确保在数据变化时能够准确通知到相应的订阅者(Watcher),实现数据的依赖追踪。
  2. 解耦数据与视图:Watcher 订阅数据的变化,但不直接操作视图,使得数据的变化能够更灵活地驱动视图的更新,实现数据与视图的解耦。
  3. 避免重复计算:每个响应式属性都有一个对应的 Dep 对象,它可以确保每个 Watcher 在依赖的数据变化时只会被触发一次,避免不必要的重复计算。
  4. 惰性求值:只有在真正需要的时候才会进行依赖收集和更新操作,减少不必要的性能开销。

Vue3响应式代码实现

// 响应式核心逻辑
function reactive(obj) {
  const handlers = {
    // 拦截对象属性的读取操作
    get(target, key, receiver) {
      track(target, key); // 收集依赖
      return Reflect.get(target, key, receiver);
    },
    // 拦截对象属性的设置操作
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      trigger(target, key); // 触发更新
      return true;
    }
  };

  // 创建并返回代理对象
  return new Proxy(obj, handlers);
}

// 依赖收集相关逻辑
let activeEffect = null; // 当前活跃的响应式副作用

// 创建响应式副作用
function effect(callback) {
  activeEffect = callback; // 将当前回调函数设为活跃的响应式副作用
  callback(); // 调用一次以收集依赖
  activeEffect = null; // 调用结束后,将活跃的副作用置空
}

// 使用 WeakMap 来存储对象的依赖关系
const targetMap = new WeakMap();

// 收集依赖
function track(target, key) {
  if (!activeEffect) return; // 如果当前没有活跃的响应式副作用,则不进行依赖收集
  let depsMap = targetMap.get(target); // 获取目标对象的依赖映射
  if (!depsMap) {
    depsMap = new Map(); // 若不存在依赖映射,则创建一个新的 Map 对象
    targetMap.set(target, depsMap); // 将依赖映射存储到 WeakMap 中
  }
  let dep = depsMap.get(key); // 获取属性对应的依赖集合
  if (!dep) {
    dep = new Set(); // 若不存在依赖集合,则创建一个新的 Set 对象
    depsMap.set(key, dep); // 将依赖集合存储到 Map 中
  }
  dep.add(activeEffect); // 将当前活跃的副作用添加到依赖集合中
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target); // 获取目标对象的依赖映射
  if (!depsMap) return; // 若不存在依赖映射,则直接返回
  const dep = depsMap.get(key); // 获取属性对应的依赖集合
  if (dep) {
    dep.forEach(effect => {
      effect(); // 遍历依赖集合并执行每个副作用函数
    });
  }
}

// 示例
const state = reactive({
  count: 0
});

// 创建响应式副作用
effect(() => {
  console.log("Count updated:", state.count);
});

console.log("Initial count:", state.count);

state.count = 1; // 触发更新

state.count = 2; // 触发更新

为何Vue 3 的响应式代码实现比 Vue 2 更简洁?

  1. 使用 Proxy 替代了 Object.defineProperty:Vue 3 使用 Proxy 对象来实现响应式,代替了 Vue 2 中复杂的 Object.defineProperty,这样减少了代码量和复杂度。
  2. 引入了 WeakMap 优化依赖收集:Vue 3 使用 WeakMap 存储对象的依赖关系,相比于 Vue 2 中使用闭包管理依赖关系,简化了代码。
  3. 副作用函数和依赖关系分离:VVue 3 中的副作用函数(effect)和依赖关系(track 和 trigger)分离得更清晰,使得代码结构更加简单易懂。
  4. 模块化的设计:Vue 3 的响应式代码实现更模块化,代码结构更清晰易于理解和维护。

结语

响应式是现代前端开发中非常重要的概念,它使得数据与视图能够保持同步,同时也提高了代码的可维护性和可读性。通过深入理解 Vue 的响应式原理,我们可以更加高效地利用 Vue 来构建复杂的前端应用程序。 希望本文能够帮助大家更好地理解 Vue 的响应式实现方式,并在面试和实际项目开发中有所帮助。下篇开始从源码层面去分析Vue的响应式原理。

项目附件:​​点此下载​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1533166.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[隐私计算实训营学习笔记] 第1讲 数据要素流通

信任四基石 数据的分级分类 技术信任&#xff1a;全链路审计、闭环完成的数据可信流通体系 技术信任&#xff1a;开启数据密态时代 数据可流通的基础设施&#xff1a;密态天空计算

react ant design radio group, 自定义modal样式,radio样式

需求&#xff1a; modal 里面需要一个list 列表&#xff0c;列表有单选框&#xff0c;并且可以确认。 遇到的问题&#xff1a;自定义modal的样式&#xff0c;修改radio/ radio group 的样式 设计图如下&#xff1a; 代码&#xff1a; return (<Modaltitle"Duplica…

7.PWM开发SG90(手把手教会)

简介 PWM&#xff0c;英文名Pulse Width Modulation&#xff0c;是脉冲宽度调制缩写&#xff0c;它是通过对一系列脉冲的宽度进 行调制&#xff0c;等效出所需要的波形&#xff08;包含形状以及幅值&#xff09;&#xff0c;对模拟信号电平进行数字编码&#xff0c;也就是说通…

Transformer的前世今生 day02(神经网络语言模型、词向量)

神经网络语言模型 使用神经网络的方法&#xff0c;去完成语言模型的两个问题&#xff0c;下图为两层感知机的神经网络语言模型&#xff1a; 假设词典V内有五个词&#xff1a;“判断”、“这个”、“词”、“的”、“词性”&#xff0c;且要输出P(w_next | “判断”、“这个”、…

李国武:如何评估一家精益制造咨询公司的实施能力?

在制造业转型升级的大背景下&#xff0c;精益制造已成为企业提升竞争力、实现可持续发展的关键。然而&#xff0c;面对市场上众多的精益制造咨询公司&#xff0c;如何评估其实施能力成为了众多企业的难题。本文将从多个方面为大家揭示评估精益制造咨询公司实施能力的方法&#…

软考网工学习笔记(6) 广域通信网

公共交换电话网&#xff08;pstn&#xff09; 在pstn是为了语音通信而建立的网络。从20世纪60你年代开始用于数据传输 电话网有三个部分组成&#xff1a; 本地回路 &#xff0c;干线 和 交换机 。 干线 和 交换机 一般采用数字传输和交换技术 &#xff0c;而 本地回路基本采…

Tomcat介绍,Tomcat服务部署

目录 一、Tomcat 介绍 二、Tomcat 核心技术和组件 2.1、Web 容器&#xff1a;完成 Web 服务器的功能 2.2、Servlet 容器&#xff0c;名字为 catalina&#xff0c;用于处理 Servlet 代码 2.3、JSP 容器&#xff1a;用于将 JSP 动态网页翻译成 Servlet 代码 Tomcat 功能组件…

【自然语言处理】NLP入门(八):1、正则表达式与Python中的实现(8):正则表达式元字符:.、[]、^、$、*、+、?、{m,n}

文章目录 一、前言二、正则表达式与Python中的实现1、字符串构造2、字符串截取3、字符串格式化输出4、字符转义符5、字符串常用函数6、字符串常用方法7、正则表达式1. .&#xff1a;表示除换行符以外的任意字符2. []&#xff1a;指定字符集3. ^ &#xff1a;匹配行首&#xff0…

蓝桥杯练习题总结(二)dfs题、飞机降落、全球变暖

一、飞机降落 问题描述&#xff1a; N架飞机准备降落到某个只有一条跑道的机场。其中第 i 架飞机在 时刻到达机场上空&#xff0c;到达时它的剩余油料还可以继续盘旋 个单位时间&#xff0c;即它最早可以于 1, 时刻开始降落&#xff0c;最晚可以于时刻开始降落。降落过程需要个…

mysql笔记:24. 主从同步环境搭建

文章目录 主从同步的基本原理主从同步的搭建步骤1. 环境准备2. 配置主服务器&#xff08;Master&#xff09;3. 配置从服务器&#xff08;Slave&#xff09;4. 测试配置5. 常见故障5.1. 主从服务器上的MySQL版本不一致导致失败&#xff1f;5.2. Slave_IO_Running状态异常&#…

AI颠覆教学系统,ChatGPT对应试教育会带来哪些挑战?

ChatGPT爆火两个月&#xff0c;整个教育系统都在被颠覆。在全美范围内&#xff0c;许多大学教授、系主任和管理人员&#xff0c;都在对课堂进行大规模的调整&#xff0c;以应对ChatGPT对教学活动造成的巨大冲击。 我们的传统中高考选出的分霸&#xff0c;是更能吃苦&#xff0…

开发技术-FeignClient 对单个接口设置超时时间

1. 背景 FeignClient 调用某个接口&#xff0c;3s 没有结果就需要停止&#xff0c;处理后续业务。 2. 方法 FeignClient 自定义 name 属性 FeignClient(name "aaa" , url "xxx") public interface TestApi {ResponseBodyPOSTMapping(value "xx…

设计模式之抽象工厂模式解析

抽象工厂模式 1&#xff09;问题 工厂方法模式中的每个工厂只生产一类产品&#xff0c;会导致系统中存在大量的工厂类&#xff0c;增加系统的开销。 2&#xff09;概述 a&#xff09;产品族 和 产品等级结构 产品等级结构&#xff1a;产品的继承结构&#xff1b; 产品族&…

TnT-LLM: Text Mining at Scale with Large Language Models

TnT-LLM: Text Mining at Scale with Large Language Models 相关链接&#xff1a;arxiv 关键字&#xff1a;Large Language Models (LLMs)、Text Mining、Label Taxonomy、Text Classification、Prompt-based Interface 摘要 文本挖掘是将非结构化文本转换为结构化和有意义的…

Java开发---上海得帆(一面)

面试感受 这是我的第一次面试&#xff0c;我感觉我这次面试的很差&#xff0c;很糟糕&#xff0c;十分的糟糕&#xff0c;万分的糟糕。第一次面试&#xff0c;面试了半个小时。我去真的好紧张&#xff0c;脑子里一篇空白。脑子空白还不是最惨的&#xff0c;最惨的是那个八股文…

使用Lerna搭建业务组件库

Lerna基本概念 Lerna 是一个用来优化托管在 git\npm 上的多 package 代码库的工作流的一个管理工具,可以让你在主项目下管理多个子项目&#xff0c;从而解决了多个包互相依赖&#xff0c;且发布时需要手动维护多个包的问题。 主要功能&#xff1a; 为单个包或多个包运行命令 …

【GPT概念-03】:人工智能中的注意力机制

说明 注意力机制生成分数&#xff08;通常使用输入函数&#xff09;&#xff0c;确定对每个数据部分的关注程度。这些分数用于创建输入的加权总和&#xff0c;该总和馈送到下一个网络层。这允许模型捕获数据中的上下文和关系&#xff0c;而传统的固定序列处理方法可能会遗漏这…

JVM垃圾收集器你会选择吗?

目录 一、Serial收集器 二、ParNew收集器 三、Paralle Scavenge 四、Serial Old 五、Parallel Old 六、CMS收集器 6.1 CMS对处理器资源非常敏感 6.2 CMS容易出现浮动垃圾 6.3 产生内存碎片 七、G1 收集器 八、如何选择合适的垃圾收集器 JVM 垃圾收集器是Java虚…

走迷宫---dfs在矩阵图里的应用模板

题目描述如下&#xff1a; dfs算法解决迷宫问题的一个标准模板 &#xff0c;通过递归与回溯暴力遍历所有能走的点&#xff0c;并比较找出所有可行方案的最优解 解决这道问题的核心思想和组合数如出一辙&#xff0c;可以说是组合数的升级版 结合注释看dfs更清晰易懂&#xff0…

群晖HomeAssistant安装HACS插件商店结合内网穿透实现公网访问本地智能家居

文章目录 基本条件一、下载HACS源码二、添加HACS集成三、绑定米家设备 ​ 上文介绍了如何实现群晖Docker部署HomeAssistant&#xff0c;通过内网穿透在户外控制家庭中枢。本文将介绍如何安装HACS插件商店&#xff0c;将米家&#xff0c;果家设备接入 Home Assistant。 基本条件…