当前位置: 首页 > article >正文

前端异步编程全场景解读

前端异步编程是现代Web开发的核心,它解决了浏览器单线程执行带来的UI阻塞问题。以下从多个维度进行深度解析:

一、异步编程的核心概念

JavaScript的执行环境是单线程的,这意味着在同一时间只能执行一个任务。为了不阻塞主线程,JavaScript通过异步API(如Web API、Promise、async/await)实现非阻塞操作。异步编程允许代码在等待耗时操作(如网络请求、定时器、文件读写)时继续执行其他任务,从而提高程序的响应速度和性能。

执行栈与任务队列
  1. 执行栈(Call Stack)

    • 负责处理同步代码的执行。每当调用一个函数,该函数会被推入执行栈;执行完毕后,函数会从栈中弹出。
    • 示例:
      function foo() {console.log("foo");
      }
      foo(); // 推入执行栈,执行完毕后弹出
      
  2. 任务队列(Task Queue)

    • 异步操作(如setTimeoutfetch)完成后,其回调函数会被放入任务队列。任务队列分为:
      • 宏任务队列(MacroTask Queue):包括setTimeoutsetInterval、DOM事件、I/O操作等。
      • 微任务队列(MicroTask Queue):包括Promise.thenMutationObserverqueueMicrotask等。
    • 示例:
      setTimeout(() => console.log("Timeout"), 0); // 宏任务
      Promise.resolve().then(() => console.log("Promise")); // 微任务
      
  3. 事件循环(Event Loop)

    • 持续检查执行栈是否为空。如果为空,则依次执行微任务队列中的所有任务,随后执行宏任务队列中的一个任务,循环往复。
    • 流程示意图:
      执行栈空 → 清空微任务队列 → 执行一个宏任务 → 重复
      
代码执行顺序示例
console.log('Start'); // 同步任务,直接执行setTimeout(() => {console.log('Timeout'); // 宏任务,放入宏任务队列
}, 0);Promise.resolve().then(() => {console.log('Promise'); // 微任务,优先于宏任务执行
});console.log('End'); // 同步任务,直接执行// 输出顺序:Start → End → Promise → Timeout

解释

  1. 同步代码StartEnd依次执行。
  2. 微任务Promise优先于宏任务Timeout执行,因为事件循环会先清空微任务队列。
实际应用场景
  • 网络请求:使用fetchaxios时,通过异步回调处理响应数据,避免页面卡顿。
  • 动画渲染:在requestAnimationFrame中拆分任务,保证流畅的动画效果。
  • 用户交互:异步处理按钮点击事件,即使后台逻辑耗时也不会阻塞UI响应。

通过理解执行栈、任务队列和事件循环的机制,可以更好地优化代码性能,避免常见的异步陷阱(如回调地狱)。

二、异步编程的演进历程

在这里插入图片描述

1. 回调函数(Callback)

回调函数是JavaScript最早采用的异步编程方式,主要通过将函数作为参数传递给异步操作,在操作完成时调用该函数。典型的应用场景包括文件读写、网络请求等I/O操作。由于多个异步操作需要依次执行时会产生层层嵌套,导致代码可读性和维护性急剧下降,这种现象被称为回调地狱(Callback Hell)。

以处理用户数据为例:

