Vue响应式原理全解析

前言

大家好,我是程序员蒿里行。浅浅记录一下面试中的高频问题,请你谈一下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 = fnfn()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 = {// 自定义 getterget: function(target, property) {console.log(`Getting ${property}`);return target[property];},// 自定义 setterset: 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的targetDep.target = this.update;// 初始化时触发getter,收集依赖this.update();// 清空Dep的target,防止notify触发时,重复绑定Watcher与DepDep.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.rhkb.cn/news/283348.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 不再“开源”,对中国的影响及应对方案

Redis 不再“开源”&#xff0c;使用双许可证 3 月 20 号&#xff0c;Redis 的 CEO Rowan Trollope 在官网上宣布了《Redis 采用双源许可证》的消息。他表示&#xff0c;今后 Redis 的所有新版本都将使用开源代码可用的许可证&#xff0c;不再使用 BSD 协议&#xff0c;而是采用…

基于SpringBoot实现WebSocket实时通讯的服务端和客户端

实现功能 服务端注册的客户端的列表&#xff1b;服务端向客户端发送广播消息&#xff1b;服务端向指定客户端发送消息&#xff1b;服务端向多个客户端发送消息&#xff1b;客户端给服务端发送消息&#xff1b; 效果&#xff1a; 环境 jdk&#xff1a;1.8 SpringBoot&#x…

如何确保多人游戏的配对体验快速而顺利

开发人员知道,游戏玩家在玩游戏时最快乐,他们等待进入多人游戏的时间越长,你失去他们的速度就越快。玩家不喜欢在大厅里等待;他们想马上参与行动。这就是为什么优化匹配是关键。 谷歌(Google)和Unity的Open Match等系统正在加速使用自定义逻辑构建匹配器的能力,使开发人…

跳过mysql权限验证来修改密码-GPT纯享版

建议重新配置一遍&#xff0c;弄成功好多次了&#xff0c;每次都出bug&#xff0c;又要重新弄&#xff0c;不是过期就是又登不进去了&#xff0c;我服了 电脑配置MySQL环境&#xff08;详细&#xff09;这个哥们的10min配完&#xff0c;轻轻松松&#xff0c; 旧方法&#xff…

Python-VBA编程500例-015-03(入门级)

飞行棋(Flying Chess)算法是一种搜索算法&#xff0c;主要用于解决图搜索和路径规划问题。它的主要特点是可以“飞跃”到棋盘上任何位置&#xff0c;从而大大减少了搜索的时间和空间复杂度。以下是一些飞行棋算法的实际应用场景&#xff1a; 1、路径规划&#xff1a;在机器人领…

利用Scala与Apache HttpClient实现网络音频流的抓取

概述 在当今数字化时代&#xff0c;网络数据的抓取和处理已成为许多应用程序和服务的重要组成部分。本文将介绍如何利用Scala编程语言结合Apache HttpClient工具库实现网络音频流的抓取。通过本文&#xff0c;读者将学习如何利用强大的Scala语言和Apache HttpClient库来抓取网…

1.6 学Python能干什么,Python的应用领域有哪些

Python能干什么&#xff0c;Python的应用领域 Python 作为一种功能强大的编程语言&#xff0c;因其简单易学而受到很多开发者的青睐。那么&#xff0c;Python 的应用领域有哪些呢&#xff1f; Python 有着非广泛的应用&#xff0c;几乎所有大中型互联网公司都在使用 Python&a…

java数据结构与算法刷题-----LeetCode215. 数组中的第K个最大元素

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 解题思路&#xff1a;时间复杂度O( n n n)&#xff0c;空间复杂度…

SQL Server 2008R2 日志文件大小设置及查询

SQL Server 2008R2 建立数据库存在日志无限增长问题&#xff0c;造成磁盘内存不足。本文解决这个问题&#xff0c;如下&#xff1a; 1.设置日志文件的最大大小 USE master; GO ALTER DATABASE [D_total] MODIFY FILE (NAME D_total_log, -- 日志文件的逻辑名称MAXSIZE 200…

精准防灾新篇章:GIS与Python机器学习技术在地质灾害风险评价与信息化建库中的前沿应用

结合项目实践案例和科研论文成果进行讲解。入门篇&#xff0c;ArcGIS软件的快速入门与GIS数据源的获取与理解&#xff1b;方法篇&#xff0c;致灾因子提取方法、灾害危险性因子分析指标体系的建立方法和灾害危险性评价模型构建方法&#xff1b;拓展篇&#xff0c;GIS在灾害重建…

idea 开发serlvet篮球秩序册管理系统idea开发mysql数据库web结构计算机java编程layUI框架开发

一、源码特点 idea开发 java servlet 篮球秩序册管理系统是一套完善的web设计系统mysql数据库 系统采用serlvetdaobean mvc 模式开发&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 servlet 篮…

机器学习-06-无监督算法-01-划分聚类Kmeans算法

总结 本系列是机器学习课程的系列课程&#xff0c;主要介绍机器学习中无监督算法&#xff0c;包括划分聚类等。 参考 数据分析实战 | K-means算法——蛋白质消费特征分析 欧洲48国英文名称的来龙去脉及其国旗动画 Kmeans在线动态演示 本门课程的目标 完成一个特定行业的…

java算法第32天 | ● 122.买卖股票的最佳时机II ● 55. 跳跃游戏 ● 45.跳跃游戏II

122.买卖股票的最佳时机II 本题中理解利润拆分是关键点&#xff01; 不要整块的去看&#xff0c;而是把整体利润拆为每天的利润。假如第 0 天买入&#xff0c;第 3 天卖出&#xff0c;那么利润为&#xff1a;prices[3] - prices[0]。 相当于(prices[3] - prices[2]) (prices[…

Panasonic松下PLC如何数据采集?如何实现快速接入IIOT云平台?

在工业自动化领域&#xff0c;数据采集与远程控制是提升生产效率、优化资源配置的关键环节。对于使用Panasonic松下PLC的用户来说&#xff0c;如何实现高效、稳定的数据采集&#xff0c;并快速接入IIOT云平台&#xff0c;是摆在他们面前的重要课题。HiWoo Box工业物联网关以其强…

练习 12 Web [极客大挑战 2019]BabySQL

本题复习&#xff1a;1.常规的万能语句SQL查询&#xff0c;union联合查询&#xff0c;Extractvalue()报错注入 extractvalue(1,concat(‘0x7e’,select(database())))%23 我一开始挨着试&#xff0c;感觉都无效 直到报错注入&#xff0c;查到了库名‘geek’ 尝试查表名&…

【赠书第21期】游戏力:竞技游戏设计实战教程

文章目录 前言 1 竞技游戏设计的核心要素 1.1 游戏机制 1.2 角色与技能 1.3 地图与环境 2 竞技游戏设计的策略与方法 2.1 以玩家为中心 2.2 不断迭代与优化 2.3 营造竞技氛围与社区文化 3 实战案例分析 4 结语 5 推荐图书 6 粉丝福利 前言 在数字化时代的浪潮中&…

Linux文件 profile、bashrc、bash_profile区别

Linux系统中&#xff0c;有三种文件 出现的非常频繁&#xff0c;那就是 profile、bash_profile、bashrc 文件。 1、profile 作用 profile&#xff0c;路径&#xff1a;/etc/profile&#xff0c;用于设置系统级的环境变量和启动程序&#xff0c;在这个文件下配置会对所有用户…

O2OA红头文件流转与O2OA版式公文编辑器基本使用

O2OA开发平台在流程管理中&#xff0c;提供了符合国家党政机关公文格式标准&#xff08;GB/T 9704—2012&#xff09;的公文编辑组件&#xff0c;可以让用户在包含公文管理的项目实施过程中&#xff0c;轻松地实现标准化公文格式的在线编辑、痕迹保留、手写签批等功能。并且可以…

QT gridlayout 循环设置组件,表格也通用 已解决

在需求中。经常遇到&#xff0c;表格 展示需求。 几乎都是json格式的。 // 列表配置文件QJsonArray listJsonArray getCfgJsonData("details_tab_table_config.json");if (listJsonArray.isEmpty()){return;}ui->gridWidget->setMaximumSize(QSize(310, 180)…

【Mysql】面试题汇总

1. 存储引擎 1-1. MySQL 支持哪些存储引擎&#xff1f;默认使用哪个&#xff1f; 答&#xff1a; MySQL 支持的存储引擎包括 InnoDB、MyISAM、Memory 等。 Mysql 5.5 之前默认的是MyISAM&#xff0c;Mysql 5.5 之后默认的是InnoDB。 可以通过 show engines 查看 Mysql 支持…