硬核Vue3响应式原理解析,为你保驾护航渡过寒冬

news2025/8/1 12:17:07

前言

大家好,我是落叶小小少年,虽然比较菜,虽然才开始写作分享,我始终相信

  • 核心demo更容易理解深的技术点
  • 每一次基础的学习都是对知识的巩固

因为从年初就开始使用Vue3了,现在才来学习Vue3,但是也不算晚,学到就是赚到,知识无价,只要今天的知识比昨天多一点就是在丰富自己。那么我们就来学习下Vue3的响应式原理

Vue3的响应式原理

大家都知道Vue3使用的是Proxy进行代理的,这里我们先用Proxy实现一个最基础的响应式

我知道看vue3源码是一件比较头疼的事,所以在这里给大家提取出最简单的一个响应式demo,方便大家学习和理解

1. 响应式函数

先写一个创建reactive的函数,里面使用了Proxy进行target对象的代理

function createReactiveObject(target, baseHandler) {
  const proxy = new Proxy(target, baseHandler);
  return proxy;
}

2. Proxy的handler函数

接着我们实现对应的baseHandler函数,其中最主要的是get和set(当然还有has、ownKeys等就先不实现)

function get(target, key, recivier) {
  const res = Reflect.get(target, key, recivier);
  track(target, key);
  // 深层响应式
  return res !== null && typeof res === 'object' ? createReactiveObject(res) : res;
}
​
function set(target, key, value, recivier) {
  const oldValue = target[key];
  const res = Reflect.set(target, key, value, recivier);
  if (!Object.is(value, oldValue)) {
    trigger(target, key);
  }
  return res;
}

对应的get和set函数做的最主要的事情就是追踪和触发

追踪就是指当你去获取这个target上的key值时,能追踪到你使用了这个target上的key值,如果放到target对象来说,就是指你调用了target[key],比如console.log(target[key]),这就是最简单的访问

触发就是指当你去修改这个值时,你已经追踪了这个值,那么你会有一个bucket去存这个副作用函数,当你修改对应key的值的时候,就需要从这个bucket里面找到对应的key的副作用函数,再去执行,达到响应式的效果

3. 如何设计副作用函数收集数据结构

那么我们应该怎么设计这个bucket才能正确存储上这个effect呢?

我们已经知道访问target[key]时要进行track,可以看出来和target和key有关系,但是一个target[key]肯定不止一个副作用函数使用,当然一个target上也有多个key可以收集副作用函数,所以我们需要这么设计:

targetMap是一个target对象到Map的映射,一个target上有多个key,所以key可以用Map存储

keyToDepMap是每个key对应的dep的映射,一个key可以对应多个dep(副作用函数收集)

在这里插入图片描述

4. 收集和触发

收集主要在track函数里面,触发则主要在trigger函数里面,也就是按照我们上面的结构在get时进行副作用收集和set时取出对应的副作用函数触发即可

const targetMap = new WeakMap();
function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  // 添加副作用函数
  dep.add(effect);
}
​
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    // 副作用函数触发
    dep.forEach((fn) => fn());
  }
}

最简单的方法就是直接写一个effect函数吧

上面看到我们实现了track和trigger,那么副作用函数从哪来呢?

const data = {
  name: 'reactive'![在这里插入图片描述](https://img-blog.csdnimg.cn/9335d91de11b4df49f02de84e522c8a8.png)


}
const proxyData = createReactiveObject(data, {
  get,
  set,
});
function effect () {
  // 直接访问了proxyData上的name属性
  console.log(proxyData.name); // reactive
}
effect()
// 修改后会触发trigger里面收集到的函数
proxyData.name = 'ref' // ref

正如我们所预料的,当修改了name值后,重新触发了effect函数,也就是从targetMap中找到了对应的effect函数,重新输出了ref值。targetMap的值如下:

在这里插入图片描述

至此已经实现了一个最小最简单的响应式原理。

在这里插入图片描述

5. 解决effect名字写死问题

大家还记得上面我们在track函数里面收集effect函数时吗?

dep.add(effect)这个add是固定添加effect函数的,这里就会有两个问题

  • 只能使用effect这个副作用函数名
  • 如果多个字段的副作用函数没办法区分开来

那么我们怎么解决这个问题呢?

很简单,就是用一个专门的name来收集effect函数,比如activeEffect,在执行某个副作用函数时就将activeEffect赋值乘这个副作用函数就行了

// 接上
let activeEffect
function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}
// 上面的track里面添加副作用函数对应修改成如下即可
// 添加副作用函数
dep.add(activeEffect);

好了,接下来我们测试一下

// proxyData接上面的代码
effect(() => {
  console.log(proxyData.name);
})
proxyData.hobby = 'coding';
effect(() => {
  console.log(proxyData.hobby);
})
proxyData.hobby = 'playing';

运行发现,报错了,思考一下为什么fn会不存在呢?track的时候为什么会收集一个空函数呢?
在这里插入图片描述

