受控组件和非受控组件

前端开发经常会涉及表单的处理,或者其他一些用于输入的组件,比如日历组件。 涉及到输入,就绕不开受控模式和非受控模式的概念。 什么是受控,什么是非受控呢? 想一下,改变表单值只有两种情况: 用户去改变 value 或者代码去改变 value。 如果不能通过代码改表单值 value,那就是非受控,也就是不受我们控制。 但是代码可以给表单设置初始值 defaultValue。 代码设置表单的初始 value,但是能改变 value 的只有用户,代码通过监听 onChange 来拿到最新的值,或者通过 ref 拿到 dom 之后读取 value。 这种就是非受控模式。 反过来,代码可以改变表单的 value,就是受控模式。 注意,value 和 defaultValue 不一样: defaultValue 会作为 value 的初始值,后面用户改变的是 value。 而一旦你给 input 设置了 value,那用户就不能修改它了,可以输入触发 onChange 事件,但是表单的值不会变。 用户输入之后在 onChange 事件里拿到输入,然后通过代码去设置 value。 这就是受控模式。 其实绝大多数情况下,非受控就可以了,因为我们只是要拿到用户的输入,不需要手动去修改表单值。 但有的时候,你需要根据用户的输入做一些处理,然后设置为表单的值,这种就需要受控模式。 或者你想同步表单的值到另一个地方的时候,类似 Form 组件,也可以用受控模式。 value 由用户控制就是非受控模式,由代码控制就是受控模式。 我们写代码试一下: npx create-vite 创建 vite + react 的项目。 去掉 main.tsx 的 index.css 和 StrictMode: ...

December 17, 2024

浏览器的同源策略

概述 本文从以下三个问题,循序渐进,一步步拆解,何为浏览器的同源策略和跨域问题: 什么是同源策略和跨域问题? 怎样才算同源? 同源策略下会有什么限制? 什么是同源策略和跨域问题? 同源政策是 1995 年由 Netscape 公司(就是创造了 JS 的那家网景公司)引入浏览器的一种策略。 想要理解同源策略的话,可以先假设如果没有同源策略会发生什么事情。接下来我们通过一则恐怖故事代入理解一下没有同源策略的世界: 在某个深夜,小明独自一人在家浏览网页,无意间打开了一些特殊网站,而恰巧的是,小明之前刚好打开过某些重要支付平台的网站且还没关闭,这时特殊网站里面可能有一些特殊 JS 脚本,在偷偷获取小明支付平台上的信息,并且偷偷利用了这些信息再结合某些手段,成功把小明支付平台上的钱转走了,小明上完厕所回来后,突然收到支付平台转账成功的短信,然后然后然后瘫坐在地上,泣不成声,仰天长啸,大喊一声:“还我攒了半年的 9.99~” 恐怖故事听完了,我们来先上个概念,什么是同源策略? 借用 MDN 上的描述: 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 怎么理解这个概念呢?举个栗子,随着汽车数量的上升,现在很多城市都有各种限行政策,就拿广州来说,如果不是 粤A 的车牌进入城区的话会有开四停四的限行限制。 同源策略跟汽车限行政策有一点点像,同源策略规定在“同源”下的脚本、文档等资源访问不受限(有点像 粤A 牌的汽车在广州内行驶不会被限行),如果不是“同源”则无法互相访问脚本、文档等资源(非 粤A 牌的汽车在广州市区内受开四停四政策限制) 理解了同源策略后,对于跨域就很好理解了,同源策略是好,我们也需要同源策略,但有些时候我们确实需要不同源的两个网页互相读取信息,这是就产生了跨域问题。 额外补充,同源策略是浏览器的行为,是浏览器的行为,是浏览器的行为(对于这点的解析可以参考:同源策略下,服务器会收到浏览器的请求吗?) 怎样才算同源? 所谓“同源”,就是以下三者必须都相同: 协议相同 协议相同的两个域名:(都是 https 协议) https://juejin.cn https://juejin.cn 协议不同的两个域名:(一个是 http 协议,一个是 https 协议) http://juejin.cn https://juejin.cn 域名相同 域名相同的两个域名: https://juejin.cn https://juejin.cn 域名不同的两个域名:(一个是二级域名,一个是三级域名)(可能这个是最容易记错的) https://juejin.cn https://www.juejin.cn 端口相同 端口相同的两个域名: https://juejin.cn https://juejin.cn 端口不同的两个域名: https://juejin.cn https://juejin.cn:8081 再重复一遍, 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同源策略下会有什么限制? 在同源策略下,如果非同源,会有以下三个方面的行为受到限制: DOM 如果非同源,其 JS 脚本,不能互相对 DOM 进行读写操作 数据 如果非同源,不能互相读写 Cookies、LocalStorage、SessionStorage 和 IndexedDB 网络请求 如果非同源,不能通过 AJAX 的方式互相发送、接收数据 为了方便理解,接下来我们展开说说这三条规则 ↓↓↓ ...