// 获取用户数据 -> 处理数据 -> 获取更多数据 -> 再次处理
fetchUserData(function(userData) {validateUser(userData, function(validatedData) {fetchUserPosts(validatedData.id, function(posts) {processPosts(posts, function(result) {// 可能需要更深的嵌套...});});});
});

这种写法不仅难以阅读,错误处理也很分散,需要在每个回调中单独处理。

2. Promise

Promise是ES6引入的异步编程解决方案,它代表一个异步操作的最终完成或失败,并允许链式调用。Promise有3种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。通过.then()和.catch()方法可以清晰地组织异步流程。

改进后的用户数据处理:

fetchUserData().then(validateUser).then(validatedData => fetchUserPosts(validatedData.id)).then(processPosts).then(finalResult => {// 处理最终结果}).catch(error => {// 统一处理所有可能的错误console.error('处理流程出错:', error);});

Promise还提供了一些实用方法:

  • Promise.all(): 并行执行多个Promise
  • Promise.race(): 获取最先完成的Promise结果
3. Generator函数

Generator是ES6引入的特殊函数,通过function*定义,可以使用yield暂停和恢复执行。虽然Generator本身不是异步解决方案,但配合执行器(如co库)可以实现类似同步的异步编程风格。

一个结合Generator的执行流程:

function* userDataFlow() {try {const userData = yield fetchUserData();const validated = yield validateUser(userData);const posts = yield fetchUserPosts(validated.id);return yield processPosts(posts);} catch (err) {console.error('Generator流程出错:', err);}
}// 使用co库执行
co(userDataFlow).then(result => {console.log('最终结果:', result);
});

Generator的缺点是需要额外的执行器,且错误处理仍需手动实现。

4. async/await(ES2017)

async/await是建立在Promise之上的语法糖,通过async标记异步函数,await暂停执行直到Promise完成,使异步代码具有同步代码的可读性,同时保持非阻塞特性。

现代JavaScript开发的最佳实践:

async function handleUserData() {try {const userData = await fetchUserData();const validated = await validateUser(userData);const posts = await fetchUserPosts(validated.id);const result = await processPosts(posts);console.log('处理完成:', result);return result;} catch (error) {console.error('异步处理失败:', error);throw error; // 可以选择重新抛出错误}
}// 调用示例
handleUserData().then(finalResult => { /*...*/ }).catch(finalError => { /*...*/ });

async/await的优势:

  1. 代码结构清晰,如同同步代码
  2. 可以使用常规的try/catch处理错误
  3. 与Promise完全兼容,await后可以接任何Promise对象
  4. 适合复杂业务逻辑的场景

实际开发中,async/await已成为现代JavaScript异步编程的主流方案,但在处理并发请求时,仍需结合Promise.all等API使用。

三、异步编程的常见场景

1. 定时器(setTimeout/setInterval)

定时器是JavaScript中最基础的异步操作之一,主要用于延迟执行代码或周期性执行任务。

典型应用场景:

  • 动画效果(如渐隐渐现)
  • 轮询检查数据变化
  • 延迟加载资源
  • 实现节流/防抖功能
// 延迟执行示例
setTimeout(() => {console.log('这条消息将在1秒后显示');// 常用于延迟执行一次性任务,如页面加载后的提示
}, 1000);// 周期性执行示例
let counter = 0;
const intervalId = setInterval(() => {console.log(`这是第${++counter}次周期性执行`);if(counter >= 5) {clearInterval(intervalId); // 清除定时器console.log('周期性执行已停止');}
}, 2000);// 实际应用:轮询检查数据
function checkDataUpdates() {const pollInterval = setInterval(async () => {const response = await fetch('/api/checkUpdates');const { hasUpdates } = await response.json();if(hasUpdates) {clearInterval(pollInterval);refreshData();}}, 5000);
}

2. 网络请求(AJAX/Fetch)

现代Web应用大量依赖异步网络请求,以避免阻塞用户界面。

常见用例:

  • 获取API数据
  • 提交表单数据
  • 上传/下载文件
  • 实时数据更新
// Fetch API基本用法
fetch('https://api.example.com/users').then(response => {if(!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json(); // 解析JSON数据}).then(data => {console.log('获取到的用户数据:', data);displayUsers(data); // 处理数据}).catch(error => {console.error('请求失败:', error);showErrorMessage(error.message);});// 实际应用:带参数的POST请求
async function submitForm(formData) {try {const response = await fetch('/api/submit', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(formData)});const result = await response.json();if(result.success) {showSuccessMessage('提交成功!');} else {throw new Error(result.message || '提交失败');}} catch (error) {console.error('表单提交出错:', error);showErrorMessage(error.message);}
}

3. 事件监听

事件驱动是浏览器环境的核心编程模式,几乎所有用户交互都是异步处理的。

常见场景:

  • 按钮点击
  • 表单提交
  • 键盘/鼠标事件
  • 自定义事件
// 基本事件监听
const submitButton = document.getElementById('submit-btn');submitButton.addEventListener('click', async (event) => {event.preventDefault(); // 阻止默认行为try {submitButton.disabled = true; // 防止重复提交showLoadingIndicator();const formData = collectFormData();const result = await submitForm(formData);if(result.success) {redirectToSuccessPage();}} catch (error) {showErrorToast(error.message);} finally {submitButton.disabled = false;hideLoadingIndicator();}
});// 实际应用:输入框防抖
const searchInput = document.getElementById('search');
let debounceTimer;searchInput.addEventListener('input', () => {clearTimeout(debounceTimer);debounceTimer = setTimeout(async () => {const query = searchInput.value.trim();if(query.length > 2) {const results = await fetchSearchResults(query);displaySearchResults(results);}}, 300); // 300毫秒的延迟
});

4. Web Workers

Web Workers允许在后台线程运行JavaScript代码,避免阻塞主线程。

典型使用场景:

  • 大数据处理/计算
  • 图像/视频处理
  • 复杂算法执行
  • 实时数据分析
// 主线程代码
const worker = new Worker('data-processing-worker.js');// 发送数据给Worker
const largeDataset = generateLargeDataset(); // 假设这是大数据集
worker.postMessage({command: 'process',data: largeDataset
});// 接收处理结果
worker.onmessage = (event) => {const { status, result } = event.data;if(status === 'success') {displayProcessedData(result);} else {showProcessingError(result);}
};// 错误处理
worker.onerror = (error) => {console.error('Worker error:', error);terminateWorker();
};function terminateWorker() {worker.terminate(); // 终止Worker
}// data-processing-worker.js (Worker文件)
self.onmessage = (event) => {const { command, data } = event.data;if(command === 'process') {try {// 执行耗时计算const processedData = processLargeDataset(data);self.postMessage({status: 'success',result: processedData});} catch (error) {self.postMessage({status: 'error',result: error.message});}}
};function processLargeDataset(data) {// 在这里执行CPU密集型的计算// 例如大数据排序、复杂转换等return data.map(item => transformItem(item));
}

注意:实际使用Web Workers时,需要处理跨文件依赖、通信协议设计等复杂问题。对于简单任务,可能需要权衡使用Worker带来的复杂度与性能提升是否值得。

四、异步控制流模式

1. 串行执行

按顺序依次执行多个异步操作。

async function sequential() {const result1 = await task1();const result2 = await task2(result1);return result2;
}
2. 并行执行

同时执行多个异步操作,等待所有完成。

async function parallel() {const [result1, result2] = await Promise.all([task1(), task2()]);return result1 + result2;
}
3. 竞态执行

同时执行多个异步操作,哪个先完成就用哪个结果。

async function race() {const result = await Promise.race([task1(), task2()]);return result; // 返回最先完成的任务结果
}
4. 限制并发数

控制同时执行的异步任务数量。

// 使用第三方库(如p-limit)
import pLimit from 'p-limit';const limit = pLimit(2); // 最多同时执行2个任务const tasks = [task1, task2, task3, task4];
const results = await Promise.all(tasks.map(task => limit(task)));

五、异步错误处理

1. Promise链中的错误
fetchData().then(process).catch(error => console.error('Caught:', error)) // 捕获前面所有Promise的错误.then(() => console.log('This will still run'));
2. async/await中的错误
async function main() {try {const data = await fetchData();return process(data);} catch (error) {console.error('Handled error:', error);throw new Error('Processing failed'); // 重新抛出错误}
}
3. 全局错误捕获
// 捕获未处理的Promise拒绝
window.addEventListener('unhandledrejection', event => {console.error('Unhandled rejection:', event.reason);event.preventDefault(); // 阻止默认行为(如控制台警告)
});

六、异步编程的性能优化

在这里插入图片描述

1. 防抖(Debounce)

限制函数在一定时间内的执行次数。

function debounce(func, delay) {let timeout;return function() {const context = this;const args = arguments;clearTimeout(timeout);timeout = setTimeout(() => func.apply(context, args), delay);};
}// 使用场景:搜索框输入联想
const searchInput = document.querySelector('input');
searchInput.addEventListener('input', debounce(fetchSuggestions, 300));
2. 节流(Throttle)

强制函数在一定时间内只执行一次。

function throttle(func, limit) {let inThrottle;return function() {const context = this;const args = arguments;if (!inThrottle) {func.apply(context, args);inThrottle = true;setTimeout(() => inThrottle = false, limit);}};
}// 使用场景:滚动加载
window.addEventListener('scroll', throttle(loadMoreData, 500));

七、异步编程的陷阱与最佳实践

1. 常见陷阱
  • 错误处理遗漏:忘记在Promise链末尾添加.catch()
  • 意外同步代码await使用不当导致代码阻塞
  • 内存泄漏:未清理定时器或事件监听器
  • 竞态条件:多个异步操作互相影响
2. 最佳实践
  • 优先使用async/await:提高代码可读性
  • 统一错误处理:使用try/catch或全局错误捕获
  • 合理控制并发:避免同时发起过多请求
  • 明确函数异步性:函数名使用Async后缀(如fetchDataAsync
  • 使用AbortController:取消不再需要的异步操作
// 取消Fetch请求示例
const controller = new AbortController();
const signal = controller.signal;fetch('https://api.example.com/data', { signal }).then(response => response.json()).catch(error => {if (error.name === 'AbortError') {console.log('Request aborted');}});// 取消请求
controller.abort();

八、异步编程的未来趋势

  1. Web标准增强:如AbortControllerSuspense等API的普及
  2. 并发原语:如Promise.any()(ES2021)、Promise.allSettled()
  3. 生成器与异步迭代for await...of循环处理异步迭代器
  4. WebAssembly:高性能模块的异步加载与执行

理解和掌握异步编程是成为优秀前端开发者的关键,它贯穿于现代Web应用的各个层面,从UI交互到服务端通信,都离不开异步技术的支持。

相关文章:

前端异步编程全场景解读

前端异步编程是现代Web开发的核心,它解决了浏览器单线程执行带来的UI阻塞问题。以下从多个维度进行深度解析: 一、异步编程的核心概念 JavaScript的执行环境是单线程的,这意味着在同一时间只能执行一个任务。为了不阻塞主线程,J…...

分布式光纤声振传感技术原理与瑞利散射机制解析

分布式光纤传感技术(Distributed Fiber Optic Sensing,简称DFOS)作为近年来迅速发展的新型感知手段,已广泛应用于边界安防、油气管道监测、结构健康诊断、地震探测等领域。其子类技术——分布式光纤声振传感(Distribut…...

RocketMQ 客户端负载均衡机制详解及最佳实践

延伸阅读:🔍「RocketMQ 中文社区」 持续更新源码解析/最佳实践,提供 RocketMQ 专家 AI 答疑服务 前言 本文介绍 RocketMQ 负载均衡机制,主要涉及负载均衡发生的时机、客户端负载均衡对消费的影响(消息堆积/消费毛刺等…...

Q1起重机指挥理论备考要点分析

Q1起重机指挥理论备考要点分析 一、考试重点内容概述 Q1起重机指挥理论考试主要包含三大核心模块:安全技术知识(占40%)、指挥信号规范(占30%)和法规标准(占30%)。考试采用百分制,8…...

c++算法学习3——深度优先搜索

一、深度优先搜索的核心概念 DFS算法是一种通过递归或栈实现的"一条路走到底"的搜索策略,其核心思想是: 深度优先:从起点出发,选择一个方向探索到底,直到无路可走 回溯机制:遇到死路时返回最近…...

如何让非 TCP/IP 协议驱动屏蔽 IPv4/IPv6 和 ARP 报文?

——从硬件过滤到协议栈隔离的完整指南 引言 在现代网络开发中,许多场景需要定制化网络协议(如工业控制、高性能计算),此时需确保驱动仅处理特定协议,避免被标准协议(如 IPv4/IPv6/ARP)干扰。本文基于 Linux 内核驱动的实现,探讨如何通过硬件过滤、驱动层拦截和协议栈…...

组合模式:构建树形结构的艺术

引言:处理复杂对象结构的挑战 在软件开发中,我们常遇到需要处理部分-整体层次结构的场景: 文件系统中的文件与文件夹GUI中的容器与组件组织结构中的部门与员工菜单系统中的子菜单与菜单项组合模式正是为解决这类问题而生的设计模式。它允许我们将对象组合成树形结构来表示&…...

【SSM】SpringMVC学习笔记7:前后端数据传输协议和异常处理

这篇学习笔记是Spring系列笔记的第7篇,该笔记是笔者在学习黑马程序员SSM框架教程课程期间的笔记,供自己和他人参考。 Spring学习笔记目录 笔记1:【SSM】Spring基础: IoC配置学习笔记-CSDN博客 对应黑马课程P1~P20的内容。 笔记2…...

Spring Boot SQL数据库功能详解

Spring Boot自动配置与数据源管理 数据源自动配置机制 当在Spring Boot项目中添加数据库驱动依赖(如org.postgresql:postgresql)后,应用启动时自动配置系统会尝试创建DataSource实现。开发者只需提供基础连接信息: 数据库URL格…...

TI德州仪器TPS3103K33DBVR低功耗电压监控器IC电源管理芯片详细解析

1. 基本介绍 TPS3103K33DBVR 是 德州仪器(Texas Instruments, TI) 推出的一款 低功耗电压监控器(Supervisor IC),属于 电源管理芯片(PMIC) 类别,主要用于 系统复位和电压监测。 2. …...

C++课设:实现本地留言板系统(支持留言、搜索、标签、加密等)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、项目功能概览与亮点分析1. 核心功能…...

【见合八方平面波导外腔激光器专题系列】用于干涉光纤传感的低噪声平面波导外腔激光器2

----翻译自Mazin Alalus等人的文章 摘要 1550 nm DWDM 平面波导外腔激光器具有低相位/频率噪声、窄线宽和低 RIN 等特点。该腔体包括一个半导体增益芯片和一个带布拉格光栅的平面光波电路波导,采用 14 引脚蝶形封装。这种平面波导外腔激光器设计用于在振动和恶劣的…...

Xcode 16.2 版本 pod init 报错

Xcode 版本升级到 16.2 后,项目执行 pod init 报错; ### Error RuntimeError - PBXGroup attempted to initialize an object with unknown ISA PBXFileSystemSynchronizedRootGroup from attributes: {"isa">"PBXFileSystemSynchron…...

timestamp时间戳转换工具

作为一名程序员,一款高效的 在线转换工具 (在线时间戳转换 计算器 字节单位转换 json格式化)必不可少!https://jsons.top 排查问题时非常痛的点: 经常在秒级、毫秒级、字符串格式的时间单位来回转换,于是决定手撸一个…...

分布式计算框架学习笔记

一、🌐 为什么需要分布式计算框架? 资源受限:单台机器 CPU/GPU 内存有限。 任务复杂:模型训练、数据处理、仿真并发等任务耗时严重。 并行优化:通过任务拆分和并行执行提升效率。 可扩展部署:适配从本地…...

数据库管理与高可用-MySQL故障排查与生产环境优化

目录 #1.1MySQL单案例故障排查 1.1.1MySQL常见的故障排查 1.1.2MySQL主从故障排查 #2.1MySQL优化 2.1.1硬件方面的优化 2.1.2进程方面的优化 #3.1MySQL存储引擎 3.1.1 MyISAM存储引擎 3.1.2 InnoDB存储引擎 1.1MySQL单案例故障排查 1.1.1MySQL常见的故障排查 (1&…...

rk3506上移植lvgl应用

本文档介绍如何在开发板上运行以及移植LVGL。 1. 移植准备 硬件环境:开发板及其配套屏幕 开发板镜像 主机环境:Ubuntu 22.04.5 2. LVGL启动 ​ 出厂系统默认配置了 LVGL,并且上电之后默认会启动 一个LVGL应用 。 LVGL 的启动脚本为/etc/init.d/pre_init/S00-lv_demo,…...

Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术点解析

Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术点解析 第一轮:基础概念问题 请解释Spring框架的核心容器是什么?它的作用是什么? 程序员JY回答:Spring框架的核心容器是IoC容器(控制反转…...

Flask和Django,你怎么选?

Flask 和 Django 是 Python 两大最流行的 Web 框架,但它们的设计哲学、目标和适用场景有显著区别。以下是详细的对比: 核心区别:哲学与定位 Django: 定位: "全栈式" Web 框架。奉行"开箱即用"的理念。 哲学: "包含…...

LangChain + LangSmith + DeepSeek 入门实战:构建代码生成助手

本文基于 Jupyter Notebook 实践代码,结合 LangChain、LangSmith 和 DeepSeek 大模型,手把手演示如何构建一个代码生成助手,并实现全流程追踪与优化。 一、环境准备与配置 1. 安装依赖 pip install langchain langchain_openai2. 设置环境变…...

湖北理元理律师事务所:债务清偿方案中的法律技术革新

文/金融法律研究组 当前债务服务市场存在结构性矛盾:债权人追求快速回款,债务人需要喘息空间。湖北理元理律师事务所通过创新法律技术,在《企业破产法》《民法典》框架下构建梯度清偿模型,实现多方利益平衡。 一、个人债务优化的…...

大模型的LoRa通讯详解与实现教程

一、LoRa通讯技术概述 LoRa(Long Range)是一种低功耗广域网(LPWAN)通信技术,由Semtech公司开发,特别适合于物联网设备的长距离、低功耗通信需求。LoRa技术基于扩频调制技术,能够在保持低功耗的同时实现数公里甚至数十公里的通信距离。 LoRa的主要特点 长距离通信:在城…...

【Elasticsearch基础】Elasticsearch批量操作(Bulk API)深度解析与实践指南

目录 1 Bulk API概述 1.1 什么是批量操作 1.2 Bulk API的优势 2 Bulk API的工作原理 2.1 请求处理流程 2.2 底层机制 3 Bulk API的使用方法 3.1 基本请求格式 3.2 操作类型示例 3.3 响应格式 4 Bulk API的最佳实践 4.1 批量大小优化 4.2 错误处理策略 4.3 性能调…...

PCA笔记

✅ 问题本质:为什么让矩阵 TT 的行列式为 1? 这个问题通常出现在我们对数据做**线性变换(旋转/缩放)**的时候,比如在 PCA 中把数据从原始坐标系变换到主成分方向时。 📌 回顾一下背景 在 PCA 中&#xff…...

MySQL 数据库深度剖析:事务、SQL 优化、索引与 Buffer Pool

在当今数据驱动的时代,数据库作为数据存储与管理的核心,其性能与可靠性至关重要。MySQL 作为一款广泛使用的开源数据库,在众多应用场景中发挥着关键作用。在这篇博客中,我将围绕 MySQL 数据库的核心知识展开,涵盖事务及…...

MAZANOKE结合内网穿透技术实现跨地域图像优化服务的远程访问过程

文章目录 前言1. 关于MAZANOKE2. Docker部署3. 简单使用MAZANOKE4. 安装cpolar内网穿透5. 配置公网地址6. 配置固定公网地址总结 前言 在数字世界高速发展的今天,您是否察觉到那些静默增长的视觉数据正在悄然蚕食存储空间?随着影像记录成为日常习惯&…...

迁移科技3D视觉系统:重塑纸箱拆垛场景的智能革命

一、传统拆垛场景的困局与破局之道 在汽车零部件仓库中,每天有超过2万只异形纸箱需要拆垛分拣。传统人工拆垛面临三大挑战: 效率瓶颈:工人每小时仅能处理200-300件,且存在间歇性疲劳安全隐患:20kg以上重箱搬运导致年…...

World-writable config file /etc/mysql/mysql.conf.d/my.cnf is ignored

https://stackoverflow.com/questions/53741107/mysql-in-docker-on-ubuntu-warning-world-writable-config-file-is-ignored 修改权限 -> 重启mysql # 检查字符集配置 SHOW VARIABLES WHERE Variable_name IN (character_set_server, character_set_database ); --------…...

JS的传统写法 vs 简写形式

一、条件判断与逻辑操作 三元运算符简化条件判断 // 传统写法 let result; if (someCondition) {result yes; } else {result no; }// 简写方式 const result someCondition ? yes : no;短路求值 // 传统写法 if (condition) {doSomething(); }// 简写方式 condition &…...

信息收集:从图像元数据(隐藏信息收集)到用户身份的揭秘 --- 7000

目录 🌐 访问Web服务 💻 分析源代码 ⬇️ 下载图片并保留元数据 🔍 提取元数据(重点) 👤 生成用户名列表 🛠️ 技术原理 图片元数据(EXIF 数据) Username-Anarch…...