Async/Await

async/await用法 其实你要实现一个东西之前,最好是先搞清楚这两样东西 这个东西有什么用? 这个东西是怎么用的? 有什么用? async/await的用处就是:用同步方式,执行异步操作,怎么说呢?举个例子 比如我现在有一个需求:先请求完接口1,再去请求接口2,我们通常会这么做 function request(num) { // 模拟接口请求 return new Promise(resolve => { setTimeout(() => { resolve(num * 2) }, 1000) }) } request(1).then(res1 => { console.log(res1) // 1秒后 输出 2 request(2).then(res2 => { console.log(res2) // 2秒后 输出 4 }) }) 或者我现在又有一个需求:先请求完接口1,再拿接口1返回的数据,去当做接口2的请求参数,那我们也可以这么做 request(5).then(res1 => { console.log(res1) // 1秒后 输出 10 request(res1).then(res2 => { console.log(res2) // 2秒后 输出 20 }) }) 其实这么做是没问题的,但是如果嵌套的多了,不免有点不雅观,这个时候就可以用async/await来解决了 async function fn () { const res1 = await request(5) const res2 = await request(res1) console.log(res2) // 2秒后输出 20 } fn() 是怎么用? 还是用刚刚的例子 ...

October 2, 2024

函数柯里化

柯里化是函数的一个比较高级的应用,想要理解它并不简单。 通过《函数与函数式编程》的学习,我们知道,接收函数作为参数的函数,都可以叫做高阶函数。我们常常利用高阶函数来封装一些公共的逻辑。 我们接下来要学习的柯里化,其实就是高阶函数的一种特殊用法。 柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。 这样的定义可能不太好理解,我们可以通过下面的例子配合理解。 假如有一个接收三个参数的函数A。 function A(a, b, c) { // do something } 又假如我们有一个已经封装好了的柯里化通用函数createCurry。他接收bar作为参数,能够将A转化为柯里化函数,返回结果就是这个被转化之后的函数。 var _A = createCurry(A); 那么_A作为createCurry运行的返回函数,他能够处理A的剩余参数。因此下面的运行结果都是等价的。 _A(1, 2, 3); _A(1, 2)(3); _A(1)(2, 3); _A(1)(2)(3); A(1, 2, 3); 函数A被createCurry转化之后得到柯里化函数_A,_A能够处理A的所有剩余参数。因此柯里化也被称为部分求值。 在简单的场景下,我们可以不用借助柯里化通用式来转化得到柯里化函数,我们可以凭借眼力自己封装。 例如有一个简单的加法函数,他能够将自身的三个参数加起来并返回计算结果。 function add(a, b, c) { return a + b + c; } 那么add函数的柯里化函数_add则可以如下: function _add(a) { return function(b) { return function(c) { return a + b + c; } } } 因此下面的运算方式是等价的。 add(1, 2, 3); _add(1)(2)(3); 当然,柯里化通用式具备更加强大的能力,我们靠眼力自己封装的柯里化函数则自由度偏低。因此我们仍然需要知道自己如何去封装这样一个柯里化的通用式。 首先通过_add可以看出,柯里化函数的运行过程其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。因此我们在实现createCurry时,可以借助这个思路来进行封装。 封装如下: // 简单实现,参数只能从右到左传递 function createCurry(func, args) { var arity = func.length; var args = args || []; return function() { var _args = [].slice.call(arguments); [].push.apply(_args, args); // 如果参数个数小于最初的func.length,则递归调用,继续收集参数 if (_args.length < arity) { return createCurry.call(this, func, _args); } // 参数收集完毕,则执行func return func.apply(this, _args); } } 尽管我已经做了足够详细的注解,但是我想理解起来也并不是那么容易,因此建议大家用点耐心多阅读几遍。这个createCurry函数的封装借助闭包与递归,实现了一个参数收集,并在收集完毕之后执行所有参数的一个过程。 ...

October 1, 2024

设计模式概述

什么是设计模式? 设计模式(Design Pattern)是软件开发中反复出现问题的经典解决方案。它们是前人在大量实践中总结出的、被证明有效的代码设计经验,可以帮助开发者编写出更加灵活、可维护、可复用的代码。 设计模式不是具体的代码,而是解决特定问题的思想和模板。正确运用设计模式可以: 提高代码的可读性和可维护性 增强代码的可复用性 降低模块间的耦合度 提供通用的设计词汇,便于团队沟通 设计模式的起源 设计模式的概念最早由"四人帮"(Gang of Four,简称GoF)在1994年出版的《Design Patterns: Elements of Reusable Object-Oriented Software》一书中系统化提出。书中总结了23种经典设计模式,这些模式至今仍是软件设计的重要参考。 设计模式分类 GoF将23种设计模式分为三大类: 创建型模式(Creational Patterns) 创建型模式关注对象的创建机制,旨在以合适的方式创建对象,而不是直接使用new操作符。这类模式使得代码在创建对象时更加灵活。 模式 描述 iOS常见应用 单例模式 确保一个类只有一个实例 UIApplication.shared、FileManager.default 工厂方法模式 定义创建对象的接口,由子类决定实例化哪个类 NSNumber的工厂方法 抽象工厂模式 创建一系列相关对象而无需指定具体类 UIKit组件创建 建造者模式 将复杂对象的构建与表示分离 URLComponents、AlertController 原型模式 通过复制现有实例创建新对象 NSCopying协议 结构型模式(Structural Patterns) 结构型模式关注类和对象的组合,用于形成更大的结构。这类模式帮助确保当系统的一部分发生改变时,整个系统不需要随之改变。 模式 描述 iOS常见应用 适配器模式 将一个类的接口转换成客户期望的另一个接口 协议适配、第三方库封装 桥接模式 将抽象部分与实现部分分离 平台无关的抽象设计 组合模式 将对象组合成树形结构以表示部分-整体层次结构 UIView层级结构 装饰器模式 通过包装对象在运行时动态添加职责 包装器对象、链式装饰 外观模式 为子系统提供一个统一的高层接口 SDK封装、模块门面 享元模式 运用共享技术有效支持大量细粒度对象 字体对象、颜色对象等可共享的不可变对象 代理模式 为其他对象提供一种代理以控制访问 NSProxy、虚拟代理、保护代理 行为型模式(Behavioral Patterns) 行为型模式关注对象之间的通信和职责分配,描述类或对象如何交互以及如何分配职责。 ...

May 27, 2026

iOS中AI Chat实战

做一个能跑的AI聊天Demo只需要几十行代码:拼一个HTTP请求、把模型返回的文本显示出来即可。但要做一个达到生产级体验的AI Chat,就会面对一堆并不"AI"的工程问题:字怎么一个个"蹦"出来?Markdown怎么边流边渲染?模型想让前端弹一个卡片甚至一个表单,该怎么办?用户点击卡片里的按钮怎么回传给模型? 本文以iOS为实现目标,系统梳理AI Chat的工程架构,重点讲清楚两件事:流式输出与A2UI(Agent to UI),同时覆盖工具调用、取消、多模态、端侧推理等常见高级能力。 一、整体架构 一个完整的iOS AI Chat客户端,通常可以拆成如下几层: graph TB subgraph UI ["展示层"] LIST["消息列表UICollectionView / List"] CELL["消息Cell文本 / Markdown / A2UI Surface"] INPUT["输入区文本 / 图片 / 语音"] end subgraph VM ["ViewModel层"] STATE["会话状态机"] STREAM["流式拼装器"] TOOL["工具执行器"] end subgraph NET ["网络层"] SSE["SSE / HTTP 分块"] PARSER["协议解析OpenAI / Claude / A2UI"] end subgraph DATA ["数据层"] DB["消息持久化GRDB / WCDB / CoreData"] CTX["上下文裁剪Token预算"] end INPUT --> STATE STATE --> NET NET --> PARSER PARSER --> STREAM STREAM --> CELL STREAM --> TOOL TOOL --> NET STATE --> DB DB --> CTX CTX --> NET 几个关键特征: ...

May 2, 2026

Swift中import详解

源码版本说明:本文涉及的源码基于 Swift 编译器 开发版本(swift main分支,commit: e0db5152d4d4,2026-01-19)。不同版本的实现细节可能略有差异,但核心机制保持一致。 在前一篇文章Objective-C中import详解中,我们深入探讨了 Objective-C 中的 import 机制,包括 #include、#import、@import 以及 Clang Modules 的工作原理。本文将继续从 Swift 编译器的角度,讲解 Swift 中 import 的语法、模块查找机制、底层实现原理以及与 Objective-C 的互操作。 从 Clang Module 到 Swift Module 在深入 Swift 的 import 机制之前,让我们先回顾一下上一篇文章中 Clang Module 的核心概念。 Clang Module 核心概念回顾 在 Objective-C 文章中,我们学习了 Clang Module 的几个关键特性: module.modulemap:定义模块结构的配置文件 framework module Foundation { umbrella header "Foundation.h" export * } 预编译模块缓存(.pcm 文件):Clang 将模块编译为二进制格式,缓存以加速后续编译 懒加载机制:只加载实际使用的声明,未使用的部分不会被解析 自动链接:@import 会自动链接对应的库,无需手动配置 模块查找路径:通过 -I、-F 等参数指定搜索路径 Swift 如何继承 Clang Module 的设计 Swift 的模块系统并非从零开始设计,而是站在 Clang Module 的肩膀上,继承了其核心优势,并针对 Swift 的特性进行了扩展: ...

May 2, 2026

前端性能监控

大纲 我们会从以下三个方向来讲解埋点与监控的知识: 什么是埋点?什么是监控? JS 中实现监控的核心方案 写一个“相对”完整的监控实例 一、什么是埋点?什么是监控? 在日常沟通中,我们经常会把【埋点】和【监控】放到一起说,但是它们在本质上是有一定的区别的: 1. 埋点 埋点主要用于收集用户行为数据。在日常开发中,我们会通过 在前端代码中插入代码或脚本的方式 来实现埋点功能。 埋点的主要作用就是:捕获特定用户行为(如点击、浏览、提交表单、页面跳转等)以及关键业务数据(如下单金额、商品类别等) 在日常开发中,埋点的实现方案大致可以分为以下三大类: 手动埋点:在代码中手动加入记录代码来捕获特定事件。 自动埋点:利用 DOM 事件代理等技术来捕获页面上所有事件,从而减少手动配置。 可视化埋点:通过工具界面标记需要采集的元素和事件,可以不用手写代码。 2. 监控 而监控则主要关注 系统的性能和稳定性。在日常开发中,我们会通过 采集页面加载时间、资源请求、错误日志等数据 的方式来实现前端监控。 监控的主要作用就是:及时发现并定位页面性能瓶颈或代码异常,目的是为了保障系统不出 bug 在日常开发中,监控一般需要完成以下三大部分: 性能监控:如:首屏加载时间、页面交互耗时、资源加载耗时等。 错误监控:捕获 JavaScript 错误、网络请求失败、资源加载异常等。 用户体验监控:收集白屏、卡顿等影响用户体验的问题等。 区别总结 维度 前端埋点 前端监控 目标 捕获用户行为数据 监控系统性能、错误、稳定性 数据类型 用户点击、表单提交、页面跳转等 页面加载时间、错误日志、卡顿情况等 实现方式 手动埋点、自动埋点、可视化埋点 错误捕获、性能指标采集 核心关注点 用户行为、业务数据 系统Bug、性能优化 二、JS 中实现监控的核心方案 根据上面所说,我们知道埋点和监控的目的存在不同,但是它们的思路确是有很多一致性的,其核心都是:获取关键的数据,发送(上报)给服务端,依据数据来解决其不同的目的。 所以,无论是埋点也好,还是监控也罢,我们都需要 获取关键位置数据。 1. 跟踪用户事件(点击、滚动等) 定义通用跟踪函数(后续事件会通过该函数完成上报):trackEvent 函数接收事件类型和事件详情,并上报到服务端。 // 用于记录或发送跟踪数据到服务器的函数 function trackEvent(eventType, details) { console.log(`Event: ${eventType}`, details); // 在控制台打印事件类型和详情 // 上报到服务端。 fetch('/测试接口地址', { method: 'POST', body: JSON.stringify({ eventType, details }) }); } 捕获按钮点击事件:获取 id 为 myButton 的按钮,并在其 click 事件上添加监听器。在按钮被点击时调用 trackEvent 函数,记录点击事件的类型(button_click)、按钮 ID 和时间戳。 ...

August 7, 2025

浏览器的缓存机制解析

概述 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下HTTP报文,HTTP报文分为两种: HTTP请求(Request)报文,报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体),如下图 HTTP响应(Response)报文,报文格式为:状态行 – HTTP头(通用信息头,响应头,实体头) – 响应报文主体,如下图 注:通用信息头指的是请求和响应报文都支持的头域,分别为Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via;实体头则是实体信息的实体头域,分别为Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。这里只是为了方便理解,将通用信息头,响应头/请求头,实体头都归为了HTTP头。 以上的概念在这里我们不做多讲解,只简单介绍,有兴趣的童鞋可以自行研究。 缓存过程分析 浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图: 由上图我们可以知道: 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中 以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强制缓存和协商缓存。 强制缓存 强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下: 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图: 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存(暂不分析),如下图 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果,如下图 那么强制缓存的缓存规则是什么? 当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。 Expires Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。 Expires是HTTP/1.0的字段,但是现在浏览器默认使用的是HTTP/1.1,那么在HTTP/1.1中网页缓存还是否由Expires控制? 到了HTTP/1.1,Expire已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义,那么Cache-Control又是如何控制的呢? Cache-Control 在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为: public:所有内容都将被缓存(客户端和代理服务器都可缓存) private:所有内容只有客户端可以缓存,Cache-Control的默认取值 no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定 no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存 max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效 接下来,我们直接看一个例子,如下: 由上面的例子我们可以知道: HTTP响应报文中expires的时间值,是一个绝对值 HTTP响应报文中Cache-Control为max-age=600,是相对值 由于Cache-Control的优先级比expires,那么直接根据Cache-Control的值进行缓存,意思就是说在600秒内再次发起该请求,则会直接使用缓存结果,强制缓存生效。 注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control相比于expires是更好的选择,所以同时存在时,只有Cache-Control生效。 了解强制缓存的过程后,我们拓展性的思考一下: 浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效? 这里我们以博客的请求为例,状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置,分别为from memory cache 和 from disk cache。 那么from memory cache 和 from disk cache又分别代表的是什么呢?什么时候会使用from disk cache,什么时候会使用from memory cache呢? ...

July 28, 2025

跨域资源共享(CORS)

跨域资源共享(CORS,Cross-Origin Resource Sharing)是浏览器用来放宽同源策略限制的一种机制,是前端跨域请求的主流解决方案。面试时考察点主要围绕它的原理、工作流程、配置细节及相关安全问题。 一、CORS 基础概念 作用:允许浏览器从不同源的服务器请求资源,突破同源策略限制。 原理:服务器通过设置特定的 HTTP 响应头,告诉浏览器允许跨域访问。 二、CORS 的关键响应头 头部 说明 Access-Control-Allow-Origin 指明允许访问的源,可以是具体域名(如 https://example.com)或 *(表示允许所有域访问) Access-Control-Allow-Methods 允许的请求方法(GET、POST、PUT、DELETE 等) Access-Control-Allow-Headers 允许请求携带的自定义头部字段 Access-Control-Allow-Credentials 是否允许携带 Cookie 或 HTTP 认证信息,值为 true 时允许 Access-Control-Max-Age 预检请求的结果缓存时间,单位秒 三、CORS 请求分类 1. 简单请求 满足以下条件,浏览器直接发请求: 请求方法是 GET、POST、HEAD 中之一。 请求头只包含简单头(如 Accept, Content-Type 仅限 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)。 不携带自定义 Cookie、认证信息。 2. 预检请求(Preflight) 当请求不满足简单请求条件时,浏览器先发送 OPTIONS 请求询问服务器是否允许该跨域请求。 服务器响应通过特定头部告诉浏览器是否放行。 四、CORS 工作流程 浏览器发送跨域请求。 如果是简单请求,直接带上请求头发送实际请求,服务器返回响应带有 CORS 相关头,浏览器决定是否允许。 如果是非简单请求,浏览器先发 OPTIONS 预检请求。 服务器响应预检,决定是否允许后,浏览器再发实际请求。 浏览器根据响应头决定是否允许前端访问响应内容。 五、携带 Cookie 的跨域请求 默认情况下,跨域请求不发送 Cookie。 前端请求时必须设置:xhr.withCredentials = true 或 fetch 的 credentials: 'include'。 服务器必须设置:Access-Control-Allow-Credentials: true。 Access-Control-Allow-Origin 不能使用 *,必须指定具体域名。 常见考点 CORS 是什么?为什么需要它? 同源策略和 CORS 的关系? 简单请求和预检请求的区别? 预检请求的触发条件有哪些? Access-Control-Allow-Origin 设置为 * 有什么限制? 如何支持带 Cookie 的跨域请求? CORS 配置中的常用响应头说明。 如何解决跨域请求失败问题? JSONP 和 CORS 的区别? 服务器如何配置支持 CORS?

December 17, 2024

高阶组件

一 前言 React高阶组件(HOC),对于很多react开发者来说并不陌生,它是灵活使用react组件的一种技巧,高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数。 高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。高阶组件也并不是很难理解,其实接触过后还是蛮简单的,接下来将按照,高阶组件理解?,高阶组件具体怎么使用?应用场景, 高阶组件实践(源码级别) 为突破口,带大家详细了解一下高阶组件。 我们带着问题去开始今天的讨论: 1 什么是高阶组件,它解决了什么问题? 2 有几种高阶组件,它们优缺点是什么? 3 如何写一个优秀高阶组件? 4 hoc怎么处理静态属性,跨层级ref等问题? 5 高阶组件怎么控制渲染,隔离渲染? 6 高阶组件怎么监控原始组件的状态? … 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 二 全方位看高阶组件 1 几种包装强化组件的方式 ① mixin模式 原型图 老版本的react-mixins 在react初期提供一种组合方法。通过React.createClass,加入mixins属性,具体用法和vue 中mixins相似。具体实现如下。 const customMixin = { componentDidMount(){ console.log( '------componentDidMount------' ) }, say(){ console.log(this.state.name) } } const APP = React.createClass({ mixins: [ customMixin ], getInitialState(){ return { name:'alien' } }, render(){ const { name } = this.state return <div> hello ,world , my name is { name } </div> } }) 这种mixins只能存在createClass中,后来React.createClass连同mixins这种模式被废弃了。mixins会带来一些负面的影响。 ...

December 17, 2024

Web Storage

一、 简介 浏览器本地存储是指浏览器提供的一种机制,允许 Web 应用程序在浏览器端存储数据,以便在用户下次访问时可以快速获取和使用这些数据。一共两种存储方式:localStorage 和 sessionStorage。下面介绍下两种缓存的特性和在内部平台的一些应用。 二、localStorage 和 sessionStorage 2.1、区别 localStorage 和 sessionStorage 的主要区别是生命周期,具体区别如下: localStorage sessionStorage 生命周期 持久化存储:除非自行删除或清除缓存,否则一直存在 会话级别的存储:浏览器标签页或窗口关闭 作用域 相同浏览器,同域名,不同标签,不同窗口 相同浏览器,同域名,同源窗口 获取方式 window.localStorage window.sessionStorage 存储容量 5M 5M 容量限制的目的是防止滥用本地存储空间,导致用户浏览器变慢。 2.2、浏览器兼容性 1)现在的浏览器基本上都是支持这两种 Storage 特性的。各浏览器支持版本如下: Chrome Firefox IE Opera Safari Android Opera Mobile Safari Mobile localStorage 4 3.5 8 10.5 4 2.1 11 iOS 3.2 sessionStorage 5 2 8 10.5 4 2.1 11 iOS 3.2 2)如果使用的是老式浏览器,比如Internet Explorer 6、7 或者其他,就需要在使用前检测浏览器是否支持本地存储或者是否被禁用。以 localStorage 为例: ...

October 9, 2024