如果你看过《你不知道的js》你就会知道,当proxyData.hobby = 'coding';执行这段代码时,会先通过LHS找到proxyData上的hobby属性,然后就类似触发了getter拦截,于是就在track的时候把activeEffect函数给收集了,但是这不是我们想要的情况,所以我们应该在track收集的时候判断activeEffect是否为空

// 修改track函数
function track(target, key) {
  // 增加判断是否为空
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  // 添加副作用函数
  dep.add(activeEffect);
}

再次执行上面的例子🌰,结果如下,正如我们所期望的一样,hobby值修改,hobby收集的effect函数重新执行了
在这里插入图片描述

可能用图来它们之间的存储结构或许你更清楚明白🫡

在这里插入图片描述

封装reactive

上面我们已经实现了创建基础响应式的函数,那么只需要简单的封装下就能得到reactive函数

const baseHandler = {
  get,
  set,
}
function reactive(obj) {
  return createReactiveObject(obj, baseHandler)
}
// get函数里的递归调用也修改一下
function get(target, key, recivier) {
  const res = Reflect.get(target, key, recivier);
  track(target, key);
  // 使用reactive即可
  return res !== null && typeof res === 'object' ? reactive(res) : res;
}

那我们就用上面的reactive来试试效果

const proxyData = reactive(data);
effect(() => {
  console.log(proxyData.name); // reactive
})
proxyData.name = 'reactive2';
// reactive2

实现ref

实现ref也很简单,因为我们已经有reactive函数了,我们只需要对reactive函数封装一下就可以达到ref的效果

先想一想我们是怎么使用ref的呢?

const count = ref(10);
// 通过value来取值的
console.log(count.value)

所以我们可以利用value的属性创造一个reactive的obj返回即可。

function ref(value) {
  // {value: value}
  return reactive({ value });
}

测试一下是否能生效

const count = ref(10);
effect(() => {
  console.log(count.value); // 10
})
count.value = 20;
// 20

可以看到修改count的值,重新执行了effect函数,这个最基本的ref函数没啥问题了

实现computed

computed的实现也是在内部封装了一个effect函数,达到响应式的效果。其次就是它也是通过value来获取值,所以我们可以利用ref来实现,返回一个ref值就行

function computed(fn) {
  let res = ref()
  effect(() => {
    res.value = fn();
  })
  return res;
}

来测试一下,是否能根据以来的值去更新

const count = ref(10);
const num = ref(20);
const total = computed(() => {
  return count.value + num.value;
})
consoel.log(total.value); // 30
num.value = 30;
console.log(total.value); // 40
count.value = 20;
console.log(total.value); // 50

不出所料,computed也能达到最简单的收集触发了

以下是基本响应式的所有代码

在这里插入图片描述

总结

至此,我们写出了一个最基本的响应式系统的demo和最小实现了ref、reactive和computed

当然你可能也会有疑问,为什么effect函数实现了,computed都实现了为啥不实现watch呢?

实现watch需要增加调度功能,也就是说可以控制trigger触发的时机、次数以及方式

所以后续我们要完善这个响应式系统,增加调度功能、处理嵌套effect、自增多次又要怎么解决等等

如果你觉得这篇文章对你有帮助,请点个赞,鼓励一下作者吧

工具

画图工具用的是excalidraw,可以手画出比较好看的图

表情工具用的是表情符号大全,里面有很多表情,支持一键快速复制

表情包制作用的是在线表情制作器,可以选择自己喜欢的表情进行制作

代码图片美化工具是carbon,支持多种语言,多种风格,很好用

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

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

相关文章

yolo5 训练无人人机识别系统

环境搭建: 安装驱动 点击鼠标右键,如果出现NVIDIA图标,点开,出现如下图片 我的显卡是1650,根据显卡的型号去官网找相应的驱动下载就好了。驱动官网 安装好之后,打开命令行cmd,输入如下指令&a…

登录功能(基于SpringBoot+MP+Vue实现的功能)

目录 前言 一、UserMapper层代码分析 二、UserService层代码分析 1.UserService接口 2.UserServiceImpl实现类 3.UserController层代码分析 4.拦截器设置 5.展示效果图 总结 前言 登录功能是web开发中常见的功能,也是学后端必须要练得一个功能,本…

【JavaWeb】手写一个Servlet+JSP+JavaBean分页

✅✅作者主页:🔗孙不坚1208的博客 🔥🔥精选专栏:🔗JavaWeb从入门到精通(持续更新中) 📋📋 本文摘要:本篇文章主要分享如何使用ServletJSPJavaBean…

C++对象拷贝

前言:本教程使用到的工具是vs2010;能用VC6就用VC6,因为vs2010生成的汇编代码可能有点乱;此外,文章中涉及的汇编,我都会予以解释,所以放心观看。 目录 一、什么是对象拷贝? 二、C对…

【微服务】SpringCloud微服务剔除下线源码解析

💖 Spring家族及微服务系列文章 ✨【微服务】SpringCloud微服务续约源码解析 ✨【微服务】SpringCloud微服务注册源码解析 ✨【微服务】Nacos2.x服务发现?RPC调用?重试机制? ✨【微服务】Nacos通知客户端服务变更以及重试机制 ✨【…