December 17, 2024

z-index

大家可能觉得,z-index 是一个很简单的属性,你给它设置哪个值,元素就会位于 y 轴的哪个位置。但它实际上并没有我们想象的这么简单,这个属性背后是一系列决定元素所在层级的规则。 在进行今天的介绍前,我们先列出三个问题,如果你能一眼看出它们的解决方案,那么恭喜你掌握了z-index,也就不需要阅读本文了;如果不行,那么耐心看完本文,相信能找到答案。 三个自测问题 问题一:为什么z-index 数值更大,但 Content 没有在 Box 2 之上? <div id="box1"> Box 1 </div> <div id="content"> Content <br/> z-index: 2; </div> <div id="box2"> Box 2 <br/> z-index: 1; </div> div { padding: 25px; font-size: larger; } #box1 { background-color: chocolate; width: 200px; height: 100px; margin-bottom: -50px; } #content { background-color: gold; width: 300px; height: 200px; z-index: 2; } #box2 { background-color: cyan; width: 200px; height: 100px; margin-top: -50px; z-index: 1; } 问题二:明明 z-index 数值更小,为什么 Content 这次反而在Box 2 之上了? <div id="box1"> Box 1 </div> <div id="content"> Content <br/> transform: rotate(90deg); <br/> z-index: 1; </div> <div id="box2"> <br/> Box 2 <br/> z-index: 2; </div> div { padding: 25px; font-size: larger; } #box1 { background-color: chocolate; width: 200px; height: 100px; margin-bottom: -10px; } #content { background-color: gold; width: 250px; height: 200px; z-index: 1; transform: rotate(90deg); } #box2 { background-color: cyan; width: 200px; height: 100px; margin-top: -10px; z-index: 2; } ...

November 9, 2024

Canvas 和 SVG

1.Canvas Canvas 是 HTML5 中新增的一个元素,它提供了一种通过 JavaScript 来绘制图形的方式,使用笔刷绘制2D图案。通过 Canvas,我们可以在网页中动态地绘制出各种图形、图表、动画等内容,实现更丰富的交互效果。 工作方式 Canvas 的工作方式是,我们首先在 HTML 页面中添加一个 <canvas> 元素,并指定其宽度和高度,然后在 JavaScript 中获取该元素的上下文对象,并通过调用它的方法来绘制图形。Canvas 的上下文对象有多种,包括 2D 上下文、WebGL 上下文等,我们可以根据需求选择相应的上下文对象来进行绘制。 常用属性 属性 效果 fillStyle 填充颜色。默认为黑色 strokeStyle 描边颜色。默认为黑色 lineWidth 线条宽度。默认为 1 像素 lineCap 线条末端的样式,可选值有 butt、round 和 square。默认为 butt lineJoin 线条相交处的样式,可选值有 miter、round 和 bevel。默认为 miter。 miterLimit 线条相交处的最大斜接长度。默认为 10 像素 globalAlpha 绘制的透明度。默认为 1(不透明) shadowColor 阴影颜色。默认为黑色 shadowOffsetX、shadowOffsetY 阴影的偏移量和模糊程度 shadowBlur 模糊程度 优缺点 优点 基于矢量绘制,图形可以自由缩放而不失真 具有良好的跨浏览器兼容性 通过 JavaScript 动态生成图形,可以更加灵活地控制图形的显示和交互 缺点 无法处理复杂的交互事件、不支持跨域图像等 使用实例 这个实例中,我们使用了一个 HTML5 的 Canvas 元素,它的宽度和高度分别为 200 像素。在 JavaScript 中,我们获取了这个元素,并通过 getContext 方法获取了它的上下文对象。然后,我们使用 fillStyle 属性设置了矩形的填充颜色为红色,并使用 fillRect 方法绘制了一个起点坐标为 (50,50),宽度为 100 像素,高度为 100 像素的矩形。 ...

October 9, 2024

v-if/v-show

v-show原理 export const vShow: ObjectDirective<VShowElement> = { beforeMount(el, { value }, { transition }) { el._vod = el.style.display === 'none' ? '' : el.style.display if (transition && value) { transition.beforeEnter(el) } else { setDisplay(el, value) } }, mounted(el, { value }, { transition }) { if (transition && value) { transition.enter(el) } }, updated(el, { value, oldValue }, { transition }) { // ... }, beforeUnmount(el, { value }) { setDisplay(el, value) } } 有transition就执行transition,没有transition就直接设置display。不管初始条件是什么,元素总是会被渲染。 v-if原理 export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { // ... return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch( branch, key, context ) as IfConditionalExpression } else { // attach this branch's codegen node to the v-if root. const parentCondition = getParentCondition(ifNode.codegenNode!) parentCondition.alternate = createCodegenNodeForBranch( branch, key + ifNode.branches.length - 1, context ) } } }) } ) 返回一个node节点,render函数通过表达式的值来决定是否生成DOM ...

October 4, 2024

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