SpringBoot SpringBoot 开发实用篇 4 数据层解决方案 4.4 Redis 下载安装与基本使用

SpringBoot 【黑马程序员SpringBoot2全套视频教程,springboot零基础到项目实战(spring boot2完整版)】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇4 数据层解决方案4.4 Redis 下载安装与基本使用4.4.1 问题引入4.4.2 …

Linux Mint(Ubuntu)上 安装 效率神器 utools

Linux Mint(Ubuntu)上 安装 效率神器 utools 我的 Windows 系统的笔记本只有 256G 固态,磁盘已经快用满了,最近想装个 Linux 玩玩,1 选择了 Linux Mint,然后就在闲置的移动硬盘上安装了 Linux Mint 21 cin…

Centos 安装Java库的多种方式

安装jdk(介绍三种方法) 查看java版本:java -version 方法一:利用yum源来安装jdk(此方法不需要配置环境变量) 查看yum库中的java安装包 :yum -y list java* 安装需要的jdk版本的所有java程序:yum -y instal…

纯正体验,极致商务 | 丽亭酒店聚焦未来赛道,实现共赢发展

10月28日,锦江酒店(中国区)“齐鲁集锦 共话未来”投资人交流会在济南盛大召开,面向华东地区投资人,行业专家、商旅客、品牌代表齐聚一堂,共同聚焦酒店市场投资新价值,商讨新时代酒店行业新机遇,多维探索酒店…

蓝牙数据包协议解析

1.前言 由于工作需要,初次接触蓝牙。从最基础的知识开始了解。 引用wiki中的介绍: 蓝牙(英语:Bluetooth),一种无线通讯技术标准,用来让固定与移动设备,在短距离间交换资料&#xff…

第一章:Spring流程执行步骤

Spring执行流程图 注意观察:每一个执行步骤的结果都会返回到DispatcherServlet ,然后再出发调用, 所以是请求接口的入口也是出口。 简单了解几个大类的走的流程和具体功能 DispatcherServlet 类 中文调度应用程序,而Servlet就…

libusb系列-007-Qt下使用libusb1.0.26源码

libusb系列-007-Qt下使用libusb1.0.26源码 文章目录libusb系列-007-Qt下使用libusb1.0.26源码摘要安装编译环境确认需要的文件开始编译错误1:找不到文件错误2:expected错误3:SCM_CREDENTALS错误4:类型冲突错误5 assert断言错误错误…

低代码平台和无代码平台有什么区别

低代码(LowCode)/无代码(NoCode)”是技术界近几年的热门词汇之一,随着企业数字化发展的深入,越来越多的场景化需求要求企业具备更加灵活敏捷的应用开发能力,传统应用开发模式周期长、技术人员能力要求高,无…

OWASP API SECURITY TOP 10

目录 1. API 安全风险 2. 细说TOP10 1. Broken Object Level Authorization 2. Broken User Authentication 3 Excessive Data Exposure 4 Lack of Resources & Rate Limiting 5 Broken Function Level Authorization 6 Mass Assignment 7 security misconfigura…

redis哨兵系列1

需要配合源码一起康~ 9.1 哨兵基本概念 官网手册yyds:https://redis.io/docs/manual/sentinel/ redis主从模式,如果主挂了,需要人工将从节点提升为主节点,通知应用修改主节点的地址。不是很友好,so Redis 2.8之后开…

同花顺_代码解析_技术指标_EJK

本文通过对同花顺中现成代码进行解析,用以了解同花顺相关策略设计的思想 目录 EMV ENV EXPMA JF_ZNZX KD KDJ KDJFS EMV 简易波动指标 1.EMV 由下往上穿越0 轴时,视为中期买进参考信号; 2.EMV 由上往下穿越0 轴时,视为中…

根据以下电路图,补全STM32F103RCT6的IO口初始化程序

void KEY_Init(void)//接按键的端口初始化程序 { RCC->APB2ENR|______________; //使能PORTA时钟 JTAG_Set(SWD_ENABLE); GPIOA->CRL&__________________; // PA3设置成下拉输入 GPIOA->CRL|__________________; } void LED_Init(void)//接LED的端…

【Qt】控件探幽——QLineEdit

注1:本系列文章使用的Qt版本为Qt 6.3.1 注2:本系列文章常规情况下不会直接贴出源码供复制,都以图片形式展示。所有代码,自己动手写一写,记忆更深刻。 本文目录QLineEdit探幽1、设置数据/获取数据2、只读(re…

【15-项目中服务的远程调用之OpenFeign订单模块与商品模块集成使用OpenFeign的案例】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了,请点击这里!】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

【面试题】圣杯布局和双飞翼布局

圣杯布局和双飞翼布局的特点: 三栏布局,中间一栏最先加载和渲染(内容最重要)两侧内容固定,中间内容随着宽度自适应一般用于PC页面 圣杯布局和双飞翼布局的实现方式: 使用float布局两侧使用margin负值&am…