Showing preview only (1,961K chars total). Download the full file or copy to clipboard to get everything.
Repository: ascoders/weekly
Branch: master
Commit: 9357b24e8b5d
Files: 303
Total size: 1.8 MB
Directory structure:
gitextract_q9gvtcfs/
├── .gitignore
├── .lintmdrc
├── .travis.yml
├── SQL/
│ ├── 231.SQL 入门.md
│ ├── 232.SQL 聚合查询.md
│ ├── 233.SQL 复杂查询.md
│ ├── 234.SQL CASE 表达式.md
│ ├── 235.SQL 窗口函数.md
│ └── 236.SQL grouping.md
├── TS 类型体操/
│ ├── 243.精读《Pick, Awaited, If...》.md
│ ├── 244.精读《Get return type, Omit, ReadOnly...》.md
│ ├── 245.精读《Promise.all, Replace, Type Lookup...》.md
│ ├── 246.精读《Permutation, Flatten, Absolute...》.md
│ ├── 247.精读《Diff, AnyOf, IsUnion...》.md
│ ├── 248.精读《MinusOne, PickByType, StartsWith...》.md
│ ├── 249.精读《ObjectEntries, Shift, Reverse...》.md
│ ├── 250.精读《Flip, Fibonacci, AllCombinations...》.md
│ ├── 251.精读《Trim Right, Without, Trunc...》.md
│ └── 252.精读《Unique, MapTypes, Construct Tuple...》.md
├── helper.js
├── package.json
├── readme.md
├── 前沿技术/
│ ├── 1.精读《js 模块化发展》.md
│ ├── 10.精读《Web Components 的困境》.md
│ ├── 100.精读《V8 引擎 Lazy Parsing》.md
│ ├── 101.精读《持续集成 vs 持续交付 vs 持续部署》.md
│ ├── 102.精读《Monorepo 的优势》.md
│ ├── 104.精读《Function Component 入门》.md
│ ├── 105.精读《What's new in javascript》.md
│ ├── 107.精读《Optional chaining》.md
│ ├── 109.精读《Vue3.0 Function API》.md
│ ├── 11.精读《前端调试技巧》.md
│ ├── 111.精读《前端未来展望》.md
│ ├── 112.精读《源码学习》.md
│ ├── 113.精读《Nodejs V12》.md
│ ├── 117.精读《Tableau 探索式模型》.md
│ ├── 118.精读《使用 css 变量生成颜色主题》.md
│ ├── 119.精读《前端深水区》.md
│ ├── 12.精读《React 高阶组件》.md
│ ├── 120.精读《React Hooks 最佳实践》.md
│ ├── 121.精读《前端与 BI》.md
│ ├── 123.精读《用 Babel 创造自定义 JS 语法》.md
│ ├── 124.精读《用 css grid 重新思考布局》.md
│ ├── 125.精读《深度学习 - 函数式之美》.md
│ ├── 126.精读《Nuxtjs》.md
│ ├── 127.精读《React Conf 2019 - Day1》.md
│ ├── 129.精读《React Conf 2019 - Day2》.md
│ ├── 13.精读《This 带来的困惑》.md
│ ├── 132.精读《正交的 React 组件》.md
│ ├── 133.精读《寻找框架设计的平衡点》.md
│ ├── 134.精读《我在阿里数据中台大前端》.md
│ ├── 138.精读《精通 console.log》.md
│ ├── 139.精读《手写 JSON Parser》.md
│ ├── 14.精读《架构设计之 DCI》.md
│ ├── 140.精读《结合 React 使用原生 Drag Drop API》.md
│ ├── 141.精读《useRef 与 createRef 的区别》.md
│ ├── 142.精读《如何做好 CodeReview》.md
│ ├── 143.精读《Suspense 改变开发方式》.md
│ ├── 144.精读《Webpack5 新特性 - 模块联邦》.md
│ ├── 145.精读《React Router v6》.md
│ ├── 146.精读《React Hooks 数据流》.md
│ ├── 147. 精读《@types react 值得注意的 TS 技巧》.md
│ ├── 148. 精读《React Error Boundaries》.md
│ ├── 149. 精读《React 性能调试》.md
│ ├── 15.精读《TC39 与 ECMAScript 提案》.md
│ ├── 150. 精读《Deno 1.0 你需要了解的》.md
│ ├── 152. 精读《recoil》.md
│ ├── 153. 精读《snowpack》.md
│ ├── 154. 精读《用 React 做按需渲染》.md
│ ├── 157. 精读《如何比较 Object 对象》.md
│ ├── 158. 精读《Typescript 4》.md
│ ├── 159. 精读《对低代码搭建的理解》.md
│ ├── 16.精读《CSS Animations vs Web Animations API》.md
│ ├── 160. 精读《函数缓存》.md
│ ├── 161.精读《可视化搭建思考 - 富文本搭建》.md
│ ├── 162.精读《Tasks, microtasks, queues and schedules》.md
│ ├── 163.精读《Spring 概念》.md
│ ├── 164.精读《数据搭建引擎 bi-designer API-设计器》.md
│ ├── 165.精读《数据搭建引擎 bi-designer API-组件》.md
│ ├── 166.精读《BI 搭建 - 筛选条件》.md
│ ├── 17.精读《如何安全地使用 React context》.md
│ ├── 18.精读《设计完美的日期选择器》.md
│ ├── 19.精读《最佳前端面试题》及面试官技巧.md
│ ├── 190.精读《DOM diff 原理详解》.md
│ ├── 191.精读《高性能表格》.md
│ ├── 192.精读《DOM diff 最长上升子序列》.md
│ ├── 193.精读《React Server Component》.md
│ ├── 194.精读《算法基础数据结构》.md
│ ├── 195.精读《新一代前端构建工具对比》.md
│ ├── 196.精读《前端职业规划 - 2021 年》.md
│ ├── 197.精读《低代码逻辑编排》.md
│ ├── 2.精读《模态框的最佳实践》.md
│ ├── 20.精读《Nestjs》文档.md
│ ├── 202.精读《React 18》.md
│ ├── 204.精读《默认、命名导出的区别》.md
│ ├── 205.精读《JS with 语法》.md
│ ├── 206.精读《一种 Hooks 数据流管理方案》.md
│ ├── 207.精读《Typescript infer 关键字》.md
│ ├── 208.精读《Typescript 4.4》.md
│ ├── 209.精读《捕获所有异步 error》.md
│ ├── 21.精读《Web fonts: when you need them, when you don’t》.md
│ ├── 210.精读《class static block》.md
│ ├── 211.精读《Microsoft Power Fx》.md
│ ├── 212.精读《可维护性思考》.md
│ ├── 213.精读《Prisma 的使用》.md
│ ├── 214.精读《web streams》.md
│ ├── 215.精读《什么是 LOD 表达式》.md
│ ├── 216.精读《15 大 LOD 表达式 - 上》.md
│ ├── 217.精读《15 大 LOD 表达式 - 下》.md
│ ├── 218.精读《Rust 是 JS 基建的未来》.md
│ ├── 219.精读《深入了解现代浏览器一》.md
│ ├── 22.精读《V8 引擎特性带来的的 JS 性能变化》.md
│ ├── 220.精读《深入了解现代浏览器二》.md
│ ├── 221.精读《深入了解现代浏览器三》.md
│ ├── 222.精读《深入了解现代浏览器四》.md
│ ├── 223.精读《Records & Tuples 提案》.md
│ ├── 224.精读《Records & Tuples for React》.md
│ ├── 225.精读《Excel JS API》.md
│ ├── 226.精读《2021 前端新秀回顾》.md
│ ├── 228.精读《pipe operator for JavaScript》.md
│ ├── 23.精读《API 设计原则》.md
│ ├── 230.精读《对 Markdown 的思考》.md
│ ├── 237.精读《Typescript 4.5-4.6 新特性》.md
│ ├── 238.精读《不再需要 JS 做的 5 件事》.md
│ ├── 239.精读《JS 数组的内部实现》.md
│ ├── 24.精读《现代 JavaScript 概览》.md
│ ├── 240.精读《React useEvent RFC》.md
│ ├── 242.精读《web reflow》.md
│ ├── 25.精读《null >= 0?》.md
│ ├── 253.精读《pnpm》.md
│ ├── 254.精读《对前端架构的理解 - 分层与抽象》.md
│ ├── 255.精读《SolidJS》.md
│ ├── 256.精读《依赖注入简介》.md
│ ├── 257.精读《State of CSS 2022》.md
│ ├── 258.精读《proposal-extractors》.md
│ ├── 259.精读《Headless 组件用法与原理》.md
│ ├── 26.精读《加密媒体扩展》.md
│ ├── 260.精读《如何为 TS 类型写单测》.md
│ ├── 261.精读《Rest vs Spread 语法》.md
│ ├── 262.精读《迭代器 Iterable》.md
│ ├── 263.精读《我们为何弃用 css-in-js》.md
│ ├── 264.精读《维护好一个复杂项目》.md
│ ├── 265.精读《磁贴布局 - 功能分析》.md
│ ├── 266.精读《磁贴布局 - 功能实现》.md
│ ├── 267.精读《磁贴布局 - 性能优化》.md
│ ├── 27.精读《css-in-js 杀鸡用牛刀》.md
│ ├── 277.精读《利用 GPT 解读 PDF》.md
│ ├── 28.精读《2017 前端性能优化备忘录》.md
│ ├── 281.精读《自由 + 磁贴混合布局》.md
│ ├── 282.精读《自由布局吸附线的实现》.md
│ ├── 287.精读《VisActor 数据可视化工具》.md
│ ├── 29.精读《JS 中的内存管理》.md
│ ├── 3.精读《前后端渲染之争》.md
│ ├── 30.精读《Javascript 事件循环与异步》.md
│ ├── 31.精读《我不再使用高阶组件》.md
│ ├── 32.精读《React Router4.0 进阶概念》.md
│ ├── 33.精读《30 行 js 代码创建神经网络》.md
│ ├── 34.精读《React 代码整洁之道》.md
│ ├── 35.精读《dob - 框架实现》.md
│ ├── 36.精读《When You “Git” in Trouble- a Version Control Story》.md
│ ├── 37.精读《how we position and what we compare》.md
│ ├── 38.精读《dob - 框架使用》.md
│ ├── 39.精读《全链路体验浏览器挖矿》.md
│ ├── 4.精读《AsyncAwait 优越之处》.md
│ ├── 40.精读《初探 Reason 与 GraphQL》.md
│ ├── 41.精读《Ant Design 3.0 背后的故事》.md
│ ├── 42.精读《前端数据流哲学》.md
│ ├── 43.精读《增强现实与可视化》.md
│ ├── 44.精读《Rekit Studio》.md
│ ├── 45.精读《React's new Context API》.md
│ ├── 46.精读《react-rxjs》.md
│ ├── 47.精读《webpack4.0 升级指南》.md
│ ├── 49.精读《Compilers are the New Frameworks》.md
│ ├── 5.精读《民工叔单页数据流方案》.md
│ ├── 50.精读《快速上手构建 ARKit 应用》.md
│ ├── 51.精读《Elements of Web Dev》.md
│ ├── 52.精读《图解 ES 模块》.md
│ ├── 53.精读《插件化思维》.md
│ ├── 54.精读《在浏览器运行 serverRender》.md
│ ├── 55.精读《async await 是把双刃剑》.md
│ ├── 56.精读《重新思考 Redux》.md
│ ├── 57.精读《现代 js 框架存在的根本原因》.md
│ ├── 58.精读《Typescript2.0 - 2.9》.md
│ ├── 59.精读《如何利用 Nodejs 监听文件夹》.md
│ ├── 6.精读《JavaScript 错误堆栈处理》.md
│ ├── 60.精读《如何在 nodejs 使用环境变量》.md
│ ├── 61.精读《React 八种条件渲染》.md
│ ├── 62.精读《JS 引擎基础之 Shapes and Inline Caches》.md
│ ├── 63.精读《React 的多态性》.md
│ ├── 68.精读《衡量用户体验》.md
│ ├── 69.精读《SQL vs Flux》.md
│ ├── 7.精读《请停止 css-in-js 的行为》.md
│ ├── 72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》.md
│ ├── 74.精读《12 个评估 JS 库你需要关心的事》.md
│ ├── 76.精读《谈谈 Web Workers》.md
│ ├── 77.精读《用 Reduce 实现 Promise 串行执行》.md
│ ├── 79.精读《React Hooks》.md
│ ├── 8.精读《入坑 React 前没有人会告诉你的事》.md
│ ├── 80.精读《怎么用 React Hooks 造轮子》.md
│ ├── 81.精读《使用 CSS 属性选择器》.md
│ ├── 83.精读《React16 新特性》.md
│ ├── 84.精读《Typescript 3.2 新特性》.md
│ ├── 86.精读《国际化布局 - Logical Properties》.md
│ ├── 87.精读《setState 做了什么》.md
│ ├── 88.精读《Caches API》.md
│ ├── 89.精读《如何编译前端项目与组件》.md
│ ├── 9.精读《Immutable 结构共享》.md
│ ├── 91.精读《正则 ES2018》.md
│ ├── 94.精读《Serverless 给前端带来了什么》.md
│ ├── 95.精读《Function VS Class 组件》.md
│ ├── 96.精读《useEffect 完全指南》.md
│ ├── 97.精读《编写有弹性的组件》.md
│ └── 99.精读《Scheduling in React》.md
├── 可视化搭建/
│ ├── 268.如何抽象可视化搭建.md
│ ├── 269.组件注册与画布渲染.md
│ ├── 270.画布与组件元信息数据流.md
│ ├── 271.可视化搭建内置 API.md
│ ├── 272.容器组件设计.md
│ ├── 273.组件值与联动.md
│ ├── 274.定义联动协议.md
│ ├── 275.组件值校验.md
│ ├── 276.keepAlive 模式.md
│ ├── 278.ComponentLoader 与动态组件.md
│ ├── 279.自动批处理与冻结.md
│ └── 280.场景实战.md
├── 商业思考/
│ ├── 103.精读《为什么专家不再关心技术细节》.md
│ ├── 106.精读《数据之上·智慧之光 - 2018》.md
│ ├── 108.精读《智能商业》.md
│ ├── 114.精读《谁在世界中心》.md
│ ├── 115.精读《Tableau 入门》.md
│ ├── 116.精读《刷新》.md
│ ├── 131.精读《从 0 到 1》.md
│ ├── 135.精读《极客公园 IFX - 上》.md
│ ├── 136.精读《极客公园 IFX - 下》.md
│ ├── 137.精读《当我在分享的时候,我在做什么?》.md
│ └── 90.精读《极客公园 2019》.md
├── 数学之美/
│ └── 296.手动算根号.md
├── 数据技术专家能力模型.md
├── 机器学习/
│ ├── 291.机器学习简介: 寻找函数的艺术.md
│ ├── 292.万能近似定理: 逼近任何函数的理论.md
│ ├── 293.实现万能近似函数: 神经网络的架构设计.md
│ ├── 294.反向传播: 揭秘神经网络的学习机制.md
│ └── 295.完整实现神经网络: 实战演练.md
├── 源码解读/
│ ├── 110.精读《Inject Instance 源码》.md
│ ├── 122.精读《robot 源码 - 有限状态机》.md
│ ├── 128.精读《Hooks 取数 - swr 源码》.md
│ ├── 130.精读《unstated 与 unstated-next 源码》.md
│ ├── 151. 精读《@umijs use-request》源码.md
│ ├── 155. 精读《use-what-changed 源码》.md
│ ├── 156. 精读《react-intersection-observer 源码》.md
│ ├── 227. 精读《zustand 源码》.md
│ ├── 229.精读《vue-lit 源码》.md
│ ├── 241.精读《react-snippets - Router 源码》.md
│ ├── 48.精读《Immer.js》源码.md
│ ├── 73.精读《sqorn 源码》.md
│ ├── 75.精读《Epitath 源码 - renderProps 新用法》.md
│ ├── 82.精读《Htm - Hyperscript 源码》.md
│ ├── 92.精读《React PowerPlug 源码》.md
│ ├── 93.精读《syntax-parser 源码》.md
│ └── 98.精读《react-easy-state 源码》.md
├── 生活/
│ └── 290.个人养老金利与弊.md
├── 算法/
│ ├── 198.精读《算法 - 动态规划》.md
│ ├── 199.精读《算法 - 滑动窗口》.md
│ ├── 200.精读《算法 - 回溯》.md
│ ├── 201.精读《算法 - 二叉树》.md
│ ├── 203.精读《算法 - 二叉搜索树》.md
│ ├── 283.精读《算法题 - 通配符匹配》.md
│ ├── 284.精读《算法题 - 统计可以被 K 整除的下标对数目》.md
│ ├── 285.精读《算法题 - 最小覆盖子串》.md
│ ├── 286.精读《算法题 - 地下城游戏》.md
│ ├── 288.精读《算法题 - 编辑距离》.md
│ └── 289.精读《算法题 - 二叉树中的最大路径和》.md
├── 编译原理/
│ ├── 64.精读《手写 SQL 编译器 - 词法分析》.md
│ ├── 65.精读《手写 SQL 编译器 - 文法介绍》.md
│ ├── 66.精读《手写 SQL 编译器 - 语法分析》.md
│ ├── 67.精读《手写 SQL 编译器 - 回溯》.md
│ ├── 70.精读《手写 SQL 编译器 - 语法树》.md
│ ├── 71.精读《手写 SQL 编译器 - 错误提示》.md
│ ├── 78.精读《手写 SQL 编译器 - 性能优化之缓存》.md
│ └── 85.精读《手写 SQL 编译器 - 智能提示》.md
└── 设计模式/
├── 167.精读《设计模式 - Abstract Factory 抽象工厂》.md
├── 168.精读《设计模式 - Builder 生成器》.md
├── 169.精读《设计模式 - Factory Method 工厂方法》.md
├── 170.精读《设计模式 - Prototype 原型模式》.md
├── 171.精读《设计模式 - Singleton 单例模式》.md
├── 172.精读《设计模式 - Adapter 适配器模式》.md
├── 173.精读《设计模式 - Bridge 桥接模式》.md
├── 174.精读《设计模式 - Composite 组合模式》.md
├── 175.精读《设计模式 - Decorator 装饰器模式》.md
├── 176.精读《设计模式 - Facade 外观模式》.md
├── 177.精读《设计模式 - Flyweight 享元模式》.md
├── 178.精读《设计模式 - Proxy 代理模式》.md
├── 179.精读《设计模式 - Chain of Responsibility 职责链模式》.md
├── 180.精读《设计模式 - Command 命令模式》.md
├── 181.精读《设计模式 - Interpreter 解释器模式》.md
├── 182.精读《设计模式 - Iterator 迭代器模式》.md
├── 183.精读《设计模式 - Mediator 中介者模式》.md
├── 184.精读《设计模式 - Memoto 备忘录模式》.md
├── 185.精读《设计模式 - Observer 观察者模式》.md
├── 186.精读《设计模式 - State 状态模式》.md
├── 187.精读《设计模式 - Strategy 策略模式》.md
├── 188.精读《设计模式 - Template Method 模版模式》.md
└── 189.精读《设计模式 - Visitor 访问者模式》.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/node_modules
/yarn.lock
================================================
FILE: .lintmdrc
================================================
{
"excludeFiles": [],
"rules": {
"no-long-code": 0,
"no-trailing-punctuation": 0
}
}
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "10"
before_install:
- npm i -g lint-md-cli
script: lint-md ./
================================================
FILE: SQL/231.SQL 入门.md
================================================
本系列是 SQL 系列的开篇,介绍一些宏观与基础的内容。
## SQL 是什么?
SQL 是一种结构化查询语言,用于管理关系型数据库,我们 90% 接触的都是查询语法,但其实它包含完整的增删改查和事物处理功能。
## 声明式特性
SQL 属于声明式编程语言,而现代通用编程语言一般都是命令式的。但是不要盲目崇拜声明式语言,比如说它未来会代替低级的命令式语言,因为声明式本身也有它的缺点,它与命令式语言也有相通的地方。
为什么我们觉得声明式编程语言更高级?因为声明式语言抽象程度更高,比如 `select * from table1` 仅描述了要从 table1 查询数据,但查询的具体步骤的完全没提,这背后可能存在复杂的索引优化与锁机制,但我们都无需关心,这简直是编程的最高境界。
那为什么现在所有通用业务代码都是命令式呢?因为 **命令式给了我们描述具体实现的机会** ,而通用领域的编程正需要建立在严谨的实现细节上。比如校验用户权限这件事,即便 AI 编程提供了将 “登陆用户仅能访问有权限的资源” 转化为代码的能力,我们也不清楚资源具体指哪些,以及在权限转移过程中的资源所有权属于谁。
SQL 之所以能保留声明式特性,完全因为锁定了关系型数据管理这个特定领域,而恰恰对这个领域的需求是标准化且可枚举的,才使声明式成为可能。
基于命令式语言也完全可拓展出声明式能力,比如许多 ORM 提供了类似 `select({}).from({}).where({})` 之类的语法,甚至一个 `login()` 函数也是声明式编程的体现,因为调用者无需关心是如何登陆的,总之调用一下就完成了登陆,这不就是声明式的全部精髓吗?
## 语法分类
作为关系型数据库管理工具,SQL 需要定义、操纵与控制数据。
数据定义即修改数据库与表级别结构,这些是数据结构,或者是数据元信息,它不代表具体数据,但描述数据的属性。
数据操纵即修改一行行具体数据,增删改查。
数据控制即对事务、用户权限的管理与控制。
### 数据定义
DDL(Data Definition Language)数据定义,包括 `CREATE` `DROP` `ALTER` 方法。
### 数据操纵
DML(Data Manipulation Language)数据操纵,包括 `SELECT` `INSERT` `UPDATE` `DELETE` 方法。
### 数据控制
DCL(Data Control Language)数据控制,包括 `COMMIT`、`ROLLBACK` 等。
所有 SQL 操作都围绕这三种类型,其中数据操纵几乎占了 90% 的代码量,毕竟数据查询的诉求远大于写,数据写入对应数据采集,而数据查询对应数据分析,数据分析领域能玩出的花样远比数据采集要多。
PS:有些情况下,会把最重要的 `SELECT` 提到 DQL(Data Query Language)分类下,这样分类就变成了四个。
## 集合运算
SQL 世界的第一公民是集合,就像 JAVA 世界第一公民是对象。我们只有以集合的视角看待 SQL,才能更好的理解它。
何为集合视角,即所有的查询、操作都是二维数据结构中进行的,而非小学算术里的单个数字间加减乘除关系。
集合的运算一般有 `UNION` 并集、`EXCEPT` 差集、`INTERSECT` 交集,这些都是以行为单位的操作,而各种 JOIN 语句则是以列为单位的集合运算,也是后面提到的连接查询。
只要站在二维数据结构中进行思考,运算无非是横向或纵向的操作。
## 数据范式
数据范式分为五层,每层要求都比上一层更严苛,因此是一个可以逐步遵循的范式。数据范式要求数据越来越解耦,减少冗余。
比如第一范式要求每列都具有原子性,即都是不可分割的最小数据单元。如果数据采集时,某一列作为字符串存储,并且以 "|" 分割表示省市区,那么它就不具有原子性。
当然实际生产过程往往不都遵循这种标准,因为表不是孤立的,在数据处理流中,可能在某个环节再把列原子化,而原始数据为了压缩体积,进行列合并处理。
希望违反范式的还不仅是底层表,现在大数据处理场景下,越来越多的业务采用大宽表结构,甚至故意进行数据冗余以提升查询效率,列存储引擎就是针对这种场景设计的,所以数据范式在大数据场景下是可以变通的,但依然值得学习。
## 聚合
当采用 GROUP BY 分组聚合数据时,如希望针对聚合值筛选,就不能用 WHERE 限定条件了,因为 WHERE 是基于行的筛选,而不是针对组合的。(GROUP BY 对数据进行分组,我们称这些组为 “组合”),所以需要使用针对组合的筛选语句 HAVING:
```sql
SELECT SUM(pv) FROM table
GROUP BY city
HAVING AVG(uv) > 100
```
这个例子中,如果 HAVING 换成 WHERE 就没有意义,因为 WHERE 加聚合条件时,需要对所有数据进行合并,不符合当前视图的详细级别。(关于视图详细级别,在我之前写的 [精读《什么是 LOD 表达式》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/215.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E3%80%8B.md) 有详细说明)。
聚合如此重要,是因为我们分析数据必须在高 LEVEL 视角看,明细数据是看不出趋势的。而复杂的需求往往伴随着带有聚合的筛选条件,明白 SQL 是如何支持的非常重要。
## CASE 表达式
CASE 表达式分为简单与搜索 CASE 表达式,简单表达式:
```sql
SELECT CASE pv WHEN 1 THEN 'low' ELSE 'high' END AS quality
```
上面的例子利用 CASE 简单表达式形成了一个新字段,这种模式等于生成了业务自定义临时字段,在对当前表进行数据加工时非常有用。搜索 CASE 表达式能力完全覆盖简单 CASE 表达式:
```sql
SELECT CASE WHEN pv < 100 THEN 'low' ELSE 'high' END AS quality
```
可以看到,搜索 CASE 表达式可以用 “表达式” 描述条件,可以轻松完成更复杂的任务,甚至可以在表达式里使用子查询、聚合等手段,这些都是高手写 SQL 的惯用技巧,所以 CASE 表达式非常值得深入学习。
## 复杂查询
SELECT 是 SQL 最复杂的部分,其中就包含三种复杂查询模式,分别是连接查询与子查询。
### 连接查询
指 JOIN 查询,比如 LEFT JOIN、RIGHT JOIN、INNER JOIN。
在介绍聚合时我们提到了,连接查询本质上就是对列进行拓展,而两个表之间不会无缘无故合成一个,所以必须有一个外键作为关系纽带:
```sql
SELECT A.pv, B.uv
FROM table1 as t1 LEFT JOIN table2 AS P t2
ON t1.productId = t2.productId
```
连接查询不仅拓展了列,还会随之拓展行,而拓展方式与连接的查询的类型有关。除了连接查询别的表,还可以连接查询自己,比如:
```sql
SELECT t1.pv AS pv1, P2.pv AS pv2
FROM tt t1, tt t2
```
这种子连接查询结果就是自己对自己的笛卡尔积,可通过 WHERE 筛选去重,后面会有文章专门介绍。
### 子查询与视图
子查询就是 SELECT 里套 SELECT,一般来说 SELECT 会从内到外执行,只有在关联子查询模式下,才会从外到内执行。
而如果把子查询保存下来,就是一个视图,这个视图并不是实体表,所以很灵活,且数据会随着原始表数据而变化:
```sql
CREATE VIEW countryGDP (country, gdp)
AS
SELECT country, SUM(gdp)
FROM tt
GROUP BY country
```
之后 `countryGDP` 这个视图就可以作为临时表来用了。
这种模式其实有点违背 SQL 声明式的特点,因为定义视图类似于定义变量,如果继续写下去,势必会形成一定命令式思维逻辑,但这是无法避免的。
## 事务
当 SQL 执行一连串操作时,难免遇到不执行完就会出现脏数据的问题,所以事务可以保证操作的原子性。一般来说每个 DML 操作都是一个内置事务,而 SQL 提供的 START TRANSACTION 就是让我们可以自定义事务范围,使一连串业务操作都可以包装在一起,成为一个原子性操作。
对 SQL 来说,原子性操作是非常安全的,即失败了不会留下任何痕迹,成功了会全部成功,不会存在中间态。
## OLAP
OLAP(OnLine Analytical Processing)即实时数据分析,是 BI 工具背后计算引擎实现的基础。
现在越来越多的 SQL 数据库支持了窗口函数实现,用于实现业务上的 runningSum 或 runningAvg 等功能,这些都是数据分析中很常见的。
以 runningSum 为例,比如双十一实时表的数据是以分钟为单位的实时 GMV,而我们要做一张累计到当前时间的 GMV 汇总折线图,Y 轴就需要支持 `running_sum(GMV)` 这样的表达式,而这背后可能就是通过窗口函数实现的。
当然也不是所有业务函数都由 SQL 直接提供,业务层仍需实现大量内存函数,在 JAVA 层计算,这其中一部分是需要下推到 SQL 执行的,只有内存函数与下推函数结合在一起,才能形成我们在 BI 工具看到的复杂计算字段效果。
## 总结
SQL 是一种声明式语言,一个看似简单的查询语句,在引擎层往往对应着复杂的实现,这就是 SQL 为何如此重要却又如此普及的原因。
虽然 SQL 容易上手,但要系统的理解它,还得从结构化数据与集合的概念开始进行思想转变。
不要小看 CASE 语法,它不仅与容易与编程语言的 CASE 语法产生混淆,本身结合表达式进行条件分支判断,是许多数据分析师在日常工作中最长用的套路。
现在使用简单 SQL 创建应用的场景越来越少了,但 BI 场景下,基于 SQL 的增强表达式场景越来越多了,本系列我就是以理解 BI 场景下查询表达式为目标创建的,希望能够学以致用。
> 讨论地址是:[精读《SQL 入门》· Issue #398 · ascoders/weekly](https://github.com/ascoders/weekly/issues/398)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/232.SQL 聚合查询.md
================================================
SQL 为什么要支持聚合查询呢?
这看上去是个幼稚的问题,但我们还是一步步思考一下。数据以行为粒度存储,最简单的 SQL 语句是 `select * from test`,拿到的是整个二维表明细,但仅做到这一点远远不够,出于以下两个目的,需要 SQL 提供聚合函数:
1. 明细数据没有统计意义,比如我想知道今天的营业额一共有多少,而不太关心某桌客人消费了多少。
2. 虽然可以先把数据查到内存中再聚合,但在数据量非常大的情况下很容易把内存撑爆,可能一张表一天的数据量就有 10TB,而 10TB 数据就算能读到内存里,聚合计算可能也会慢到难以接受。
另外聚合本身也有一定逻辑复杂度,而 SQL 提供了聚合函数与分组聚合能力,可以方便快速的统计出有业务价值的聚合数据,这奠定了 SQL 语言的分析价值,因此大部分分析软件直接采用 SQL 作为直接面向用户的表达式。
## 聚合函数
常见的聚合函数有:
- COUNT:计数。
- SUM:求和。
- AVG:求平均值。
- MAX:求最大值。
- MIN:求最小值。
### COUNT
COUNT 用来计算有多少条数据,比如我们看 id 这一列有多少条:
```sql
SELECT COUNT(id) FROM test
```
但我们发现其实查任何一列的 COUNT 都是一样的,那传入 id 有什么意义呢?没必要特殊找一个具体列指代呀,所以也可以写成:
```sql
SELECT COUNT(*) FROM test
```
但这两者存在微妙差异。SQL 存在一种很特殊的值类型 `NULL`,如果 COUNT 指定了具体列,则统计时会跳过此列值为 `NULL` 的行,而 `COUNT(*)` 由于未指定具体列,所以就算包含了 `NULL`,甚至某一行所有列都为 `NULL`,也都会包含进来。所以 `COUNT(*)` 查出的结果一定大于等于 `COUNT(c1)`。
当然任何聚合函数都可以跟随查询条件 WHERE,比如:
```sql
SELECT COUNT(*) FROM test
WHERE is_gray = 1
```
### SUM
SUM 求和所有项,因此必须作用于数值字段,而不能用于字符串。
```sql
SELECT SUM(cost) FROM test
```
SUM 遇到 NULL 值时当 0 处理,因为这等价于忽略。
### AVG
AVG 求所有项均值,因此必须作用于数值字段,而不能用于字符串。
```sql
SELECT AVG(cost) FROM test
```
AVG 遇到 NULL 值时采用了最彻底的忽略方式,即 NULL 完全不参与分子与分母的计算,就像这一行数据不存在一样。
### MAX、MIN
MAX、MIN 分别求最大与最小值,与上面不同的是,也可以作用于字符串上,因此可以根据字母判断大小,从大到小依次对应 `a-z`,但即便能算,也没有实际意义且不好理解,因此不建议对字符串求极值。
```sql
SELECT MAX(cost) FROM test
```
### 多个聚合字段
虽然都是聚合函数,但 MAX、MIN 严格意义上不算是聚合函数,因为它们只是寻找了满足条件的行。可以看看下面两段查询结果的对比:
```sql
SELECT MAX(cost), id FROM test -- id: 100
SELECT SUM(cost), id FROM test -- id: 1
```
第一条查询可以找到最大值那一行的 id,而第二条查询的 id 是无意义的,因为不知道归属在哪一行,所以只返回了第一条数据的 id。
当然,如果同时计算 MAX、MIN,那么此时 id 也只返回第一条数据的值,因为这个查询结果对应了复数行:
```sql
SELECT MAX(cost), MIN(cost), id FROM test -- id: 1
```
基于这些特性,最好不要混用聚合与非聚合,也就是一条查询一旦有一个字段是聚合的,那么所有字段都要聚合。
现在很多 BI 引擎的自定义字段都有这条限制,因为混用聚合与非聚合在自定义内存计算时处理起来边界情况很多,虽然 SQL 能支持,但业务自定义的函数可能不支持。
## 分组聚合
分组聚合就是 GROUP BY,其实可以把它当作一种高级的条件语句。
举个例子,查询每个国家的 GDP 总量:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY country
```
返回的结果就会按照国家进行分组,这时,聚合函数就变成了在组内聚合。
其实如果我们只想看中、美的 GDP,用非分组也可以查,只是要分成两条 SQL:
```sql
SELECT SUM(GDP) FROM amazing_table
WHERE country = '中国'
SELECT SUM(GDP) FROM amazing_table
WHERE country = '美国'
```
所以 GROUP BY 也可理解为,将某个字段的所有可枚举的情况都查了出来,并整合成一张表,每一行代表了一种枚举情况,不需要分解为一个个 WHERE 查询了。
### 多字段分组聚合
GROUP BY 可以对多个维度使用,含义等价于表格查询时行/列拖入多个维度。
上面是 BI 查询工具视角,如果没有上下文,可以看下面这个递进描述:
- 按照多个字段进行分组聚合。
- 多字段组合起来成为唯一 Key,即 `GROUP BY a,b` 表示 a,b 合在一起描述一个组。
- `GROUP BY a,b,c` 查询结果第一列可能看到许多重复的 a 行,第二列看到重复 b 行,但在同一个 a 值内不会重复,c 在 b 行中同理。
下面是一个例子:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY province, city, area
```
查询结果为:
```text
浙江 杭州 余杭区
浙江 杭州 西湖区
浙江 宁波 海曙区
浙江 宁波 江北区
北京 .........
```
### GROUP BY + WHERE
WHERE 是根据行进行条件筛选的。因此 GROUP BY + WHERE 并不是在组内做筛选,而是对整体做筛选。
但由于按行筛选,其实组内或非组内结果都完全一样,所以我们几乎无法感知这种差异:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY province, city, area
WHERE industry = 'internet'
```
然而,忽略这个差异会导致我们在聚合筛选时碰壁。
比如要筛选出平均分大于 60 学生的成绩总和,如果不使用子查询,是无法在普通查询中在 WHERE 加聚合函数实现的,比如下面就是一个语法错误的例子:
```sql
SELECT SUM(score) FROM amazing_table
WHERE AVG(score) > 60
```
不要幻想上面的 SQL 可以执行成功,不要在 WHERE 里使用聚合函数。
### GROUP BY + HAVING
HAVING 是根据组进行条件筛选的。因此可以在 HAVING 使用聚合函数:
```sql
SELECT SUM(score) FROM amazing_table
GROUP BY class_name
HAVING AVG(score) > 60
```
上面的例子中可以正常查询,表示按照班级分组看总分,且仅筛选出平均分大于 60 的班级。
所以为什么 HAVING 可以使用聚合条件呢?因为 HAVING 筛选的是组,所以可以对组聚合后过滤掉不满足条件的组,这样是有意义的。而 WHERE 是针对行粒度的,聚合后全表就只有一条数据,无论过滤与否都没有意义。
但要注意的是,GROUP BY 生成派生表是无法利用索引筛选的,所以 WHERE 可以利用给字段建立索引优化性能,而 HAVING 针对索引字段不起作用。
## 总结
聚合函数 + 分组可以实现大部分简单 SQL 需求,在写 SQL 表达式时,需要思考这样的表达式是如何计算的,比如 `MAX(c1), c2` 是合理的,而 `SUM(c1), c2` 这个 `c2` 就是无意义的。
最后记住 WHERE 是 GROUP BY 之前执行的,HAVING 针对组进行筛选。
> 讨论地址是:[精读《SQL 聚合查询》· Issue #401 · ascoders/weekly](https://github.com/ascoders/weekly/issues/401)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/233.SQL 复杂查询.md
================================================
SQL 复杂查询指的就是子查询。
为什么子查询叫做复杂查询呢?因为子查询相当于查询嵌套查询,因为嵌套导致复杂度几乎可以被无限放大(无限嵌套),因此叫复杂查询。下面是一个最简单的子查询例子:
```sql
SELECT pv FROM (
SELECT pv FROM test
)
```
上面的例子等价于 `SELECT pv FROM test`,但因为把表的位置替换成了一个新查询,所以摇身一变成为了复杂查询!所以复杂查询不一定真的复杂,甚至可能写出和普通查询等价的复杂查询,要避免这种无意义的行为。
我们也要借此机会了解为什么子查询可以这么做。
### 理解查询的本质
当我们查一张表时,数据库认为我们在查什么?
这点很重要,因为下面两个语句都是合法的:
```sql
SELECT pv FROM test
SELECT pv FROM (
SELECT pv FROM test
)
```
为什么数据库可以把子查询当作表呢?为了统一理解这些概念,我们有必要对查询内容进行抽象理解:**任意查询位置都是一条或多条记录**。
比如 `test` 这张表,显然是多条记录(当然只有一行就是一条记录),而 `SELECT pv FROM test` 也是多条记录,然而因为 `FROM` 后面可以查询任意条数的记录,所以这两种语法都支持。
不仅是 `FROM` 可以跟单条或多条记录,甚至 `SELECT`、`GROUP BY`、`WHERE`、`HAVING` 后都可以跟多条记录,这个后面再说。
说到这,也就很好理解子查询的变种了,比如我们可以在子查询内使用 `WHERE` 或 `GROUP BY` 等等,因为无论如何,只要查询结果是多条记录就行了:
```sql
SELECT sum(people) as allPeople, sum(gdp), city FROM (
SELECT people, gdp, city FROM test
GROUP BY city
HAVING sum(gdp) > 10000
)
```
这个例子就有点业务含义了。子查询是从内而外执行的,因此我们先看内部的逻辑:按照城市分组,筛选出总 GDP 超过一万的所有地区的人口数量明细。外层查询再把人口数加总,这样就能对比每个 GDP 超过一万的地区,总人口和总 GDP 分别是多少,方便对这些重点城市做对比。
不过这个例子看起来还是不太自然,因为我们没必要写成复杂查询,其实简单查询也是等价的:
```sql
SELECT sum(people) as allPeople, sum(gdp), city FROM test
GROUP BY city
HAVING sum(gdp) > 10000
```
那为什么要多此一举呢?因为复杂查询的真正用法并不在这里。
### 视图
正因为子查询的存在,我们才可能以类似抽取变量的方式,抽取子查询,这个抽取出来的抽象就是视图:
```sql
CREATE VIEW my_table(people, gdp, city)
AS
SELECT sum(people) as allPeople, sum(gdp), city FROM test
GROUP BY city
HAVING sum(gdp) > 10000
SELECT sum(people) as allPeople, sum(gdp), city FROM my_table
```
这样的好处是,这个视图可以被多条 SQL 语句复用,不仅可维护性变好了,执行时也仅需查询一次。
要注意的是,SELECT 可以使用任何视图,但 INSERT、DELETE、UPDATE 用于视图时,需要视图满足一下条件:
1. 未使用 DISTINCT 去重。
2. FROM 单表。
3. 未使用 GROUP BY 和 HAVING。
因为上面几种模式都会导致视图成为聚合后的数据,不方便做除了查以外的操作。
另外一个知识点就是物化视图,即使用 MATERIALIZED 描述视图:
```sql
CREATE MATERIALIZED VIEW my_table(people, gdp, city)
AS ...
```
这种视图会落盘,为什么要支持这个特性呢?因为普通视图作为临时表,无法利用索引等优化手段,查询性能较低,所以物化视图是较为常见的性能优化手段。
说到性能优化手段,还有一些比较常见的理念,即把读的复杂度分摊到写的时候,比如提前聚合新表落盘或者对 CASE 语句固化为字段等,这里先不展开。
### 标量子查询
上面说了,WHERE 也可以跟子查询,比如:
```sql
SELECT city FROM test
WHERE gdp > (
SELECT avg(gdp) from test
)
```
这样可以查询出 gdp 大于平均值的城市。
那为什么不能直接这么写呢?
```sql
SELECT city FROM test
WHERE gdp > avg(gdp) -- 报错,WHERE 无法使用聚合函数
```
看上去很美好,但其实第一篇我们就介绍了,WHERE 不能跟聚合查询,因为这样会把整个父查询都聚合起来。那为什么子查询可以?因为子查询聚合的是子查询啊,父查询并没有被聚合,所以这才符合我们的意图。
所以上面例子不合适的地方在于,直接在当前查询使用 `avg(gdp)` 会导致聚合,而我们并不想聚合当前查询,但又要通过聚合拿到平均 GDP,所以就要使用子查询了!
回过头来看,为什么这一节叫标量子查询?标量即单一值,因为 `avg(gdp)` 聚合出来的只有一个值,所以 WHERE 可以把它当做一个单一数值使用。反之,如果子查询没有使用聚合函数,或 GROUP BY 分组,那么就不能使用 `WHERE >` 这种语法,但可以使用 `WHERE IN`,这涉及到单条与多条记录的思考,我们接着看下一节。
### 单条和多条记录
介绍标量子查询时说到了,`WHERE >` 的值必须时单一值。但其实 WHERE 也可以跟返回多条记录的子查询结果,只要使用合理的条件语句,比如 IN:
```sql
SELECT area FROM test
WHERE gdp IN (
SELECT max(gdp) from test
GROUP BY city
)
```
上面的例子,子查询按照城市分组,并找到每一组 GDP 最大的那条记录,所以如果数据粒度是区域,那么我们就查到了每个城市 GDP 最大的那些记录,然后父查询通过 WHERE IN 找到 gdp 符合的复数结果,所以最后就把每个城市最大 gdp 的区域列了出来。
但实际上 `WHERE >` 语句跟复数查询结果也不会报错,但没有任何意义,所以我们要理解查询结果是单条还是多条,在 WHERE 判断时选择合适的条件。WHERE 适合跟复数查询结果的语法有:`WHERE IN`、`WHERE SOME`、`WHERE ANY`。
### 关联子查询
所谓关联子查询,即父子查询间存在关联,既然如此,子查询肯定不能单独优先执行,毕竟和父查询存在关联嘛,所以关联子查询是先执行外层查询,再执行内层查询的。要注意的是,对每一行父查询,子查询都会执行一次,因此性能不高(当然 SQL 会对相同参数的子查询结果做缓存)。
那这个关联是什么呢?关联的是每一行父查询时,对子查询执行的条件。这么说可能有点绕,举个例子:
```sql
SELECT * FROM test where gdp > (
select avg(gdp) from test
group by city
)
```
对这个例子来说,想要查找 gdp 大于按城市分组的平均 gdp,比如北京地区按北京比较,上海地区按上海比较。但很可惜这样做是不行的,因为父子查询没有关联,SQL 并不知道要按照相同城市比较,因此只要加一个 WHERE 条件,就变成关联子查询了:
```sql
SELECT * FROM test as t1 where gdp > (
select avg(gdp) from test as t2 where t1.city = t2.city
group by city
)
```
就是在每次判断 `WHERE gdp >` 条件时,重新计算子查询结果,将平均值限定在相同的城市,这样就符合需求了。
## 总结
学会灵活运用父子查询,就掌握了复杂查询了。
SQL 第一公民是集合,所以所谓父子查询就是父子集合的灵活组合,这些集合可以出现在几乎任何位置,根据集合的数量、是否聚合、关联条件,就派生出了标量查询、关联子查询。
更深入的了解就需要大量实战案例了,但万变不离其宗,掌握了复杂查询后,就可以理解大部分 SQL 案例了。
> 讨论地址是:[精读《SQL 复杂查询》· Issue #403 · ascoders/weekly](https://github.com/ascoders/weekly/issues/403)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/234.SQL CASE 表达式.md
================================================
CASE 表达式分为简单表达式与搜索表达式,其中搜索表达式可以覆盖简单表达式的全部能力,我也建议只写搜索表达式,而不要写简单表达式。
简单表达式:
```sql
SELECT CASE city
WHEN '北京' THEN 1
WHEN '天津' THEN 2
ELSE 0
END AS abc
FROM test
```
搜索表达式:
```sql
SELECT CASE
WHEN city = '北京' THEN 1
WHEN city = '天津' THEN 2
ELSE 0
END AS abc
FROM test
```
明显可以看出,简单表达式只是搜索表达式 `a = b` 的特例,因为无法书写任何符号,只要条件换成 `a > b` 就无法胜任了,而搜索表达式不但可以轻松胜任,甚至可以写聚合函数。
## CASE 表达式里的聚合函数
为什么 CASE 表达式里可以写聚合函数?
因为本身表达式就支持聚合函数,比如下面的语法,我们不会觉得奇怪:
```sql
SELECT sum(pv), avg(uv) from test
```
本身 SQL 就支持多种不同的聚合方式同时计算,所以将其用在 CASE 表达式里,也是顺其自然的:
```sql
SELECT CASE
WHEN count(city) = 100 THEN 1
WHEN sum(dau) > 200 THEN 2
ELSE 0
END AS abc
FROM test
```
只要 SQL 表达式中存在聚合函数,那么整个表达式都聚合了,此时访问非聚合变量没有任何意义。所以上面的例子,即便在 CASE 表达式中使用了聚合,其实也不过是聚合了一次后,按照条件进行判断罢了。
这个特性可以解决很多实际问题,比如将一些复杂聚合判断条件的结果用 SQL 结构输出,那么很可能是下面这种写法:
```sql
SELECT CASE
WHEN 聚合函数(字段) 符合什么条件 THEN xxx
... 可能有 N 个
ELSE NULL
END AS abc
FROM test
```
这也可以认为是一种行转列的过程,即 **把行聚合后的结果通过一条条 CASE 表达式形成一个个新的列**。
## 聚合与非聚合不能混用
我们希望利用 CASE 表达式找出那些 pv 大于平均值的行,以下这种想当然的写法是错误的:
```sql
SELECT CASE
WHEN pv > avg(pv) THEN 'yes'
ELSE 'no'
END AS abc
FROM test
```
原因是,只要 SQL 中存在聚合表达式,那么整条 SQL 就都是聚合的,所以返回的结果只有一条,而我们期望查询结果不聚合,只是判断条件用到了聚合结果,那么就要使用子查询。
为什么子查询可以解决问题?因为子查询的聚合发生在子查询,而不影响当前父查询,理解了这一点,就知道为什么下面的写法才是正确的了:
```sql
SELECT CASE
WHEN pv > ( SELECT avg(pv) from test ) THEN 'yes'
ELSE 'no'
END AS abc
FROM test
```
这个例子也说明了 CASE 表达式里可以使用子查询,因为子查询是先计算的,所以查询结果在哪儿都能用,CASE 表达式也不例外。
## WHERE 中的 CASE
WHERE 后面也可以跟 CASE 表达式的,用来做一些需要特殊枚举处理的筛选。
比如下面的例子:
```sql
SELECT * FROM demo WHERE
CASE
WHEN city = '北京' THEN true
ELSE ID > 5
END
```
本来我们要查询 ID 大于 5 的数据,但我想对北京这个城市特别对待,那么就可以在判断条件中再进行 CASE 分支判断。
这个场景在 BI 工具里等价于,创建一个 CASE 表达式字段,可以拖入筛选条件生效。
## GROUP BY 中的 CASE
想不到吧,GROUP BY 里都可以写 CASE 表达式:
```sql
SELECT isPower, sum(gdp) FROM test GROUP BY CASE
WHEN isPower = 1 THEN city, area
ELSE city
END
```
上面例子表示,计算 GDP 时,对于非常发达的城市,按照每个区粒度查看聚合结果,也就是看的粒度更细一些,而对于欠发达地区,本身 gdp 也不高,直接按照城市粒度看聚合结果。
这样,就按照不同的条件对数据进行了分组聚合。由于返回行结果是混在一起的,像这个例子,可以根据 isPower 字段是否为 1 判断,是否按照城市、区域进行了聚合,如果没有其他更显著的标识,可能导致无法区分不同行的聚合粒度,因此谨慎使用。
## ORDER BY 中的 CASE
同样,ORDER BY 使用 CASE 表达式,会将排序结果按照 CASE 分类进行分组,每组按照自己的规则排序,比如:
```sql
SELECT * FROM test ORDER BY CASE
WHEN isPower = 1 THEN gdp
ELSE people
END
```
上面的例子,对发达地区采用 gdp 排序,否则采用人口数量排序。
## 总结
CASE 表达式总结一下有如下特点:
1. 支持简单与搜索两种写法,推荐搜索写法。
2. 支持聚合与子查询,需要注意不同情况的特点。
3. 可以写在 SQL 查询的几乎任何地方,只要是可以写字段的地方,基本上就可以替换为 CASE 表达式。
4. 除了 SELECT 外,CASE 表达式还广泛应用在 INSERT 与 UPDATE,其中 UPDATE 的妙用是不用将 SQL 拆分为多条,所以不用担心数据变更后对判断条件的二次影响。
> 讨论地址是:[精读《SQL CASE 表达式》· Issue #404 · ascoders/weekly](https://github.com/ascoders/weekly/issues/404)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/235.SQL 窗口函数.md
================================================
窗口函数形如:
```sql
表达式 OVER (PARTITION BY 分组字段 ORDER BY 排序字段)
```
有两个能力:
1. 当表达式为 `rank()` `dense_rank()` `row_number()` 时,拥有分组排序能力。
2. 当表达式为 `sum()` 等聚合函数时,拥有累计聚合能力。
无论何种能力,**窗口函数都不会影响数据行数,而是将计算平摊在每一行**。
这两种能力需要区分理解。
## 底表
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdUL60.png">
以上是示例底表,共有 8 条数据,城市1、城市2 两个城市,下面各有地区1~4,每条数据都有该数据的人口数。
## 分组排序
如果按照人口排序,`ORDER BY people` 就行了,但如果我们想在城市内排序怎么办?
此时就要用到窗口函数的分组排序能力:
<img width=500 src="https://s1.ax1x.com/2022/03/26/qddfIg.png">
```sql
SELECT *, rank() over (PARTITION BY city ORDER BY people) FROM test
```
该 SQL 表示在 city 组内按照 people 进行排序。
其实 PARTITION BY 也是可选的,如果我们忽略它:
```sql
SELECT *, rank() over (ORDER BY people) FROM test
```
也是生效的,但该语句与普通 ORDER BY 等价,因此利用窗口函数进行分组排序时,一般都会使用 PARTITION BY。
### 各分组排序函数的差异
我们将 `rank()` `dense_rank()` `row_number()` 的结果都打印出来:
```sql
SELECT *,
rank() over (PARTITION BY city ORDER BY people),
dense_rank() over (PARTITION BY city ORDER BY people),
row_number() over (PARTITION BY city ORDER BY people)
FROM test
```
<img width=500 src="https://s1.ax1x.com/2022/03/26/qd0uh4.png">
其实从结果就可以猜到,这三个函数在处理排序遇到相同值时,对排名统计逻辑有如下差异:
1. `rank()`: 值相同时排名相同,但占用排名数字。
2. `dense_rank()`: 值相同时排名相同,但不占用排名数字,整体排名更加紧凑。
3. `row_number()`: 无论值是否相同,都强制按照行号展示排名。
上面的例子可以优化一下,因为所有窗口逻辑都是相同的,我们可以利用 WINDOW AS 提取为一个变量:
```sql
SELECT *,
rank() over wd, dense_rank() over wd, row_number() over wd
FROM test
WINDOW wd as (PARTITION BY city ORDER BY people)
```
## 累计聚合
我们之前说过,凡事使用了聚合函数,都会让查询变成聚合模式。如果不用 GROUP BY,聚合后返回行数会压缩为一行,即使用了 GROUP BY,返回的行数一般也会大大减少,因为分组聚合了。
然而使用窗口函数的聚合却不会导致返回行数减少,那么这种聚合是怎么计算的呢?我们不如直接看下面的例子:
```sql
SELECT *,
sum(people) over (PARTITION BY city ORDER BY people)
FROM test
```
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdsDJS.png">
可以看到,在每个 city 分组内,按照 people 排序后进行了 **累加**(相同的值会合并在一起),这就是 BI 工具一般说的 RUNNGIN_SUM 的实现思路,当然一般我们排序规则使用绝对不会重复的日期,所以不会遇到第一个红框中合并计算的问题。
累计函数还有 `avg()` `min()` 等等,这些都一样可以作用于窗口函数,其逻辑可以按照下图理解:
<img width=400 src="https://s1.ax1x.com/2022/03/26/qd6or9.png">
你可能有疑问,直接 `sum(上一行结果,下一行)` 不是更方便吗?为了验证猜想,我们试试 `avg()` 的结果:
<img width=400 src="https://s1.ax1x.com/2022/03/26/qdciIP.png">
可见,如果直接利用上一行结果的缓存,那么 avg 结果必然是不准确的,所以窗口累计聚合是每行重新计算的。当然也不排除对于 sum、max、min 做额外性能优化的可能性,但 avg 只能每行重头计算。
### 与 GROUP BY 组合使用
窗口函数是可以与 GROUP BY 组合使用的,遵循的规则是,窗口范围对后面的查询结果生效,所以其实并不关心是否进行了 GROUP BY。我们看下面的例子:
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdgMOH.png">
按照地区分组后进行累加聚合,是对 GROUP BY 后的数据行粒度进行的,而不是之前的明细行。
## 总结
窗口函数在计算组内排序或累计 GVM 等场景非常有用,我们只要牢记两个知识点就行了:
1. 分组排序要结合 PARTITION BY 才有意义。
2. 累计聚合作用于查询结果行粒度,支持所有聚合函数。
> 讨论地址是:[精读《SQL 窗口函数》· Issue #405 · ascoders/weekly](https://github.com/ascoders/weekly/issues/405)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/236.SQL grouping.md
================================================
SQL grouping 解决 OLAP 场景总计与小计问题,其语法分为几类,但要解决的是同一个问题:
ROLLUP 与 CUBE 是封装了规则的 GROUPING SETS,而 GROUPING SETS 则是最原始的规则。
为了方便理解,让我们从一个问题入手,层层递进吧。
## 底表
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdUL60.png">
以上是示例底表,共有 8 条数据,城市1、城市2 两个城市,下面各有地区1~4,每条数据都有该数据的人口数。
现在想计算人口总计,以及各城市人口小计。在没有掌握 grouping 语法前,我们只能通过两个 select 语句 union 后得到:
```sql
SELECT city, sum(people) FROM test GROUP BY city
union
SELECT '合计' as city, sum(people) FROM test
```
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbKPRs.png">
但两条 select 语句聚合了两次,性能是一个不小的开销,因此 SQL 提供了 GROUPING SETS 语法解决这个问题。
## GROUPING SETS
GROUP BY GROUPING SETS 可以指定任意聚合项,比如我们要同时计算总计与分组合计,就要按照空内容进行 GROUP BY 进行一次 sum,再按照 city 进行 GROUP BY 再进行一次 sum,换成 GROUPING SETS 描述就是:
```sql
SELECT
city, area,
sum(people)
FROM test
GROUP BY GROUPING SETS((), (city, area))
```
其中 `GROUPING SETS((), (city, area))` 表示分别按照 `()`、`(city, area)` 聚合计算总计。返回结果是:
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRnWF.png">
可以看到,值为 NULL 的行就是我们要的总计,其值是没有任何 GROUP BY 限制算出来的。
类似的,我们还可以写 `GROUPING SETS((), (city), (city, area), (area))` 等任意数量、任意组合的 GROUP BY 条件。
通过这种规则计算的数据我们称为 “超级分组记录”。我们发现 “超级分组记录” 产生的 NULL 值很容易和真正的 NULL 值弄混,所以 SQL 提供了 GROUPING 函数解决这个问题。
## 函数 GROUPING
对于超级分组记录产生的 NULL,是可以被 `GROUPING()` 函数识别为 1 的:
```sql
SELECT
GROUPING(city),
GROUPING(area),
sum(people)
FROM test
GROUP BY GROUPING SETS((), (city, area))
```
具体效果见下图:
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRLpF.png">
可以看到,但凡是超级分组计算出来的字段都会识别为 1,我们利用之前学习的 [SQL CASE 表达式](https://github.com/ascoders/weekly/blob/master/SQL/234.SQL%20CASE%20%E8%A1%A8%E8%BE%BE%E5%BC%8F.md) 将其转换为总计、小计字样,就可以得出一张数据分析表了:
```sql
SELECT
CASE WHEN GROUPING(city) = 1 THEN '总计' ELSE city END,
CASE WHEN GROUPING(area) = 1 THEN '小计' ELSE area END,
sum(people)
FROM test
GROUP BY GROUPING SETS((), (city, area))
```
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRz01.png">
然后前端表格展示时,将第一行 “总计”、“小计” 单元格合并为 “总计”,就完成了总计这个 BI 可视化分析功能。
## ROLLUP
ROLLUP 是卷起的意思,是一种特定规则的 GROUPING SETS,以下两种写法是等价的:
```sql
SELECT sum(people) FROM test
GROUP BY ROLLUP(city)
-- 等价于
SELECT sum(people) FROM test
GROUP BY GROUPING SETS((), (city))
```
再看一组等价描述:
```sql
SELECT sum(people) FROM test
GROUP BY ROLLUP(city, area)
-- 等价于
SELECT sum(people) FROM test
GROUP BY GROUPING SETS((), (city), (city, area))
```
发现规律了吗?ROLLUP 会按顺序把 GROUP BY 内容 “一个个卷起来”。用 GROUPING 函数判断超级分组记录对 ROLLUP 同样适用。
## CUBE
CUBE 又有所不同,它对内容进行了所有可能性展开(所以叫 CUBE)。
类比上面的例子,我们再写两组等价的展开:
```sql
SELECT sum(people) FROM test
GROUP BY CUBE(city)
-- 等价于
SELECT sum(people) FROM test
GROUP BY GROUPING SETS((), (city))
```
上面的例子因为只有一项还看不出来,下面两项分组就能看出来了:
```sql
SELECT sum(people) FROM test
GROUP BY CUBE(city, area)
-- 等价于
SELECT sum(people) FROM test
GROUP BY GROUPING SETS((), (city), (area), (city, area))
```
所谓 CUBE,是一种多维形状的描述,二维时有 2^1 种展开,三维时有 2^2 种展开,四维、五维依此类推。可以想象,如果用 CUBE 描述了很多组合,复杂度会爆炸。
## 总结
学习了 GROUPING 语法,以后前端同学的你不会再纠结这个问题了吧:
> 产品开启了总计、小计,我们是额外取一次数还是放到一起获取啊?
这个问题的标准答案和原理都在这篇文章里了。PS:对于不支持 GROUPING 语法数据库,要想办法屏蔽,就像前端 polyfill 一样,是一种降级方案。至于如何屏蔽,参考文章开头提到的两个 SELECT + UNION。
> 讨论地址是:[精读《SQL grouping》· Issue #406 · ascoders/weekly](https://github.com/ascoders/weekly/issues/406)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/243.精读《Pick, Awaited, If...》.md
================================================
TS 强类型非常好用,但在实际运用中,免不了遇到一些难以描述,反复看官方文档也解决不了的问题,至今为止也没有任何一篇文档,或者一套教材可以解决所有犄角旮旯的类型问题。为什么会这样呢?因为 TS 并不是简单的注释器,而是一门图灵完备的语言,所以很多问题的解决方法藏在基础能力里,但你学会了基础能力又不一定能想到这么用。
解决该问题的最好办法就是多练,通过实际案例不断刺激你的大脑,让你养成 TS 思维习惯。所以话不多说,我们今天从 [type-challenges](https://github.com/type-challenges/type-challenges) 的 Easy 难度题目开始吧。
## 精读
### [Pick](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md)
手动实现内置 `Pick<T, K>` 函数,返回一个新的类型,从对象 T 中抽取类型 K:
```ts
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
```
结合例子更容易看明白,也就是 `K` 是一个字符串,我们需要返回一个新类型,仅保留 `K` 定义的 Key。
第一个难点在如何限制 `K` 的取值,比如传入 `T` 中不存在的值就要报错。这个考察的是硬知识,只要你知道 `A extends keyof B` 这个语法就能联想到。
第二个难点在于如何生成一个仅包含 `K` 定义 Key 的类型,你首先要知道有 `{ [A in keyof B]: B[A] }` 这个硬知识,这样可以重新组合一个对象:
```ts
// 代码 1
type Foo<T> = {
[P in keyof T]: T[P]
}
```
只懂这个语法不一定能想出思路,原因是你要打破对 TS 的刻板理解,`[K in keyof T]` 不是一个固定模板,其中 `keyof T` 只是一个指代变量,它可以被换掉,如果你换掉成另一个范围的变量,那么这个对象的 Key 值范围就变了,这正好契合本题的 `K`:
```ts
// 代码 2(本题答案)
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
```
这个题目别看知道答案后简单,回顾下还是有收获的。对比上面两个代码例子,你会发现,只不过是把代码 1 的 `keyof T` 从对象描述中提到了泛型定义里而已,所以功能上没有任何变化,但因为泛型可以由用户传入,所以代码 1 的 `P in keyof T` 因为没有泛型支撑,这里推导出来的就是 `T` 的所有 Keys,而代码 2 虽然把代码挪到了泛型,但因为用的是 `extends` 描述,所以表示 `P` 的类型被约束到了 `T` 的 Keys,至于具体是什么,得看用户代码怎么传。
所以其实放到泛型里的 `K` 是没有默认值的,而写到对象里作为推导值就有了默认值。泛型里给默认值的方式如下:
```ts
// 代码 3
type MyPick<T, K extends keyof T = keyof T> = {
[P in K]: T[P]
}
```
也就是说,这样 `MyPick<Todo>` 就也可以正确工作并原封不动返回 `Todo` 类型,也就是说,代码 3 在不传第二个参数时,与代码 1 的功能完全一样。仔细琢磨一下共同点与区别,为什么代码 3 可以做到和代码 1 功能一样,又有更强的拓展性,你对 TS 泛型的实战理解就上了一个台阶。
### [Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.md)
手动实现内置 `Readonly<T>` 函数,将对象所有属性设置为只读:
```ts
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
```
这道题反而比第一题简单,只要我们用 `{ [A in keyof B]: B[A] }` 重新声明对象,并在每个 Key 前面加上 `readonly` 修饰即可:
```ts
// 本题答案
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
```
根据这个特性我们可以做很多延伸改造,比如将对象所有 Key 都设定为可选:
```ts
type Optional<T> = {
[K in keyof T]?: T[K]
}
```
`{ [A in keyof B]: B[A] }` 给了我们描述每一个 Key 属性细节的机会,限制我们发挥的只有想象力。
### [First Of Array](https://github.com/type-challenges/type-challenges/blob/main/questions/00014-easy-first/README.md)
实现类型 `First<T>`,取到数组第一项的类型:
```ts
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
```
这题比较简单,很容易想到的答案:
```ts
// 本题答案
type First<T extends any[]> = T[0]
```
但在写这个答案时,有 10% 脑细胞提醒我没有判断边界情况,果然看了下答案,有空数组的情况要考虑,空数组时返回类型 `never` 而不是 `undefined` 会更好,下面几种写法都是答案:
```ts
type First<T extends any[]> = T extends [] ? never : T[0]
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
type First<T> = T extends [infer P, ...infer Rest] ? P : never
```
第一种写法通过 `extends []` 判断 `T` 是否为空数组,是的话返回 `never`。
第二种写法通过长度为 0 判断空数组,此时需要理解两点:1. 可以通过 `T['length']` 让 TS 访问到值长度(类型的),2. `extends 0` 表示是否匹配 0,即 `extends` 除了匹配类型,还能直接匹配值。
第三种写法是最省心的,但也使用了 `infer` 关键字,即使你充分知道 `infer` 怎么用([精读《Typescript infer 关键字》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md)),也很难想到它。用 `infer` 的理由是:该场景存在边界情况,最便于理解的写法是 “如果 T 形如 `<P, ...>`” 那我就返回类型 `P`,否则返回 `never`”,这句话用 TS 描述就是:`T extends [infer P, ...infer Rest] ? P : never`。
### [Length of Tuple](https://github.com/type-challenges/type-challenges/blob/main/questions/00018-easy-tuple-length/README.md)
实现类型 `Length<T>` 获取元组长度:
```ts
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
```
经过上一题的学习,很容易想到这个答案:
```ts
type Length<T extends any[]> = T['length']
```
对 TS 来说,元组和数组都是数组,但元组对 TS 来说可以观测其长度,`T['length']` 对元组来说返回的是具体值,而对数组来说返回的是 `number`。
### [Exclude](https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.md)
实现类型 `Exclude<T, U>`,返回 `T` 中不存在于 `U` 的部分。该功能主要用在联合类型场景,所以我们直接用 `extends` 判断就行了:
```ts
// 本题答案
type Exclude<T, U> = T extends U ? never : T
```
实际运行效果:
```ts
type C = Exclude<'a' | 'b', 'a' | 'c'> // 'b'
```
看上去有点不那么好理解,这是因为 TS 对联合类型的执行是分配律的,即:
```ts
Exclude<'a' | 'b', 'a' | 'c'>
// 等价于
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>
```
### [Awaited](https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.md)
实现类型 `Awaited`,比如从 `Promise<ExampleType>` 拿到 `ExampleType`。
首先 TS 永远不会执行代码,所以脑子里不要有 “await 得等一下才知道结果” 的念头。该题关键就是从 `Promise<T>` 中抽取类型 `T`,很适合用 `infer` 做:
```ts
type MyAwaited<T> = T extends Promise<infer U> ? U : never
```
然而这个答案还不够标准,标准答案考虑了嵌套 `Promise` 的场景:
```ts
// 该题答案
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer P>
? P extends Promise<unknown> ? MyAwaited<P> : P
: never
```
如果 `Promise<P>` 取到的 `P` 还形如 `Promise<unknown>`,就递归调用自己 `MyAwaited<P>`。这里提到了递归,也就是 TS 类型处理可以是递归的,所以才有了后面版本做尾递归优化。
### [If](https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md)
实现类型 `If<Condition, True, False>`,当 `C` 为 `true` 时返回 `T`,否则返回 `F`:
```ts
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
```
之前有提过,`extends` 还可以用来判定值,所以果断用 `extends true` 判断是否命中了 `true` 即可:
```ts
// 本题答案
type If<C, T, F> = C extends true ? T : F
```
### [Concat](https://github.com/type-challenges/type-challenges/blob/main/questions/00533-easy-concat/README.md)
用类型系统实现 `Concat<P, Q>`,将两个数组类型连起来:
```ts
type Result = Concat<[1], [2]> // expected to be [1, 2]
```
由于 TS 支持数组解构语法,所以可以大胆的尝试这么写:
```ts
type Concat<P extends any[], Q extends any[]> = [...P, ...Q]
```
考虑到 `Concat` 函数应该也能接收非数组类型,所以做一个判断,为了方便书写,把 `extends` 从泛型定义位置挪到 TS 类型推断的运行时:
```ts
// 本题答案
type Concat<P, Q> = [
...P extends any[] ? P : [P],
...Q extends any[] ? Q : [Q],
]
```
解决这题需要信念,相信 TS 可以像 JS 一样写逻辑。这些能力都是版本升级时渐进式提供的,所以需要不断阅读最新 TS 特性,快速将其理解为固化知识,其实还是有一定难度的。
### [Includes](https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.md)
用类型系统实现 `Includes<T, K>` 函数:
```ts
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
```
由于之前的经验,很容易做下面的联想:
```ts
// 如果题目要求是这样
type isPillarMen = Includes<'Kars' | 'Esidisi' | 'Wamuu' | 'Santana', 'Dio'>
// 那我就能用 extends 轻松解决了
type Includes<T, K> = K extends T ? true : false
```
可惜第一个输入是数组类型,`extends` 可不支持判定 “数组包含” 逻辑,此时要了解一个新知识点,即 TS 判断中的 `[number]` 下标。不仅这道题,以后很多困难题都需要它作为基础知识。
`[number]` 下标表示任意一项,而 `extends T[number]` 就可以实现数组包含的判定,因此下面的解法是有效的:
```ts
type Includes<T extends any[], K> = K extends T[number] ? true : false
```
但翻答案后发现这并不是标准答案,还真找到一个反例:
```ts
type Includes<T extends any[], K> = K extends T[number] ? true : false
type isPillarMen = Includes<[boolean], false> // true
```
原因很简单,`true`、`false` 都继承自 `boolean`,所以 `extends` 判断的界限太宽了,题目要求的是精确值匹配,故上面的答案理论上是错的。
标准答案是每次判断数组第一项,并递归(讲真觉得这不是 easy 题),分别有两个难点。
第一如何写 Equal 函数?比较流行的方案是这个:
```ts
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
```
关于如何写 Equal 函数还引发了一次 [小讨论](https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650),上面的代码构造了两个函数,这两个函数内的 `T` 属于 deferred(延迟)判断的类型,该类型判断依赖于内部 `isTypeIdenticalTo` 函数完成判断。
有了 `Equal` 后就简单了,我们用解构 + `infer` + 递归的方式做就可以了:
```ts
// 本题答案
type Includes<T extends any[], K> =
T extends [infer F, ...infer Rest] ?
Equal<F, K> extends true ?
true
: Includes<Rest, K>
: false
```
每次取数组第一个值判断 `Equal`,如果不匹配则拿剩余项递归判断。这个函数组合了不少 TS 知识,比如:
- 递归
- 解构
- `infer`
- `extends true`
可以发现,就为了解决 `true extends boolean` 为 `true` 的问题,我们绕了一大圈使用了更复杂的方式来实现,这在 TS 体操中也算是常态,解决问题需要耐心。
### [Push](https://github.com/type-challenges/type-challenges/blob/main/questions/03057-easy-push/README.md)
实现 `Push<T, K>` 函数:
```ts
type Result = Push<[1, 2], '3'> // [1, 2, '3']
```
这道题真的很简单,用解构就行了:
```ts
// 本题答案
type Push<T extends any[], K> = [...T, K]
```
可见,想要轻松解决一个 TS 简单问题,首先你需要能解决一些困难问题 😁。
### [Unshift](https://github.com/type-challenges/type-challenges/blob/main/questions/03060-easy-unshift/README.md)
实现 `Unshift<T, K>` 函数:
```ts
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
```
在 `Push` 基础上改下顺序就行了:
```ts
// 本题答案
type Unshift<T extends any[], K> = [K, ...T]
```
### [Parameters](https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.md)
实现内置函数 `Parameters`:
`Parameters` 可以拿到函数的参数类型,直接用 `infer` 实现即可,也比较简单:
```ts
type Parameters<T> = T extends (...args: infer P) => any ? P : []
```
`infer` 可以很方便从任何具体的位置取值,属于典型难懂易用的语法。
## 总结
学会 TS 基础语法后,活用才是关键。
> 讨论地址是:[精读《Pick, Awaited, If...》· Issue #422 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/422)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 1~8 题。
## 精读
### [Get Return Type](https://github.com/type-challenges/type-challenges/blob/main/questions/00002-medium-return-type/README.md)
实现非常经典的 `ReturnType<T>`:
```ts
const fn = (v: boolean) => {
if (v)
return 1
else
return 2
}
type a = MyReturnType<typeof fn> // should be "1 | 2"
```
首先不要被例子吓到了,觉得必须执行完代码才知道返回类型,其实 TS 已经帮我们推导好了返回类型,所以上面的函数 `fn` 的类型已经是这样了:
```ts
const fn = (v: boolean): 1 | 2 => { ... }
```
我们要做的就是把函数返回值从内部抽出来,这非常适合用 `infer` 实现:
```ts
// 本题答案
type MyReturnType<T> = T extends (...args: any[]) => infer P ? P : never
```
`infer` 配合 `extends` 是解构复杂类型的神器,如果对上面代码不能一眼理解,说明对 `infer` 熟悉度还是不够,需要多看。
### [Omit](https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.md)
实现 `Omit<T, K>`,作用恰好与 `Pick<T, K>` 相反,排除对象 `T` 中的 `K` key:
```ts
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
const todo: TodoPreview = {
completed: false,
}
```
这道题比较容易尝试的方案是:
```ts
type MyOmit<T, K extends keyof T> = {
[P in keyof T]: P extends K ? never : T[P]
}
```
其实仍然包含了 `description`、`title` 这两个 Key,只是这两个 Key 类型为 `never`,不符合要求。
所以只要 `P in keyof T` 写出来了,后面怎么写都无法将这个 Key 抹去,我们应该从 Key 下手:
```ts
type MyOmit<T, K extends keyof T> = {
[P in (keyof T extends K ? never : keyof T)]: T[P]
}
```
但这样写仍然不对,我们思路正确,即把 `keyof T` 中归属于 `K` 的排除,但因为前后 `keyof T` 并没有关联,所以需要借助 `Exclude` 告诉 TS,前后 `keyof T` 是同一个指代(上一讲实现过 `Exclude`):
```ts
// 本题答案
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P]
}
type Exclude<T, U> = T extends U ? never : T
```
这样就正确了,掌握该题的核心是:
1. 三元判断还可以写在 Key 位置。
2. JS 抽不抽函数效果都一样,但 TS 需要推断,很多时候抽一个函数出来就是为了告诉 TS “是同一指代”。
当然既然都用上了 `Exclude`,我们不如再结合 `Pick`,写出更优雅的 `Omit` 实现:
```ts
// 本题优雅答案
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
```
### [Readonly 2](https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.md)
实现 `MyReadonly2<T, K>`,让指定的 Key `K` 成为 ReadOnly:
```ts
interface Todo {
title: string
description: string
completed: boolean
}
const todo: MyReadonly2<Todo, 'title' | 'description'> = {
title: "Hey",
description: "foobar",
completed: false,
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK
```
该题乍一看蛮难的,因为 `readonly` 必须定义在 Key 位置,但我们又没法在这个位置做三元判断。其实利用之前我们自己做的 `Pick`、`Omit` 以及内置的 `Readonly` 组合一下就出来了:
```ts
// 本题答案
type MyReadonly2<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
```
即我们可以将对象一分为二,先 `Pick` 出 `K` Key 部分设置为 Readonly,再用 `&` 合并上剩下的 Key,正好用到上一题的函数 `Omit`,完美。
### [Deep Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.md)
实现 `DeepReadonly<T>` 递归所有子元素:
```ts
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type Expected = {
readonly x: {
readonly a: 1
readonly b: 'hi'
}
readonly y: 'hey'
}
type Todo = DeepReadonly<X> // should be same as `Expected`
```
这肯定需要用类型递归实现了,既然要递归,肯定不能依赖内置 `Readonly` 函数,我们需要将函数展开手写:
```ts
// 本题答案
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Object> ? DeepReadonly<T[K]> : T[K]
}
```
这里 `Object` 也可以用 `Record<string, any>` 代替。
### [Tuple to Union](https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.md)
实现 `TupleToUnion<T>` 返回元组所有值的集合:
```ts
type Arr = ['1', '2', '3']
type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
```
该题将元组类型转换为其所有值的可能集合,也就是我们希望用所有下标访问这个数组,在 TS 里用 `[number]` 作为下标即可:
```ts
// 本题答案
type TupleToUnion<T extends any[]> = T[number]
```
### [Chainable Options](https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.md)
直接看例子比较好懂:
```ts
declare const config: Chainable
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// expect the type of result to be:
interface Result {
foo: number
name: string
bar: {
value: string
}
}
```
也就是我们实现一个相对复杂的 `Chainable` 类型,拥有该类型的对象可以 `.option(key, value)` 一直链式调用下去,直到使用 `get()` 后拿到聚合了所有 `option` 的对象。
如果我们用 JS 实现该函数,肯定需要在当前闭包存储 Object 的值,然后提供 `get` 直接返回,或 `option` 递归并传入新的值。我们不妨用 Class 来实现:
```ts
class Chain {
constructor(previous = {}) {
this.obj = { ...previous }
}
obj: Object
get () {
return this.obj
}
option(key: string, value: any) {
return new Chain({
...this.obj,
[key]: value
})
}
}
const config = new Chain()
```
而本地要求用 TS 实现,这就比较有趣了,正好对比一下 JS 与 TS 的思维。先打个岔,该题用上面 JS 方式写出来后,其实类型也就出来了,但用 TS 完整实现类型也另有其用,特别在一些复杂函数场景,需要用 TS 系统描述类型,JS 真正实现时拿到 any 类型做纯运行时处理,将类型与运行时分离开。
好我们回到题目,我们先把 `Chainable` 的框架写出来:
```ts
type Chainable = {
option: (key: string, value: any) => any
get: () => any
}
```
问题来了,如何用类型描述 `option` 后还可以接 `option` 或 `get` 呢?还有更麻烦的,如何一步一步将类型传导下去,让 `get` 知道我此时拿的类型是什么呢?
`Chainable` 必须接收一个泛型,这个泛型默认值是个空对象,所以 `config.get()` 返回一个空对象也是合理的:
```ts
type Chainable<Result = {}> = {
option: (key: string, value: any) => any
get: () => Result
}
```
上面的代码对于第一层是完全没问题的,直接调用 `get` 返回的就是空对象。
第二步解决递归问题:
```ts
// 本题答案
type Chainable<Result = {}> = {
option: <K extends string, V>(key: K, value: V) => Chainable<Result & {
[P in K]: V
}>
get: () => Result
}
```
递归思维大家都懂就不赘述了。这里有个看似不值得一提,但确实容易坑人的地方,就是如何描述一个对象仅包含一个 Key 值,这个值为泛型 `K` 呢?
```ts
// 这是错的,因为描述了一大堆类型
{
[K] : V
}
// 这也是错的,这个 K 就是字面量 K,而非你希望的类型指代
{
K: V
}
```
所以必须使用 TS “习惯法” 的 `[K in keyof T]` 的套路描述,即便我们知道 `T` 只有一个固定的类型。可见 JS 与 TS 完全是两套思维方式,所以精通 JS 不必然精通 TS,TS 还是要大量刷题培养思维的。
### [Last of Array](https://github.com/type-challenges/type-challenges/blob/main/questions/00015-medium-last/README.md)
实现 `Last<T>` 获取元组最后一项的类型:
```ts
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1
```
我们之前实现过 `First`,类似的,这里无非是解构时把最后一个描述成 `infer`:
```ts
// 本题答案
type Last<T> = T extends [...infer Q, infer P] ? P : never
```
这里要注意,`infer Q` 有人第一次可能会写成:
```ts
type Last<T> = T extends [...Others, infer P] ? P : never
```
发现报错,因为 TS 里不可能随便使用一个未定义的泛型,而如果把 Others 放在 `Last<T, Others>` 里,你又会面临一个 TS 大难题:
```ts
type Last<T, Others extends any[]> = T extends [...Others, infer P] ? P : never
// 必然报错
Last<arr2>
```
因为 `Last<arr2>` 仅传入了一个参数,必然报错,但第一个参数是用户给的,第二个参数是我们推导出来的,这里既不能用默认值,又不能不写,无解了。
如果真的硬着头皮要这么写,必须借助 TS 还未通过的一项特性:[部分类型参数推断](https://github.com/microsoft/TypeScript/issues/26242),举个例子,很可能以后的语法是:
```ts
type Last<T, Others extends any[] = infer> = T extends [...Others, infer P] ? P : never
```
这样首先传参只需要一个了,而且还申明了第二个参数是一个推断类型。不过该提案还未支持,而且本质上和把 `infer` 写到表达式里面含义和效果也都一样,所以对这道题来说就不用折腾了。
### [Pop](https://github.com/type-challenges/type-challenges/blob/main/questions/00016-medium-pop/README.md)
实现 `Pop<T>`,返回去掉元组最后一项之后的类型:
```ts
type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]
type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]
```
这道题和 `Last` 几乎完全一样,返回第一个解构值就行了:
```ts
// 本题答案
type Pop<T> = T extends [...infer Q, infer P] ? Q : never
```
## 总结
从题目中很明显能看出 TS 思维与 JS 思维有很大差异,想要真正掌握 TS,大量刷题是必须的。
> 讨论地址是:[精读《Get return type, Omit, ReadOnly...》· Issue #422 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/422)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/245.精读《Promise.all, Replace, Type Lookup...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 9~16 题。
## 精读
### [Promise.all](https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.md)
实现函数 `PromiseAll`,输入 PromiseLike,输出 `Promise<T>`,其中 `T` 是输入的解析结果:
```ts
const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
```
该题难点不在 `Promise` 如何处理,而是在于 `{ [K in keyof T]: T[K] }` 在 TS 同样适用于描述数组,这是 JS 选手无论如何也想不到的:
```ts
// 本题答案
declare function PromiseAll<T>(values: T): Promise<{
[K in keyof T]: T[K] extends Promise<infer U> ? U : T[K]
}>
```
不知道是 bug 还是 feature,TS 的 `{ [K in keyof T]: T[K] }` 能同时兼容元组、数组与对象类型。
### [Type Lookup](https://github.com/type-challenges/type-challenges/blob/main/questions/00062-medium-type-lookup/README.md)
实现 `LookUp<T, P>`,从联合类型 `T` 中查找 `type` 为 `P` 的项并返回:
```ts
interface Cat {
type: 'cat'
breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}
interface Dog {
type: 'dog'
breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
color: 'brown' | 'white' | 'black'
}
type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`
```
该题比较简单,只要学会灵活使用 `infer` 与 `extends` 即可:
```ts
// 本题答案
type LookUp<T, P> = T extends {
type: infer U
} ? (
U extends P ? T : never
) : never
```
联合类型的判断是一个个来的,所以我们只要针对每一个单独写判断就行了。上面的解法中,我们先利用 `extend` + `infer` 锁定 `T` 的类型是包含 `type` key 的对象,且将 `infer U` 指向了 `type`,所以在内部再利用三元运算符判断 `U extends P ?` 就能将 `type` 命中的类型挑出来。
笔者翻了下答案,发现还有一种更高级的解法:
```ts
// 本题答案
type LookUp<U extends { type: any }, T extends U['type']> = U extends { type: T } ? U : never
```
该解法更简洁,更完备:
- 在泛型处利用 `extends { type: any }`、`extends U['type']` 直接锁定入参类型,让错误校验更早发生。
- `T extends U['type']` 精确缩小了参数 `T` 范围,可以学到的是,之前定义的泛型 `U` 可以直接被后面的新泛型使用。
- `U extends { type: T }` 是一种新的思考角度。在第一个答案中,我们的思维方式是 “找到对象中 `type` 值进行判断”,而第二个答案直接用整个对象结构 `{ type: T }` 判断,是更纯粹的 TS 思维。
### [Trim Left](https://github.com/type-challenges/type-challenges/blob/main/questions/00106-medium-trimleft/README.md)
实现 `TrimLeft<T>`,将字符串左侧空格清空:
```ts
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World '
```
在 TS 处理这类问题只能用递归,不能用正则。比较容易想到的是下面的写法:
```ts
// 本题答案
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T
```
即如果字符串前面包含空格,就把空格去了继续递归,否则返回字符串本身。掌握该题的关键是 `infer` 也能用在字符串内进行推导。
### [Trim](https://github.com/type-challenges/type-challenges/blob/main/questions/00108-medium-trim/README.md)
实现 `Trim<T>`,将字符串左右两侧空格清空:
```ts
type trimmed = Trim<' Hello World '> // expected to be 'Hello World'
```
这个问题简单的解法是,左右都 Trim 一下:
```ts
// 本题答案
type Trim<T extends string> = TrimLeft<TrimRight<T>>
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T
type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T
```
这个成本很低,性能也不差,因为单写 `TrimLeft` 与 `TrimRight` 都很简单。
如果不采用先 Left 后 Right 的做法,想要一次性完成,就要有一些 TS 思维了。比较笨的思路是 “如果左边有空格就切分左边,或者右边有空格就切分右边”,最后写出来一个复杂的三元表达式。比较优秀的思路是利用 TS 联合类型:
```ts
// 本题答案
type Trim<T extends string> = T extends ` ${infer R}` | `${infer R} ` ? Trim<R> : T
```
`extends` 后面还可以跟联合类型,这样任意一个匹配都会走到 `Trim<R>` 递归里。这就是比较难说清楚的 TS 思维,如果没有它,你只能想到三元表达式,但一旦理解了联合类型还可以在 `extends` 里这么用,TS 帮你做了 N 元表达式的能力,那么写出来的代码就会非常清秀。
### [Capitalize](https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.md)
实现 `Capitalize<T>` 将字符串第一个字母大写:
```ts
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'
```
如果这是一道 JS 题那就简单到爆,可题目是 TS 的,我们需要再度切换为 TS 思维。
首先要知道利用基础函数 `Uppercase` 将单个字母转化为大写,然后配合 `infer` 就不用多说了:
```ts
type MyCapitalize<T extends string> = T extends `${infer F}${infer U}` ? `${Uppercase<F>}${U}` : T
```
### [Replace](https://github.com/type-challenges/type-challenges/blob/main/questions/00116-medium-replace/README.md)
实现 TS 版函数 `Replace<S, From, To>`,将字符串 `From` 替换为 `To`:
```ts
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'
```
把 `From` 夹在字符串中间,前后用两个 `infer` 推导,最后输出时前后不变,把 `From` 换成 `To` 就行了:
```ts
// 本题答案
type Replace<S extends string, From extends string, To extends string,> =
S extends `${infer A}${From}${infer B}` ? `${A}${To}${B}` : S
```
### [ReplaceAll](https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.md)
实现 `ReplaceAll<S, From, To>`,将字符串 `From` 替换为 `To`:
```ts
type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'
```
该题与上题不同之处在于替换全部,解法肯定是递归,关键是何时递归的判断条件是什么。经过一番思考,如果 `infer From` 能匹配到不就说明还可以递归吗?所以加一层三元判断 `From extends ''` 即可:
```ts
// 本题答案
type ReplaceAll<S extends string, From extends string, To extends string> =
From extends '' ? S : (
S extends `${infer A}${From}${infer B}` ? (
From extends '' ? `${A}${To}${B}` : `${A}${To}${ReplaceAll<B, From, To>}`
) : S
)
```
补充一些细节:
1. 如果替换文本为空字符串需要跳过,否则会匹配第二个任意字符。
2. 为了防止替换完后结果可以再度匹配,对递归形式做一下调整,下次递归直接从剩余部分开始。
### [Append Argument](https://github.com/type-challenges/type-challenges/blob/main/questions/00191-medium-append-argument/README.md)
实现类型 `AppendArgument<F, E>`,将函数参数拓展一个:
```ts
type Fn = (a: number, b: string) => number
type Result = AppendArgument<Fn, boolean>
// expected be (a: number, b: string, x: boolean) => number
```
该题很简单,用 `infer` 就行了:
```ts
// 本题答案
type AppendArgument<F, E> = F extends (...args: infer T) => infer R ? (...args: [...T, E]) => R : F
```
## 总结
这几道题都比较简单,主要考察对 `infer` 和递归的熟练使用。
> 讨论地址是:[精读《Promise.all, Replace, Type Lookup...》· Issue #425 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/425)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/246.精读《Permutation, Flatten, Absolute...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 17~24 题。
## 精读
### [Permutation](https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation/README.md)
实现 `Permutation` 类型,将联合类型替换为可能的全排列:
```ts
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
```
看到这题立马联想到 TS 对多个联合类型泛型处理是采用分配律的,在第一次做到 `Exclude` 题目时遇到过:
```ts
Exclude<'a' | 'b', 'a' | 'c'>
// 等价于
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>
```
所以这题如果能 “递归触发联合类型分配率”,就有戏解决啊。但触发的条件必须存在两个泛型,而题目传入的只有一个,我们只好创造第二个泛型,使其默认值等于第一个:
```ts
type Permutation<T, U = T>
```
这样对本题来说,会做如下展开:
```ts
Permutation<'A' | 'B' | 'C'>
// 等价于
Permutation<'A' | 'B' | 'C', 'A' | 'B' | 'C'>
// 等价于
Permutation<'A', 'A' | 'B' | 'C'> | Permutation<'B', 'A' | 'B' | 'C'> | Permutation<'C', 'A' | 'B' | 'C'>
```
对于 `Permutation<'A', 'A' | 'B' | 'C'>` 来说,排除掉对自身的组合,可形成 `'A', 'B'`,`'A', 'C'` 组合,之后只要再递归一次,再拼一次,把已有的排除掉,就形成了 `A` 的全排列,以此类推,形成所有字母的全排列。
这里要注意两点:
1. 如何排除掉自身?`Exclude<T, P>` 正合适,该函数遇到 `T` 在联合类型 `P` 中时,会返回 `never`,否则返回 `T`。
2. 递归何时结束?每次递归时用 `Exclude<U, T>` 留下没用过的组合,最后一次组合用完一定会剩下 `never`,此时终止递归。
```ts
// 本题答案
type Permutation<T, U = T> = [T] extends [never] ? [] : T extends U ? [T, ...Permutation<Exclude<U, T>>] : []
```
验证一下答案,首先展开 `Permutation<'A', 'B', 'C'>`:
```ts
'A' extends 'A' | 'B' | 'C' ? ['A', ...Permutation<'B' | 'C'>] : []
'B' extends 'A' | 'B' | 'C' ? ['B', ...Permutation<'A' | 'C'>] : []
'C' extends 'A' | 'B' | 'C' ? ['C', ...Permutation<'A' | 'B'>] : []
```
我们再展开第一行 `Permutation<'B' | 'C'>`:
```ts
'B' extends 'B' | 'C' ? ['B', ...Permutation<'C'>] : []
'C' extends 'B' | 'C' ? ['C', ...Permutation<'B'>] : []
```
再展开第一行的 `Permutation<'C'>`:
```ts
'C' extends 'C' ? ['C', ...Permutation<never>] : []
```
此时已经完成全排列,但我们还要处理一下 `Permutation<never>`,使其返回 `[]` 并终止递归。那为什么要用 `[T] extends [never]` 而不是 `T extends never` 呢?
如果我们用 `T extends never` 代替本题答案,输出结果是 `never`,原因如下:
```ts
type X = never extends never ? 1 : 0 // 1
type Custom<T> = T extends never ? 1 : 0
type Y = Custom<never> // never
```
理论上相同的代码,为什么用泛型后输出就变成 `never` 了呢?原因是 TS 在做 `T extends never ?` 时,会对联合类型进行分配,此时有一个特例,即当 `T = never` 时,会跳过分配直接返回 `T` 本身,所以三元判断代码实际上没有执行。
`[T] extends [never]` 这种写法可以避免 TS 对联合类型进行分配,继而绕过上面的问题。
### [Length of String](https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.md)
实现 `LengthOfString<T>` 返回字符串 T 的长度:
```ts
LengthOfString<'abc'> // 3
```
破解此题你需要知道一个前提,即 TS 访问数组类型的 `[length]` 属性可以拿到长度值:
```ts
['a','b','c']['length'] // 3
```
也就是说,我们需要把 `'abc'` 转化为 `['a', 'b', 'c']`。
第二个需要了解的前置知识是,用 `infer` 指代字符串时,第一个指代指向第一个字母,第二个指向其余所有字母:
```ts
'abc' extends `${infer S}${infer E}` ? S : never // 'a'
```
那转换后的数组存在哪呢?类似 js,我们弄第二个默认值泛型存储即可:
```ts
// 本题答案
type LengthOfString<S, N extends any[] = []> = S extends `${infer S}${infer E}` ? LengthOfString<E, [...N, S]> : N['length']
```
思路就是,每次把字符串第一个字母拿出来放到数组 `N` 的第一项,直到字符串被取完,直接拿此时的数组长度。
### [Flatten](https://github.com/type-challenges/type-challenges/blob/main/questions/00459-medium-flatten/README.md)
实现类型 `Flatten`:
```ts
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]
```
此题一看就需要递归:
```ts
// 本题答案
type Flatten<T extends any[], Result extends any[] = []> = T extends [infer Start, ...infer Rest] ? (
Start extends any[] ? Flatten<Rest, [...Result, ...Flatten<Start>]> : Flatten<Rest, [...Result, Start]>
) : Result
```
这道题看似答案复杂,其实还是用到了上一题的套路:**递归时如果需要存储临时变量,用泛型默认值来存储**。
本题我们就用 `Result` 这个泛型存储打平后的结果,每次拿到数组第一个值,如果第一个值不是数组,则直接存进去继续递归,此时 `T` 自然是剩余的 `Rest`;如果第一个值是数组,则将其打平,此时有个精彩的地方,即 `...Start` 打平后依然可能是数组,比如 `[[5]]` 就套了两层,能不能想到 `...Flatten<Start>` 继续复用递归是解题关键。
### [Append to object](https://github.com/type-challenges/type-challenges/blob/main/questions/00527-medium-append-to-object/README.md)
实现 `AppendToObject`:
```ts
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }
```
结合之前刷题的经验,该题解法很简单,注意 `K in Key` 可以给对象拓展某些指定 Key:
```ts
// 本题答案
type AppendToObject<Obj, Key extends string, Value> = Obj & {
[K in Key]: Value
}
```
当然也有不用 `Obj &` 的写法,即把原始对象和新 Key, Value 合在一起的描述方式:
```ts
// 本题答案
type AppendToObject<T, U extends number | string | symbol, V> = {
[key in (keyof T) | U]: key extends U ? V : T[Exclude<key, U>]
}
```
### [Absolute](https://github.com/type-challenges/type-challenges/blob/main/questions/00529-medium-absolute/README.md)
实现 `Absolute` 将数字转成绝对值:
```ts
type Test = -100;
type Result = Absolute<Test>; // expected to be "100"
```
该题重点是把数字转成绝对值字符串,所以我们可以用字符串的方式进行匹配:
```ts
// 本题答案
type Absolute<T extends number> = `${T}` extends `-${infer R}` ? R : `${T}`
```
为什么不用 `T extends` 来判断呢?因为 `T` 是数字,这样写无法匹配符号的字符串描述。
### [String to Union](https://github.com/type-challenges/type-challenges/blob/main/questions/00531-medium-string-to-union/README.md)
实现 `StringToUnion` 将字符串转换为联合类型:
```ts
type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"
```
还是老套路,用一个新的泛型存储答案,递归即可:
```ts
// 本题答案
type StringToUnion<T, P = never> = T extends `${infer F}${infer R}` ? StringToUnion<R, P | F> : P
```
当然也可以不依托泛型存储答案,因为该题比较特殊,可以直接用 `|`:
```ts
// 本题答案
type StringToUnion<T> = T extends `${infer F}${infer R}` ? F | StringToUnion<R> : never
```
### [Merge](https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.md)
实现 `Merge` 合并两个对象,冲突时后者优先:
```ts
type foo = {
name: string;
age: string;
}
type coo = {
age: number;
sex: string
}
type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string}
```
这道题答案甚至是之前题目的解题步骤,即用一个对象描述 + `keyof` 的思维:
```ts
// 本题答案
type Merge<A extends object, B extends object> = {
[K in keyof A | keyof B] : K extends keyof B ? B[K] : (
K extends keyof A ? A[K] : never
)
}
```
只要知道 `in keyof` 支持元组,值部分用 `extends` 进行区分即可,很简单。
### [KebabCase](https://github.com/type-challenges/type-challenges/blob/main/questions/00612-medium-kebabcase/README.md)
实现驼峰转横线的函数 `KebabCase`:
```ts
KebabCase<'FooBarBaz'> // 'foo-bar-baz'
```
还是老套路,用第二个参数存储结果,用递归的方式遍历字符串,遇到大写字母就转成小写并添加上 `-`,最后把开头的 `-` 干掉就行了:
```ts
// 本题答案
type KebabCase<S, U extends string = ''> = S extends `${infer F}${infer R}` ? (
Lowercase<F> extends F ? KebabCase<R, `${U}${F}`> : KebabCase<R, `${U}-${Lowercase<F>}`>
) : RemoveFirstHyphen<U>
type RemoveFirstHyphen<S> = S extends `-${infer Rest}` ? Rest : S
```
分开写就非常容易懂了,首先 `KebabCase` 每次递归取第一个字符,如何判断这个字符是大写呢?只要小写不等于原始值就是大写,所以判断条件就是 `Lowercase<F> extends F` 的 false 分支。然后再写个函数 `RemoveFirstHyphen` 把字符串第一个 `-` 干掉即可。
## 总结
TS 是一门编程语言,而不是一门简单的描述或者修饰符,很多复杂类型问题要动用逻辑思维来实现,而不是查查语法就能简单实现。
> 讨论地址是:[精读《Permutation, Flatten, Absolute...》· Issue #426 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/426)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/247.精读《Diff, AnyOf, IsUnion...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 25~32 题。
## 精读
### [Diff](https://github.com/type-challenges/type-challenges/blob/main/questions/00645-medium-diff/README.md)
实现 `Diff<A, B>`,返回一个新对象,类型为两个对象类型的 Diff:
```ts
type Foo = {
name: string
age: string
}
type Bar = {
name: string
age: string
gender: number
}
Equal<Diff<Foo, Bar> // { gender: number }
```
首先要思考 Diff 的计算方式,A 与 B 的 Diff 是找到 A 存在 B 不存在,与 B 存在 A 不存在的值,那么正好可以利用 `Exclude<X, Y>` 函数,它可以得到存在于 `X` 不存在于 `Y` 的值,我们只要用 `keyof A`、`keyof B` 代替 `X` 与 `Y`,并交替 A、B 位置就能得到 Diff:
```ts
// 本题答案
type Diff<A, B> = {
[K in Exclude<keyof A, keyof B> | Exclude<keyof B, keyof A>]:
K extends keyof A ? A[K] : (
K extends keyof B ? B[K]: never
)
}
```
Value 部分的小技巧我们之前也提到过,即需要用两套三元运算符保证访问的下标在对象中存在,即 `extends keyof` 的语法技巧。
### [AnyOf](https://github.com/type-challenges/type-challenges/blob/main/questions/00949-medium-anyof/README.md)
实现 `AnyOf` 函数,任意项为真则返回 `true`,否则返回 `false`,空数组返回 `false`:
```ts
type Sample1 = AnyOf<[1, '', false, [], {}]> // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]> // expected to be false.
```
本题有几个问题要思考:
第一是用何种判定思路?像这种判断数组内任意元素是否满足某个条件的题目,都可以用递归的方式解决,具体是先判断数组第一项,如果满足则继续递归判断剩余项,否则终止判断。这样能做但比较麻烦,还有种取巧的办法是利用 `extends Array<>` 的方式,让 TS 自动帮你遍历。
第二个是如何判断任意项为真?为真的情况很多,我们尝试枚举为假的 Case:`0` `undefined` `''` `undefined` `null` `never` `[]`。
结合上面两个思考,本题作如下解答不难想到:
```ts
type Falsy = '' | never | undefined | null | 0 | false | []
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true
```
但会遇到这个测试用例没通过:
```ts
AnyOf<[0, '', false, [], {}]>
```
如果此时把 `{}` 补在 `Falsy` 里,会发现除了这个 case 外,其他判断都挂了,原因是 `{ a: 1 } extends {}` 结果为真,因为 `{}` 并不表示空对象,而是表示所有对象类型,所以我们要把它换成 `Record<PropertyKey, never>`,以锁定空对象:
```ts
// 本题答案
type Falsy = '' | never | undefined | null | 0 | false | [] | Record<PropertyKey, never>
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true
```
### [IsNever](https://github.com/type-challenges/type-challenges/blob/main/questions/01042-medium-isnever/README.md)
实现 `IsNever` 判断值类型是否为 `never`:
```ts
type A = IsNever<never> // expected to be true
type B = IsNever<undefined> // expected to be false
type C = IsNever<null> // expected to be false
type D = IsNever<[]> // expected to be false
type E = IsNever<number> // expected to be false
```
首先我们可以毫不犹豫的写下一个错误答案:
```ts
type IsNever<T> = T extends never ? true :false
```
这个错误答案离正确答案肯定是比较近的,但错在无法判断 `never` 上。在 `Permutation` 全排列题中我们就认识到了 `never` 在泛型中的特殊性,它不会触发 `extends` 判断,而是直接终结,致使判断无效。
而解法也很简单,只要绕过 `never` 这个特性即可,包一个数组:
```ts
// 本题答案
type IsNever<T> = [T] extends [never] ? true :false
```
### [IsUnion](https://github.com/type-challenges/type-challenges/blob/main/questions/01097-medium-isunion/README.md)
实现 `IsUnion` 判断是否为联合类型:
```ts
type case1 = IsUnion<string> // false
type case2 = IsUnion<string|number> // true
type case3 = IsUnion<[string|number]> // false
```
这道题完全是脑筋急转弯了,因为 TS 肯定知道传入类型是否为联合类型,并且会对联合类型进行特殊处理,但并没有暴露联合类型的判断语法,所以我们只能对传入类型进行测试,推断是否为联合类型。
我们到现在能想到联合类型的特征只有两个:
1. 在 TS 处理泛型为联合类型时进行分发处理,即将联合类型拆解为独立项一一进行判定,最后再用 `|` 连接。
2. 用 `[]` 包裹联合类型可以规避分发的特性。
所以怎么判定传入泛型是联合类型呢?如果泛型进行了分发,就可以反推出它是联合类型。
难点就转移到了:如何判断泛型被分发了?首先分析一下,分发的效果是什么样:
```ts
A extends A
// 如果 A 是 1 | 2,分发结果是:
(1 extends 1 | 2) | (2 extends 1 | 2)
```
也就是这个表达式会被执行两次,第一个 `A` 在两次值分别为 `1` 与 `2`,而第二个 `A` 在两次执行中每次都是 `1 | 2`,但这两个表达式都是 `true`,无法体现分发的特殊性。
此时要利用包裹 `[]` 不分发的特性,即在分发后,由于在每次执行过程中,第一个 `A` 都是联合类型的某一项,因此用 `[]` 包裹后必然与原始值不相等,所以我们在 `extends` 分发过程中,再用 `[]` 包裹 `extends` 一次,如果此时匹配不上,说明产生了分发:
```ts
type IsUnion<A> = A extends A ? (
[A] extends [A] ? false : true
) : false
```
但这段代码依然不正确,因为在第一个三元表达式括号内,`A` 已经被分发,所以 `[A] extends [A]` 即便对联合类型也是判定为真的,此时需要用原始值代替 `extends` 后面的 `[A]`,骚操作出现了:
```ts
type IsUnion<A, B = A> = A extends A ? (
[B] extends [A] ? false : true
) : false
```
虽然我们申明了 `B = A`,但过程中因为 `A` 被分发了,所以运行时 `B` 是不等于 `A` 的,才使得我们达成目的。`[B]` 放 `extends` 前面是因为,`B` 是未被分发的,不可能被分发后的结果包含,所以分发时此条件必定为假。
最后因为测试用例有一个 `never` 情况,我们用刚才的 `IsNever` 函数提前判否即可:
```ts
// 本题答案
type IsUnion<A, B = A> = IsNever<A> extends true ? false : (
A extends A ? (
[B] extends [A] ? false : true
) : false
)
```
从该题我们可以深刻体会到 TS 的怪异之处,即 `type X<T> = T extends ...` 中 `extends` 前面的 `T` 不一定是你看到传入的 `T`,如果是联合类型的话,会分发为单个类型分别处理。
### [ReplaceKeys](https://github.com/type-challenges/type-challenges/blob/main/questions/01130-medium-replacekeys/README.md)
实现 `ReplaceKeys<Obj, Keys, Targets>` 将 `Obj` 中每个对象的 `Keys` Key 类型转化为符合 `Targets` 对象对应 Key 描述的类型,如果无法匹配到 `Targets` 则类型置为 `never`:
```ts
type NodeA = {
type: 'A'
name: string
flag: number
}
type NodeB = {
type: 'B'
id: number
flag: number
}
type NodeC = {
type: 'C'
name: string
flag: number
}
type Nodes = NodeA | NodeB | NodeC
type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.
type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never
```
本题别看描述很吓人,其实非常简单,思路:用 `K in keyof Obj` 遍历原始对象所有 Key,如果这个 Key 在描述的 `Keys` 中,且又在 `Targets` 中存在,则返回类型 `Targets[K]` 否则返回 `never`,如果不在描述的 `Keys` 中则用在对象里本来的类型:
```ts
// 本题答案
type ReplaceKeys<Obj, Keys, Targets> = {
[K in keyof Obj] : K extends Keys ? (
K extends keyof Targets ? Targets[K] : never
) : Obj[K]
}
```
### [Remove Index Signature](https://github.com/type-challenges/type-challenges/blob/main/questions/01367-medium-remove-index-signature/README.md)
实现 `RemoveIndexSignature<T>` 把对象 `<T>` 中 Index 下标移除:
```ts
type Foo = {
[key: string]: any;
foo(): void;
}
type A = RemoveIndexSignature<Foo> // expected { foo(): void }
```
该题思考的重点是如何将对象字符串 Key 识别出来,可以用 \`${infer P}\` 是否能识别到 `P` 来判断当前是否命中了字符串 Key:
```ts
// 本题答案
type RemoveIndexSignature<T> = {
[K in keyof T as K extends `${infer P}` ? P : never]: T[K]
}
```
### [Percentage Parser](https://github.com/type-challenges/type-challenges/blob/main/questions/01978-medium-percentage-parser/README.md)
实现 `PercentageParser<T>`,解析出百分比字符串的符号位与数字:
```ts
type PString1 = ''
type PString2 = '+85%'
type PString3 = '-85%'
type PString4 = '85%'
type PString5 = '85'
type R1 = PercentageParser<PString1> // expected ['', '', '']
type R2 = PercentageParser<PString2> // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3> // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4> // expected ["", "85", "%"]
type R5 = PercentageParser<PString5> // expected ["", "85", ""]
```
这道题充分说明了 TS 没有正则能力,尽量还是不要做正则的事情 ^_^。
回到正题,如果非要用 TS 实现,我们只能枚举各种场景:
```ts
// 本题答案
type PercentageParser<A extends string> =
// +/-xxx%
A extends `${infer X extends '+' | '-'}${infer Y}%`? [X, Y, '%'] : (
// +/-xxx
A extends `${infer X extends '+' | '-'}${infer Y}` ? [X, Y, ''] : (
// xxx%
A extends `${infer X}%` ? ['', X, '%'] : (
// xxx 包括 ['100', '%', ''] 这三种情况
A extends `${infer X}` ? ['', X, '']: never
)
)
)
```
这道题运用了 `infer` 可以无限进行分支判断的知识。
### [Drop Char](https://github.com/type-challenges/type-challenges/blob/main/questions/02070-medium-drop-char/README.md)
实现 `DropChar` 从字符串中移除指定字符:
```ts
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'
```
这道题和 `Replace` 很像,只要用递归不断把 `C` 排除掉即可:
```ts
// 本题答案
type DropChar<S, C extends string> = S extends `${infer A}${C}${infer B}` ?
`${A}${DropChar<B, C>}` : S
```
## 总结
写到这,越发觉得 TS 虽然具备图灵完备性,但在逻辑处理上还是不如 JS 方便,很多设计计算逻辑的题目的解法都不是很优雅。
但是解决这类题目有助于强化对 TS 基础能力组合的理解与综合运用,在解决实际类型问题时又是必不可少的。
> 讨论地址是:[精读《Diff, AnyOf, IsUnion...》· Issue #429 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/429)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/248.精读《MinusOne, PickByType, StartsWith...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 33~40 题。
## 精读
### [MinusOne](https://github.com/type-challenges/type-challenges/blob/main/questions/02257-medium-minusone/README.md)
用 TS 实现 `MinusOne` 将一个数字减一:
```ts
type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54
```
TS 没有 “普通” 的运算能力,但涉及数字却有一条生路,即 TS 可通过 `['length']` 访问数组长度,几乎所有数字计算都是通过它推导出来的。
这道题,我们只要构造一个长度为泛型长度 -1 的数组,获取其 `['length']` 属性即可,但该方案有一个硬伤,无法计算负值,因为数组长度不可能小于 0:
```ts
// 本题答案
type MinusOne<T extends number, arr extends any[] = []> = [
...arr,
''
]['length'] extends T
? arr['length']
: MinusOne<T, [...arr, '']>
```
该方案的原理不是原数字 -1,而是从 0 开始不断加 1,一直加到目标数字减一。但该方案没有通过 `MinusOne<1101>` 测试,因为递归 1000 次就是上限了。
还有一种能打破递归的思路,即:
```ts
type Count = ['1', '1', '1'] extends [...infer T, '1'] ? T['length'] : 0 // 2
```
也就是把减一转化为 `extends [...infer T, '1']`,这样数组 `T` 的长度刚好等于答案。那么难点就变成了如何根据传入的数字构造一个等长的数组?即问题变成了如何实现 `CountTo<N>` 生成一个长度为 `N`,每项均为 `1` 的数组,而且生成数组的递归效率也要高,否则还会遇到递归上限的问题。
网上有一个神仙解法,笔者自己想不到,但是可以拿出来给大家分析下:
```ts
type CountTo<
T extends string,
Count extends 1[] = []
> = T extends `${infer First}${infer Rest}`
? CountTo<Rest, N<Count>[keyof N & First]>
: Count
type N<T extends 1[] = []> = {
'0': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
'1': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1]
'2': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1]
'3': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1]
'4': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1]
'5': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
1,
1,
1,
1,
1
]
'6': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
1,
1,
1,
1,
1,
1
]
'7': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
1,
1,
1,
1,
1,
1,
1
]
'8': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
1,
1,
1,
1,
1,
1,
1,
1
]
'9': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
1,
1,
1,
1,
1,
1,
1,
1,
1
]
}
```
也就是该方法可以高效的实现 `CountTo<'1000'>` 产生长度为 1000,每项为 `1` 的数组,更具体一点,只需要遍历 `<T>` 字符串长度次数,比如 `1000` 只要递归 4 次,而 `10000` 也只需要递归 5 次。
`CountTo` 函数体的逻辑是,如果字符串 `T` 非空,就拆为第一个字符 `First` 与剩余字符 `Rest`,然后拿剩余字符递归,但是把 `First` 一次性生成到了正确的长度。最核心的逻辑就是函数 `N<T>` 了,它做的其实是把 `T` 的数组长度放大 10 倍再追加上当前数量的 1 在数组末尾。
而 `keyof N & First` 也是神来之笔,此处本意就是访问 `First` 下标,但 TS 不知道它是一个安全可访问的下标,而 `keyof N & First` 最终值还是 `First`,也可以被 TS 安全识别为下标。
拿 `CountTo<'123'>` 举例:
第一次执行 `First='1'`、`Rest='23'`:
```ts
CountTo<'23', N<[]>['1']>
// 展开时,...[] 还是 [],所以最终结果为 ['1']
```
第二次执行 `First='2'`、`Rest='3'`
```ts
CountTo<'3', N<['1']>['2']>
// 展开时,...[] 有 10 个,所以 ['1'] 变成了 10 个 1,追加上 N 映射表里的 2 个 1,现在一共有 12 个 1
```
第三次执行 `First='3'`、`Rest=''`
```ts
CountTo<'', N<['1', ...共 12 个]>['3']>
// 展开时,...[] 有 10 个,所以 12 个 1 变成 120 个,加上映射表中 3,一共有 123 个 1
```
总结一下,就是将数字 `T` 变成字符串,从最左侧开始获取,每次都把已经积累的数组数量乘以 10 再追加上当前值数量的 1,实现递归次数极大降低。
### [PickByType](https://github.com/type-challenges/type-challenges/blob/main/questions/02595-medium-pickbytype/README.md)
实现 `PickByType<P, Q>`,将对象 `P` 中类型为 `Q` 的 key 保留:
```ts
type OnlyBoolean = PickByType<
{
name: string
count: number
isReadonly: boolean
isEnable: boolean
},
boolean
> // { isReadonly: boolean; isEnable: boolean; }
```
本题很简单,因为之前碰到 Remove Index Signature 题目时,我们用了 `K in keyof P as xxx` 来对 Key 位置进行进一步判断,所以只要 `P[K] extends Q` 就保留,否则返回 `never` 即可:
```ts
// 本题答案
type PickByType<P, Q> = {
[K in keyof P as P[K] extends Q ? K : never]: P[K]
}
```
### [StartsWith](https://github.com/type-challenges/type-challenges/blob/main/questions/02688-medium-startswith/README.md)
实现 `StartsWith<T, U>` 判断字符串 `T` 是否以 `U` 开头:
```ts
type a = StartsWith<'abc', 'ac'> // expected to be false
type b = StartsWith<'abc', 'ab'> // expected to be true
type c = StartsWith<'abc', 'abcd'> // expected to be false
```
本题也比较简单,用递归 + 首字符判等即可破解:
```ts
// 本题答案
type StartsWith<
T extends string,
U extends string
> = U extends `${infer US}${infer UE}`
? T extends `${infer TS}${infer TE}`
? TS extends US
? StartsWith<TE, UE>
: false
: false
: true
```
思路是:
1. `U` 如果为空字符串则匹配一切场景,直接返回 `true`;否则 `U` 可以拆为以 `US`(U Start) 开头、`UE`(U End) 的字符串进行后续判定。
2. 接着上面的判定,如果 `T` 为空字符串则不可能被 `U` 匹配,直接返回 `false`;否则 `T` 可以拆为以 `TS`(T Start) 开头、`TE`(T End) 的字符串进行后续判定。
3. 接着上面的判定,如果 `TS extends US` 说明此次首字符匹配了,则递归匹配剩余字符 `StartsWith<TE, UE>`,如果首字符不匹配提前返回 `false`。
笔者看了一些答案后发现还有一种降维打击方案:
```ts
// 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${string}`
? true
: false
```
没想到还可以用 `${string}` 匹配任意字符串进行 `extends` 判定,有点正则的意思了。当然 `${string}` 也可以被 `${infer X}` 代替,只是拿到的 `X` 不需要再用到了:
```ts
// 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${infer X}`
? true
: false
```
笔者还试了下面的答案在后缀 Diff 部分为 string like number 时也正确:
```ts
// 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${number}`
? true
: false
```
说明字符串模板最通用的指代是 `${infer X}` 或 `${string}`,如果要匹配特定的数字类字符串也可以混用 `${number}`。
### EndsWith
实现 `EndsWith<T, U>` 判断字符串 `T` 是否以 `U` 结尾:
```ts
type a = EndsWith<'abc', 'bc'> // expected to be true
type b = EndsWith<'abc', 'abc'> // expected to be true
type c = EndsWith<'abc', 'd'> // expected to be false
```
有了上题的经验,这道题不要太简单:
```ts
// 本题答案
type EndsWith<T extends string, U extends string> = T extends `${string}${U}`
? true
: false
```
这可以看出 TS 的技巧掌握了就非常简单,但不知道就几乎无解,或者用很笨的递归来解决。
### [PartialByKeys](https://github.com/type-challenges/type-challenges/blob/main/questions/02757-medium-partialbykeys/README.md)
实现 `PartialByKeys<T, K>`,使 `K` 匹配的 Key 变成可选的定义,如果不传 `K` 效果与 `Partial<T>` 一样:
```ts
interface User {
name: string
age: number
address: string
}
type UserPartialName = PartialByKeys<User, 'name'> // { name?:string; age:number; address:string }
```
看到题目要求是不传参数时和 `Partial<T>` 行为一直,就应该能想到应该这么起头写个默认值:
```ts
type PartialByKeys<T, K = keyof T> = {}
```
我们得用可选与不可选分别描述两个对象拼起来,因为 TS 不支持同一个对象下用两个 `keyof` 描述,所以只能写成两个对象:
```ts
type PartialByKeys<T, K = keyof T> = {
[Q in keyof T as Q extends K ? Q : never]?: T[Q]
} & {
[Q in keyof T as Q extends K ? never : Q]: T[Q]
}
```
但不匹配测试用例,原因是最终类型正确,但因为分成了两个对象合并无法匹配成一个对象,所以需要用一点点 Magic 行为合并:
```ts
// 本题答案
type PartialByKeys<T, K = keyof T> = {
[Q in keyof T as Q extends K ? Q : never]?: T[Q]
} & {
[Q in keyof T as Q extends K ? never : Q]: T[Q]
} extends infer R
? {
[Q in keyof R]: R[Q]
}
: never
```
将一个对象 `extends infer R` 再重新展开一遍看似无意义,但确实让类型上合并成了一个对象,很有意思。我们也可以将其抽成一个函数 `Merge<T>` 来使用。
本题还有一个函数组合的答案:
```ts
// 本题答案
type Merge<T> = {
[K in keyof T]: T[K]
}
type PartialByKeys<T, K extends PropertyKey = keyof T> = Merge<
Partial<T> & Omit<T, K>
>
```
- 利用 `Partial & Omit` 来合并对象。
- 因为 `Omit<T, K>` 中 `K` 有来自于 `keyof T` 的限制,而测试用例又包含 `unknown` 这种不存在的 Key 值,此时可以用 `extends PropertyKey` 处理此场景。
### [RequiredByKeys](https://github.com/type-challenges/type-challenges/blob/main/questions/02759-medium-requiredbykeys/README.md)
实现 `RequiredByKeys<T, K>`,使 `K` 匹配的 Key 变成必选的定义,如果不传 `K` 效果与 `Required<T>` 一样:
```ts
interface User {
name?: string
age?: number
address?: string
}
type UserRequiredName = RequiredByKeys<User, 'name'> // { name: string; age?: number; address?: string }
```
和上题正好相反,答案也呼之欲出了:
```ts
type Merge<T> = {
[K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
Required<T> & Omit<T, K>
>
```
等等,一个测试用例都没过,为啥呢?仔细想想发现确实暗藏玄机:
```ts
Merge<{
a: number
} & {
a?: number
}> // 结果是 { a: number }
```
也就是同一个 Key 可选与必选同时存在时,合并结果是必选。上一题因为将必选 `Omit` 掉了,所以可选不会被必选覆盖,但本题 `Merge<Required<T> & Omit<T, K>>`,前面的 `Required<T>` 必选优先级最高,后面的 `Omit<T, K>` 虽然本身逻辑没错,但无法把必选覆盖为可选,因此测试用例都挂了。
解法就是破解这一特征,用原始对象 & 仅包含 `K` 的必选对象,使必选覆盖前面的可选 Key。后者可以 `Pick` 出来:
```ts
type Merge<T> = {
[K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
T & Required<Pick<T, K>>
>
```
这样就剩一个单测没通过了:
```ts
Expect<Equal<RequiredByKeys<User, 'name' | 'unknown'>, UserRequiredName>>
```
我们还要兼容 `Pick` 访问不存在的 Key,用 `extends` 躲避一下即可:
```ts
// 本题答案
type Merge<T> = {
[K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
T & Required<Pick<T, K extends keyof T ? K : never>>
>
```
### [Mutable](https://github.com/type-challenges/type-challenges/blob/main/questions/02793-medium-mutable/README.md)
实现 `Mutable<T>`,将对象 `T` 的所有 Key 变得可写:
```ts
interface Todo {
readonly title: string
readonly description: string
readonly completed: boolean
}
type MutableTodo = Mutable<Todo> // { title: string; description: string; completed: boolean; }
```
把对象从可写变成不可写:
```ts
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
```
从不可写改成可写也简单,主要看你是否记住了这个语法:`-readonly`:
```ts
// 本题答案
type Mutable<T extends object> = {
-readonly [K in keyof T]: T[K]
}
```
### [OmitByType](https://github.com/type-challenges/type-challenges/blob/main/questions/02852-medium-omitbytype/README.md)
实现 `OmitByType<T, U>` 根据类型 U 排除 T 中的 Key:
```ts
type OmitBoolean = OmitByType<
{
name: string
count: number
isReadonly: boolean
isEnable: boolean
},
boolean
> // { name: string; count: number }
```
本题和 `PickByType` 正好反过来,只要把 `extends` 后内容对调一下即可:
```ts
// 本题答案
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
```
## 总结
本周的题目除了 `MinusOne` 那道神仙解法比较难以外,其他的都比较常见,其中 `Merge` 函数的妙用需要领悟一下。
> 讨论地址是:[精读《MinusOne, PickByType, StartsWith...》· Issue #430 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/430)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/249.精读《ObjectEntries, Shift, Reverse...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 41~48 题。
## 精读
### [ObjectEntries](https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md)
实现 TS 版本的 `Object.entries`:
```ts
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];
```
经过前面的铺垫,大家应该熟悉了 TS 思维思考问题,这道题看到后第一个念头应该是:如何先把对象转换为联合类型?这个问题不解决,就无从下手。
对象或数组转联合类型的思路都是类似的,一个数组转联合类型用 `[number]` 作为下标:
```ts
['1', '2', '3']['number'] // '1' | '2' | '3'
```
对象的方式则是 `[keyof T]` 作为下标:
```ts
type ObjectToUnion<T> = T[keyof T]
```
再观察这道题,联合类型每一项都是数组,分别是 Key 与 Value,这样就比较好写了,我们只要构造一个 Value 是符合结构的对象即可:
```ts
type ObjectEntries<T> = {
[K in keyof T]: [K, T[K]]
}[keyof T]
```
为了通过单测 `ObjectEntries<{ key?: undefined }>`,让 Key 位置不出现 `undefined`,需要强制把对象描述为非可选 Key:
```TS
type ObjectEntries<T> = {
[K in keyof T]-?: [K, T[K]]
}[keyof T]
```
为了通过单测 `ObjectEntries<Partial<Model>>`,得将 Value 中 `undefined` 移除:
```ts
// 本题答案
type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
type ObjectEntries<T> = {
[K in keyof T]-?: [K, RemoveUndefined<T[K]>]
}[keyof T]
```
### [Shift](https://github.com/type-challenges/type-challenges/blob/main/questions/03062-medium-shift/README.md)
实现 TS 版 `Array.shift`:
```ts
type Result = Shift<[3, 2, 1]> // [2, 1]
```
这道题应该是简单难度的,只要把第一项抛弃即可,利用 `infer` 轻松实现:
```ts
// 本题答案
type Shift<T> = T extends [infer First, ...infer Rest] ? Rest : never
```
### [Tuple to Nested Object](https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md)
实现 `TupleToNestedObject<T, P>`,其中 `T` 仅接收字符串数组,`P` 是任意类型,生成一个递归对象结构,满足如下结果:
```ts
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
```
这道题用到了 5 个知识点:递归、辅助类型、`infer`、如何指定对象 Key、`PropertyKey`,你得全部知道并组合起来才能解决该题。
首先因为返回值是个递归对象,递归过程中必定不断修改它,因此给泛型添加第三个参数 `R` 存储这个对象,并且在递归数组时从最后一个开始,这样从最内层对象开始一点点把它 “包起来”:
```ts
type TupleToNestedObject<T, U, R = U> = /** 伪代码
T extends [...infer Rest, infer Last]
*/
```
下一步是如何描述一个对象 Key?之前 `Chainable Options` 例子我们学到的 `K in Q`,但需要注意直接这么写会报错,因为必须申明 `Q extends PropertyKey`。最后再处理一下递归结束条件,即 `T` 变成空数组时直接返回 `R`:
```ts
// 本题答案
type TupleToNestedObject<T, U, R = U> = T extends [] ? R : (
T extends [...infer Rest, infer Last extends PropertyKey] ? (
TupleToNestedObject<Rest, U, {
[P in Last]: R
}>
) : never
)
```
### [Reverse](https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md)
实现 TS 版 `Array.reverse`:
```ts
type a = Reverse<['a', 'b']> // ['b', 'a']
type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']
```
这道题比上一题简单,只需要用一个递归即可:
```ts
// 本题答案
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
```
### [Flip Arguments](https://github.com/type-challenges/type-challenges/blob/main/questions/03196-medium-flip-arguments/README.md)
实现 `FlipArguments<T>` 将函数 `T` 的参数反转:
```ts
type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>
// (arg0: boolean, arg1: number, arg2: string) => void
```
本题与上题类似,只是反转内容从数组变成了函数的参数,只要用 `infer` 定义出函数的参数,利用 `Reverse` 函数反转一下即可:
```ts
// 本题答案
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
type FlipArguments<T> =
T extends (...args: infer Args) => infer Result ? (...args: Reverse<Args>) => Result : never
```
### [FlattenDepth](https://github.com/type-challenges/type-challenges/blob/main/questions/03243-medium-flattendepth/README.md)
实现指定深度的 Flatten:
```ts
type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1
```
这道题比之前的 `Flatten` 更棘手一些,因为需要控制打平的次数。
基本想法就是,打平 `Deep` 次,所以需要实现打平一次的函数,再根据 `Deep` 值递归对应次:
```ts
type FlattenOnce<T extends any[], U extends any[] = []> = T extends [infer X, ...infer Y] ? (
X extends any[] ? FlattenOnce<Y, [...U, ...X]> : FlattenOnce<Y, [...U, X]>
) : U
```
然后再实现主函数 `FlattenDepth`,因为 TS 无法实现 +、- 号运算,我们必须用数组长度判断与操作数组来辅助实现:
```ts
// FlattenOnce
type FlattenDepth<
T extends any[],
U extends number = 1,
P extends any[] = []
> = P['length'] extends U ? T : (
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
)
```
当递归没有达到深度 `U` 时,就用 `[...P, any]` 的方式给数组塞一个元素,下次如果能匹配上 `P['length'] extends U` 说明递归深度已达到。
但考虑到测试用例 `FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>` 会引发超长次数递归,需要提前终止,即如果打平后已经是平的,就不用再继续递归了,此时可以用 `FlattenOnce<T> extends T` 判断:
```ts
// 本题答案
// FlattenOnce
type FlattenDepth<
T extends any[],
U extends number = 1,
P extends any[] = []
> = P['length'] extends U ? T : (
FlattenOnce<T> extends T ? T : (
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
)
)
```
### [BEM style string](https://github.com/type-challenges/type-challenges/blob/main/questions/03326-medium-bem-style-string/README.md)
实现 `BEM` 函数完成其规则拼接:
```ts
Expect<Equal<BEM<'btn', [], ['small', 'medium', 'large']>, 'btn--small' | 'btn--medium' | 'btn--large' >>,
```
之前我们了解了通过下标将数组或对象转成联合类型,这里还有一个特殊情况,即字符串中通过这种方式申明每一项,会自动笛卡尔积为新的联合类型:
```ts
type BEM<B extends string, E extends string[], M extends string[]> =
`${B}__${E[number]}--${M[number]}`
```
这是最简单的写法,但没有考虑项不存在的情况。不如创建一个 `SafeUnion` 函数,当传入值不存在时返回空字符串,保证安全的跳过:
```ts
type IsNever<TValue> = TValue[] extends never[] ? true : false;
type SafeUnion<TUnion> = IsNever<TUnion> extends true ? "" : TUnion;
```
最终代码:
```ts
// 本题答案
// IsNever, SafeUnion
type BEM<B extends string, E extends string[], M extends string[]> =
`${B}${SafeUnion<`__${E[number]}`>}${SafeUnion<`--${M[number]}`>}`
```
### [InorderTraversal](https://github.com/type-challenges/type-challenges/blob/main/questions/03376-medium-inordertraversal/README.md)
实现 TS 版二叉树中序遍历:
```ts
const tree1 = {
val: 1,
left: null,
right: {
val: 2,
left: {
val: 3,
left: null,
right: null,
},
right: null,
},
} as const
type A = InorderTraversal<typeof tree1> // [1, 3, 2]
```
首先回忆一下二叉树中序遍历 JS 版的实现:
```js
function inorderTraversal(tree) {
if (!tree) return []
return [
...inorderTraversal(tree.left),
res.push(val),
...inorderTraversal(tree.right)
]
}
```
对 TS 来说,实现递归的方式有一点点不同,即通过 `extends TreeNode` 来判定它不是 Null 从而递归:
```ts
// 本题答案
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode] ? (
[
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
]
): []
```
你可能会问,问什么不能像 JS 一样,用 `null` 做判断呢?
```ts
type InorderTraversal<T extends TreeNode | null> = [T] extends [null] ? [] : (
[ // error
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
]
)
```
如果这么写会发现 TS 抛出了异常,因为 TS 不能确定 `T` 此时符合 `TreeNode` 类型,所以要执行操作时一般采用正向判断。
## 总结
这些类型挑战题目需要灵活组合 TS 的基础知识点才能破解,常用的包括:
- 如何操作对象,增减 Key、只读、合并为一个对象等。
- 递归,以及辅助类型。
- `infer` 知识点。
- 联合类型,如何从对象或数组生成联合类型,字符串模板与联合类型的关系。
> 讨论地址是:[精读《ObjectEntries, Shift, Reverse...》· Issue #431 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/431)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/250.精读《Flip, Fibonacci, AllCombinations...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 49~56 题。
## 精读
### [Flip](https://github.com/type-challenges/type-challenges/blob/main/questions/04179-medium-flip/README.md)
实现 `Flip<T>`,将对象 `T` 中 Key 与 Value 对调:
```ts
Flip<{ a: "x", b: "y", c: "z" }>; // {x: 'a', y: 'b', z: 'c'}
Flip<{ a: 1, b: 2, c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'}
Flip<{ a: false, b: true }>; // {false: 'a', true: 'b'}
```
在 `keyof` 描述对象时可以通过 `as` 追加变形,所以这道题应该这样处理:
```ts
type Flip<T> = {
[K in keyof T as T[K]]: K
}
```
由于 Key 位置只能是 String or Number,所以 `T[K]` 描述 Key 会显示错误,我们需要限定 Value 的类型:
```ts
type Flip<T extends Record<string, string | number>> = {
[K in keyof T as T[K]]: K
}
```
但这个答案无法通过测试用例 `Flip<{ pi: 3.14; bool: true }>`,原因是 `true` 不能作为 Key。只能用字符串 `'true'` 作为 Key,所以我们得强行把 Key 位置转化为字符串:
```ts
// 本题答案
type Flip<T extends Record<string, string | number | boolean>> = {
[K in keyof T as `${T[K]}`]: K
}
```
### [Fibonacci Sequence](https://github.com/type-challenges/type-challenges/blob/main/questions/04182-medium-fibonacci-sequence/README.md)
用 TS 实现斐波那契数列计算:
```ts
type Result1 = Fibonacci<3> // 2
type Result2 = Fibonacci<8> // 21
```
由于测试用例没有特别大的 Case,我们可以放心用递归实现。JS 版的斐波那契非常自然,但 TS 版我们只能用数组长度模拟计算,代码写起来自然会比较扭曲。
首先需要一个额外变量标记递归了多少次,递归到第 N 次结束:
```ts
type Fibonacci<T extends number, N = [1]> = N['length'] extends T ? (
// xxx
) : Fibonacci<T, [...N, 1]>
```
上面代码每次执行都判断是否递归完成,否则继续递归并把计数器加一。我们还需要一个数组存储答案,一个数组存储上一个数:
```ts
// 本题答案
type Fibonacci<
T extends number,
N extends number[] = [1],
Prev extends number[] = [1],
Cur extends number[] = [1]
> = N['length'] extends T
? Prev['length']
: Fibonacci<T, [...N, 1], Cur, [...Prev, ...Cur]>
```
递归时拿 `Cur` 代替下次的 `Prev`,用 `[...Prev, ...Cur]` 代替下次的 `Cur`,也就是说,下次的 `Cur` 符合斐波那契定义。
### [AllCombinations](https://github.com/type-challenges/type-challenges/blob/main/questions/04260-medium-nomiwase/README.md)
实现 `AllCombinations<S>` 对字符串 `S` 全排列:
```ts
type AllCombinations_ABC = AllCombinations<'ABC'>
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'
```
首先要把 `ABC` 字符串拆成一个个独立的联合类型,进行二次组合才可能完成全排列:
```ts
type StrToUnion<S> = S extends `${infer F}${infer R}`
? F | StrToUnion<R>
: never
```
`infer` 描述字符串时,第一个指向第一个字母,第二个指向剩余字母;对剩余字符串递归可以将其逐一拆解为单个字符并用 `|` 连接:
```ts
StrToUnion<'ABC'> // 'A' | 'B' | 'C'
```
将 `StrToUnion<'ABC'>` 的结果记为 `U`,则利用对象转联合类型特征,可以制造出 `ABC` 在三个字母时的全排列:
```ts
{ [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // `ABC${any}` | `ACB${any}` | `BAC${any}` | `BCA${any}` | `CAB${any}` | `CBA${any}`
```
然而只要在每次递归时巧妙的加上 `'' |` 就可以直接得到答案了:
```ts
type AllCombinations<S extends string, U extends string = StrToUnion<S>> =
| ''
| { [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'
```
为什么这么神奇呢?这是因为每次递归时都会经历 `''`、`'A'`、`'AB'`、`'ABC'` 这样逐渐累加字符的过程,而每次都会遇到 `'' |` 使其自然形成了联合类型,比如遇到 `'A'` 时,会自然形成 `'A'` 这项联合类型,同时继续用 `'A'` 与 `Exclude<'A' | 'B' | 'C', 'A'>` 进行组合。
更精妙的是,第一次执行时的 `''` 填补了全排列的第一个 Case。
最后注意到上面的结果产生了一个 Error:"Type instantiation is excessively deep and possibly infinite",即这样递归可能产生死循环,因为 `Exclude<U, K>` 的结果可能是 `never`,所以最后在开头修补一下对 `never` 的判否,利用之前学习的知识,`never` 不会进行联合类型展开,所以我们用 `[never]` 判断来规避:
```ts
// 本题答案
type AllCombinations<S extends string, U extends string = StrToUnion<S>> = [
U
] extends [never]
? ''
: '' | { [K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U]
```
### [Greater Than](https://github.com/type-challenges/type-challenges/blob/main/questions/04425-medium-greater-than/README.md)
实现 `GreaterThan<T, U>` 判断 `T > U`:
```ts
GreaterThan<2, 1> //should be true
GreaterThan<1, 1> //should be false
GreaterThan<10, 100> //should be false
GreaterThan<111, 11> //should be true
```
因为 TS 不支持加减法与大小判断,看到这道题时就应该想到有两种做法,一种是递归,但会受限于入参数量限制,可能堆栈溢出,一种是参考 [MinusOne](https://github.com/ascoders/weekly/blob/master/TS%20%E7%B1%BB%E5%9E%8B%E4%BD%93%E6%93%8D/248.%E7%B2%BE%E8%AF%BB%E3%80%8AMinusOne%2C%20PickByType%2C%20StartsWith...%E3%80%8B.md) 的特殊方法,用巧妙的方式构造出长度符合预期的数组,用数组 `['length']` 进行比较。
先说第一种,递归肯定要有一个递增 Key,拿 `T` `U` 先后进行对比,谁先追上这个数,谁就是较小的那个:
```ts
// 本题答案
type GreaterThan<T, U, R extends number[] = []> = T extends R['length']
? false
: U extends R['length']
? true
: GreaterThan<T, U, [...R, 1]>
```
另一种做法是快速构造两个长度分别等于 `T` `U` 的数组,用数组快速判断谁更长。构造方式不再展开,参考 `MinusOne` 那篇的方法即可,重点说下如何快速判断 `[1, 1]` 与 `[1, 1, 1]` 谁更大。
因为 TS 没有大小判断能力,所以拿到了 `['length']` 也没有用,我们得考虑 `arr1 extends arr2` 这种方式。可惜的是,长度不相等的数组,`extends` 永远等于 `false`:
```ts
[1,1,1,1] extends [1,1,1] ? true : false // false
[1,1,1] extends [1,1,1,1] ? true : false // false
[1,1,1] extends [1,1,1] ? true : false // true
```
但我们期望进行如下判断:
```ts
ArrGreaterThan<[1,1,1,1],[1,1,1]> // true
ArrGreaterThan<[1,1,1],[1,1,1,1]> // false
ArrGreaterThan<[1,1,1],[1,1,1]> // false
```
解决方法非常体现 TS 思维:既然俩数组相等才返回 `true`,那我们用 `[...T, ...any]` 进行补充判定,如果能判定为 `true`,就说明前者长度更短(因为后者补充几项后可以判等):
```ts
type ArrGreaterThan<T extends 1[], U extends 1[]> = U extends [...T, ...any]
? false
: true
```
这样一来,第二种答案就是这样的:
```ts
// 本题答案
type GreaterThan<T extends number, U extends number> = ArrGreaterThan<
NumberToArr<T>,
NumberToArr<U>
>
```
### [Zip](https://github.com/type-challenges/type-challenges/blob/main/questions/04471-medium-zip/README.md)
实现 TS 版 `Zip` 函数:
```ts
type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]]
```
此题同样配合辅助变量,进行计数递归,并额外用一个类型变量存储结果:
```ts
// 本题答案
type Zip<
T extends any[],
U extends any[],
I extends number[] = [],
R extends any[] = []
> = I['length'] extends T['length']
? R
: U[I['length']] extends undefined
? Zip<T, U, [...I, 0], R>
: Zip<T, U, [...I, 0], [...R, [T[I['length']], U[I['length']]]]>
```
`[...R, [T[I['length']], U[I['length']]]]` 在每次递归时按照 Zip 规则添加一条结果,其中 `I['length']` 起到的作用类似 for 循环的下标 i,只是在 TS 语法中,我们只能用数组的方式模拟这种计数。
### [IsTuple](https://github.com/type-challenges/type-challenges/blob/main/questions/04484-medium-istuple/README.md)
实现 `IsTuple<T>` 判断 `T` 是否为元组类型(Tuple):
```ts
type case1 = IsTuple<[number]> // true
type case2 = IsTuple<readonly [number]> // true
type case3 = IsTuple<number[]> // false
```
不得不吐槽的是,无论是 TS 内部或者词法解析都是更有效的判断方式,但如果用 TS 来实现,就要换一种思路了。
Tuple 与 Array 在 TS 里的区别是前者长度有限,后者长度无限,从结果来看,如果访问其 `['length']` 属性,前者一定是一个固定数字,而后者返回 `number`,用这个特性判断即可:
```ts
// 本题答案
type IsTuple<T> = [T] extends [never]
? false
: T extends readonly any[]
? number extends T['length']
? false
: true
: false
```
其实这个答案是根据单测一点点试出来的,因为存在 `IsTuple<{ length: 1 }>` 单测用例,它可以通过 `number extends T['length']` 的校验,但因为其本身不是数组类型,所以无法通过 `T extends readonly any[]` 的前置判断。
### [Chunk](https://github.com/type-challenges/type-challenges/blob/main/questions/04499-medium-chunk/README.md)
实现 TS 版 `Chunk`:
```ts
type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]]
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]]
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]]
```
老办法还是要递归,需要一个变量记录当前收集到 Chunk 里的内容,在 Chunk 达到上限时释放出来,同时也要注意未达到上限就结束时也要释放出来。
```ts
type Chunk<
T extends any[],
N extends number = 1,
Chunked extends any[] = []
> = T extends [infer First, ...infer Last]
? Chunked['length'] extends N
? [Chunked, ...Chunk<T, N>]
: Chunk<Last, N, [...Chunked, First]>
: [Chunked]
```
`Chunked['length'] extends N` 判断 `Chunked` 数组长度达到 `N` 后就释放出来,否则把当前数组第一项 `First` 继续塞到 `Chunked` 数组,数组项从 `Last` 开始继续递归。
我们发现 `Chunk<[], 1>` 这个单测没过,因为当 `Chunked` 没有项目时,就无需成组了,所以完整的答案是:
```ts
// 本题答案
type Chunk<
T extends any[],
N extends number = 1,
Chunked extends any[] = []
> = T extends [infer Head, ...infer Tail]
? Chunked['length'] extends N
? [Chunked, ...Chunk<T, N>]
: Chunk<Tail, N, [...Chunked, Head]>
: Chunked extends []
? Chunked
: [Chunked]
```
### [Fill](https://github.com/type-challenges/type-challenges/blob/main/questions/04518-medium-fill/README.md)
实现 `Fill<T, N, Start?, End?>`,将数组 `T` 的每一项替换为 `N`:
```ts
type exp = Fill<[1, 2, 3], 0> // expected to be [0, 0, 0]
```
这道题也需要用递归 + Flag 方式解决,即定义一个 `I` 表示当前递归的下标,一个 `Flag` 表示是否到了要替换的下标,只要到了这个下标,该 `Flag` 就永远为 `true`:
```ts
type Fill<
T extends unknown[],
N,
Start extends number = 0,
End extends number = T['length'],
I extends any[] = [],
Flag extends boolean = I['length'] extends Start ? true : false
>
```
由于递归会不断生成完整答案,我们将 `T` 定义为可变的,即每次仅处理第一条,如果当前 `Flag` 为 `true` 就采用替换值 `N`,否则就拿原本的第一个字符:
```ts
type Fill<
T extends unknown[],
N,
Start extends number = 0,
End extends number = T['length'],
I extends any[] = [],
Flag extends boolean = I['length'] extends Start ? true : false
> = I['length'] extends End
? T
: T extends [infer F, ...infer R]
? Flag extends false
? [F, ...Fill<R, N, Start, End, [...I, 0]>]
: [N, ...Fill<R, N, Start, End, [...I, 0]>]
: T
```
但这个答案没有通过测试,仔细想想发现 `Flag` 在 `I` 长度超过 `Start` 后就判定失败了,为了让超过后维持 `true`,在 `Flag` 为 `true` 时将其传入覆盖后续值即可:
```ts
// 本题答案
type Fill<
T extends unknown[],
N,
Start extends number = 0,
End extends number = T['length'],
I extends any[] = [],
Flag extends boolean = I['length'] extends Start ? true : false
> = I['length'] extends End
? T
: T extends [infer F, ...infer R]
? Flag extends false
? [F, ...Fill<R, N, Start, End, [...I, 0]>]
: [N, ...Fill<R, N, Start, End, [...I, 0], Flag>]
: T
```
## 总结
勤用递归、辅助变量可以解决大部分本周遇到的问题。
> 讨论地址是:[精读《Flip, Fibonacci, AllCombinations...》· Issue #432 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/432)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/251.精读《Trim Right, Without, Trunc...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 57~62 题。
## 精读
### [Trim Right](https://github.com/type-challenges/type-challenges/blob/main/questions/04803-medium-trim-right/README.md)
实现 `TrimRight` 删除右侧空格:
```ts
type Trimed = TrimRight<' Hello World '> // expected to be ' Hello World'
```
用 `infer` 找出空格前的字符串递归一下即可:
```ts
type TrimRight<S extends string> = S extends `${infer R}${' '}`
? TrimRight<R>
: S
```
再补上测试用例的边界情况,`\n` 与 `\t` 后就是完整答案了:
```ts
// 本题答案
type TrimRight<S extends string> = S extends `${infer R}${' ' | '\n' | '\t'}`
? TrimRight<R>
: S
```
### [Without](https://github.com/type-challenges/type-challenges/blob/main/questions/05117-medium-without/README.md)
实现 `Without<T, U>`,从数组 `T` 中移除 `U` 中元素:
```ts
type Res = Without<[1, 2], 1> // expected to be [2]
type Res1 = Without<[1, 2, 4, 1, 5], [1, 2]> // expected to be [4, 5]
type Res2 = Without<[2, 3, 2, 3, 2, 3, 2, 3], [2, 3]> // expected to be []
```
该题最难的点在于,参数 `U` 可能是字符串或字符串数组,我们要判断是否存在只能用 `extends`,这样就存在两个问题:
1. 既是字符串又是数组如何判断,合在一起判断还是分开判断?
2. `[1] extends [1, 2]` 为假,数组模式如何判断?
可以用数组转 Union 的方式解决该问题:
```ts
type ToUnion<T> = T extends any[] ? T[number] : T
```
这样无论是数字还是数组,都会转成联合类型,而联合类型很方便判断 `extends` 包含关系:
```ts
// 本题答案
type Without<T, U> = T extends [infer H, ...infer R]
? H extends ToUnion<U>
? Without<R, U>
: [H, ...Without<R, U>]
: []
```
每次取数组第一项,判断是否被 `U` 包含,是的话就丢弃(丢弃的动作是把 `H` 抛弃继续递归),否则包含(包含的动作是形成新的数组 `[H, ...]` 并把递归内容解构塞到后面)。
### [Trunc](https://github.com/type-challenges/type-challenges/blob/main/questions/05140-medium-trunc/README.md)
实现 `Math.trunc` 相同功能的函数 `Trunc`:
```ts
type A = Trunc<12.34> // 12
```
如果入参是字符串就很简单了:
```ts
type Trunc<T> = T extends `${infer H}.${infer R}` ? H : ''
```
如果不是字符串,将其转换为字符串即可:
```ts
// 本题答案
type Trunc<T extends string | number> = `${T}` extends `${infer H}.${infer R}`
? H
: `${T}`
```
### [IndexOf](https://github.com/type-challenges/type-challenges/blob/main/questions/05153-medium-indexof/README.md)
实现 `IndexOf` 寻找元素所在下标,找不到返回 `-1`:
```ts
type Res = IndexOf<[1, 2, 3], 2>; // expected to be 1
type Res1 = IndexOf<[2,6, 3,8,4,1,7, 3,9], 3>; // expected to be 2
type Res2 = IndexOf<[0, 0, 0], 2>; // expected to be -1
```
需要用一个辅助变量存储命中下标,递归的方式一个个判断是否匹配:
```ts
type IndexOf<T, U, Index extends any[] = []> =
T extends [infer F, ...infer R]
? F extends U
? Index['length']
: IndexOf<R, U, [...Index, 0]>
: -1
```
但没有通过测试用例 `IndexOf<[string, 1, number, 'a'], number>`,原因是 `1 extends number` 结果为真,所以我们要换成 `Equal` 函数判断相等:
```ts
// 本题答案
type IndexOf<T, U, Index extends any[] = []> =
T extends [infer F, ...infer R]
? Equal<F, U> extends true
? Index['length']
: IndexOf<R, U, [...Index, 0]>
: -1
```
### [Join](https://github.com/type-challenges/type-challenges/blob/main/questions/05310-medium-join/README.md)
实现 TS 版 `Join<T, P>`:
```ts
type Res = Join<["a", "p", "p", "l", "e"], "-">; // expected to be 'a-p-p-l-e'
type Res1 = Join<["Hello", "World"], " ">; // expected to be 'Hello World'
type Res2 = Join<["2", "2", "2"], 1>; // expected to be '21212'
type Res3 = Join<["o"], "u">; // expected to be 'o'
```
递归 `T` 每次拿第一个元素,再使用一个辅助字符串存储答案,拼接起来即可:
```ts
// 本题答案
type Join<T, U extends string | number> =
T extends [infer F extends string, ...infer R extends string[]]
? R['length'] extends 0
? F
: `${F}${U}${Join<R, U>}`
: ''
```
唯一要注意的是处理到最后一项时,不要再追加 `U` 了,可以通过 `R['length'] extends 0` 来判断。
### [LastIndexOf](https://github.com/type-challenges/type-challenges/blob/main/questions/05317-medium-lastindexof/README.md)
实现 `LastIndexOf` 寻找最后一个匹配的下标:
```ts
type Res1 = LastIndexOf<[1, 2, 3, 2, 1], 2> // 3
type Res2 = LastIndexOf<[0, 0, 0], 2> // -1
```
和 `IndexOf` 类似,从最后一个下标往前判断即可。需要注意的是,我们无法用常规办法把 `Index` 下标减一,但好在 `R` 数组长度可以代替当前下标:
```ts
// 本题答案
type LastIndexOf<T, U> = T extends [...infer R, infer L]
? Equal<L, U> extends true
? R['length']
: LastIndexOf<R, U>
: -1
```
## 总结
本周六道题都没有刷到新知识点,中等难题还剩 6 道,如果学到这里能有种索然无味的感觉,说明前面学习的很扎实。
> 讨论地址是:[精读《Trim Right, Without, Trunc...》· Issue #433 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/433)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/252.精读《Unique, MapTypes, Construct Tuple...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 63~68 题。
## 精读
### [Unique](https://github.com/type-challenges/type-challenges/blob/main/questions/05360-medium-unique/README.md)
实现 `Unique<T>`,对 `T` 去重:
```ts
type Res = Unique<[1, 1, 2, 2, 3, 3]> // expected to be [1, 2, 3]
type Res1 = Unique<[1, 2, 3, 4, 4, 5, 6, 7]> // expected to be [1, 2, 3, 4, 5, 6, 7]
type Res2 = Unique<[1, 'a', 2, 'b', 2, 'a']> // expected to be [1, "a", 2, "b"]
type Res3 = Unique<[string, number, 1, 'a', 1, string, 2, 'b', 2, number]> // expected to be [string, number, 1, "a", 2, "b"]
type Res4 = Unique<[unknown, unknown, any, any, never, never]> // expected to be [unknown, any, never]
```
去重需要不断递归产生去重后结果,因此需要一个辅助变量 `R` 配合,并把 `T` 用 `infer` 逐一拆解,判断第一个字符是否在结果数组里,如果不在就塞进去:
```ts
type Unique<T, R extends any[] = []> = T extends [infer F, ...infer Rest]
? Includes<R, F> extends true
? Unique<Rest, R>
: Unique<Rest, [...R, F]>
: R
```
那么剩下的问题就是,如何判断一个对象是否出现在数组中,使用递归可以轻松完成:
```ts
type Includes<Arr, Value> = Arr extends [infer F, ...infer Rest]
? Equal<F, Value> extends true
? true
: Includes<Rest, Value>
: false
```
每次取首项,如果等于 `Value` 直接返回 `true`,否则继续递归,如果数组递归结束(不构成 `Arr extends [xxx]` 的形式)说明递归完了还没有找到相等值,直接返回 `false`。
把这两个函数组合一下就能轻松解决本题:
```ts
// 本题答案
type Unique<T, R extends any[] = []> = T extends [infer F, ...infer Rest]
? Includes<R, F> extends true
? Unique<Rest, R>
: Unique<Rest, [...R, F]>
: R
type Includes<Arr, Value> = Arr extends [infer F, ...infer Rest]
? Equal<F, Value> extends true
? true
: Includes<Rest, Value>
: false
```
### [MapTypes](https://github.com/type-challenges/type-challenges/blob/main/questions/05821-medium-maptypes/README.md)
实现 `MapTypes<T, R>`,根据对象 `R` 的描述来替换类型:
```ts
type StringToNumber = {
mapFrom: string; // value of key which value is string
mapTo: number; // will be transformed for number
}
MapTypes<{iWillBeANumberOneDay: string}, StringToNumber> // gives { iWillBeANumberOneDay: number; }
```
因为要返回一个新对象,所以我们使用 `{ [K in keyof T]: ... }` 的形式描述结果对象。然后就要对 Value 类型进行判断了,为了防止 `never` 的作用,我们包一层数组进行判断:
```ts
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
[K in keyof T]: [T[K]] extends [R['mapFrom']] ? R['mapTo'] : T[K]
}
```
但这个解答还有一个 case 无法通过:
```ts
MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; }
```
我们需要考虑到 Union 分发机制以及每次都要重新匹配一次是否命中 `mapFrom`,因此需要抽一个函数:
```ts
type Transform<R extends { mapFrom: any; mapTo: any }, T> = R extends any
? T extends R['mapFrom']
? R['mapTo']
: never
: never
```
为什么要 `R extends any` 看似无意义的写法呢?原因是 `R` 是联合类型,这样可以触发分发机制,让每一个类型独立判断。所以最终答案就是:
```ts
// 本题答案
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
[K in keyof T]: [T[K]] extends [R['mapFrom']] ? Transform<R, T[K]> : T[K]
}
type Transform<R extends { mapFrom: any; mapTo: any }, T> = R extends any
? T extends R['mapFrom']
? R['mapTo']
: never
: never
```
### [Construct Tuple](https://github.com/type-challenges/type-challenges/blob/main/questions/07544-medium-construct-tuple/README.md)
生成指定长度的 Tuple:
```ts
type result = ConstructTuple<2> // expect to be [unknown, unkonwn]
```
比较容易想到的办法是利用下标递归:
```ts
type ConstructTuple<
L extends number,
I extends number[] = []
> = I['length'] extends L ? [] : [unknown, ...ConstructTuple<L, [1, ...I]>]
```
但在如下测试用例会遇到递归长度过深的问题:
```ts
ConstructTuple<999> // Type instantiation is excessively deep and possibly infinite
```
一种解法是利用 [minusOne](https://github.com/ascoders/weekly/blob/master/TS%20%E7%B1%BB%E5%9E%8B%E4%BD%93%E6%93%8D/248.%E7%B2%BE%E8%AF%BB%E3%80%8AMinusOne%2C%20PickByType%2C%20StartsWith...%E3%80%8B.md#minusone) 提到的 `CountTo` 方法快捷生成指定长度数组,把 `1` 替换为 `unknown` 即可:
```ts
// 本题答案
type ConstructTuple<L extends number> = CountTo<`${L}`>
type CountTo<
T extends string,
Count extends unknown[] = []
> = T extends `${infer First}${infer Rest}`
? CountTo<Rest, N<Count>[keyof N & First]>
: Count
type N<T extends unknown[] = []> = {
'0': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
'1': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, unknown]
'2': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown
]
'3': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown
]
'4': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown
]
'5': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown,
unknown
]
'6': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown
]
'7': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown
]
'8': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown
]
'9': [
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
...T,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown
]
}
```
### [Number Range](https://github.com/type-challenges/type-challenges/blob/main/questions/08640-medium-number-range/README.md)
实现 `NumberRange<T, P>`,生成数字为从 `T` 到 `P` 的联合类型:
```ts
type result = NumberRange<2, 9> // | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
```
以 `NumberRange<2, 9>` 为例,我们需要实现 `2` 到 `9` 的递增递归,因此需要一个数组长度从 `2` 递增到 `9` 的辅助变量 `U`,以及一个存储结果的辅助变量 `R`:
```ts
type NumberRange<T, P, U extends any[] = 长度为 T 的数组, R>
```
所以我们先实现 `LengthTo` 函数,传入长度 `N`,返回一个长度为 `N` 的数组:
```ts
type LengthTo<N extends number, R extends any[] = []> =
R['length'] extends N ? R : LengthTo<N, [0, ...R]>
```
然后就是递归了:
```ts
// 本题答案
type NumberRange<T extends number, P extends number, U extends any[] = LengthTo<T>, R extends number = never> =
U['length'] extends P ? (
R | U['length']
) : (
NumberRange<T, P, [0, ...U], R | U['length']>
)
```
`R` 的默认值为 `never` 非常重要,否则默认值为 `any`,最终类型就会被放大为 `any`。
### [Combination](https://github.com/type-challenges/type-challenges/blob/main/questions/08767-medium-combination/README.md)
实现 `Combination<T>`:
```ts
// expected to be `"foo" | "bar" | "baz" | "foo bar" | "foo bar baz" | "foo baz" | "foo baz bar" | "bar foo" | "bar foo baz" | "bar baz" | "bar baz foo" | "baz foo" | "baz foo bar" | "baz bar" | "baz bar foo"`
type Keys = Combination<['foo', 'bar', 'baz']>
```
本题和 `AllCombination` 类似:
```ts
type AllCombinations_ABC = AllCombinations<'ABC'>
// should be '' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'
```
还记得这题吗?我们要将字符串变成联合类型:
```ts
type StrToUnion<S> = S extends `${infer F}${infer R}`
? F | StrToUnion<R>
: never
```
而本题 `Combination` 更简单,把数组转换为联合类型只需要 `T[number]`。所以本题第一种组合解法是,将 `AllCombinations` 稍微改造下,再利用 `Exclude` 和 `TrimRight` 删除多余的空格:
```ts
// 本题答案
type AllCombinations<T extends string[], U extends string = T[number]> = [
U
] extends [never]
? ''
: '' | { [K in U]: `${K} ${AllCombinations<never, Exclude<U, K>>}` }[U]
type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T
type Combination<T extends string[]> = TrimRight<Exclude<AllCombinations<T>, ''>>
```
还有一种非常精彩的答案在此分析一下:
```ts
// 本题答案
type Combination<T extends string[], U = T[number], A = U> = U extends infer U extends string
? `${U} ${Combination<T, Exclude<A, U>>}` | U
: never;
```
依然利用 `T[number]` 的特性将数组转成联合类型,再利用联合类型 `extends` 会分组的特性递归出结果。
之所以不会出现结尾出现多余的空格,是因为 `U extends infer U extends string` 这段判断已经杜绝了 `U` 消耗完的情况,如果消耗完会及时返回 `never`,所以无需用 `TrimRight` 处理右侧多余的空格。
至于为什么要定义 `A = U`,在前面章节已经介绍过了,因为联合类型 `extends` 过程中会进行分组,此时访问的 `U` 已经是具体类型了,但此时访问 `A` 还是原始的联合类型 `U`。
### [Subsequence](https://github.com/type-challenges/type-challenges/blob/main/questions/08987-medium-subsequence/README.md)
实现 `Subsequence<T>` 输出所有可能的子序列:
```ts
type A = Subsequence<[1, 2]> // [] | [1] | [2] | [1, 2]
```
因为是返回数组的全排列,只要每次取第一项,与剩余项的递归构造出结果,`|` 上剩余项本身递归的结果就可以了:
```ts
// 本题答案
type Subsequence<T extends number[]> = T extends [infer F, ...infer R extends number[]] ? (
Subsequence<R> | [F, ...Subsequence<R>]
) : T
```
## 总结
对全排列问题有两种经典解法:
- 利用辅助变量方式递归,注意联合类型与字符串、数组之间转换的技巧。
- 直接递归,不借助辅助变量,一般在题目返回类型容易构造时选择。
> 讨论地址是:[精读《Unique, MapTypes, Construct Tuple...》· Issue #434 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/434)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: helper.js
================================================
/**
* 发布辅助脚本
* @author 黄子毅
*/
const fs = require("fs");
const dirs = [
"前沿技术",
"TS 类型体操",
"设计模式",
"编译原理",
"源码解读",
"商业思考",
"算法",
"可视化搭建",
"SQL",
"机器学习",
"数学之美",
"生活",
];
dirs.forEach((dir) => {
const readDir = fs.readdirSync(`./${dir}`);
console.log(`### ${dir}\n`);
readDir
.sort((left, right) => left.split(".")[0] - right.split(".")[0])
.forEach((dirName) => {
console.log(
`- <a href="./${dir}/${encodeURIComponent(dirName)}">${dirName.replace(
".md",
""
)}</a>`
);
});
console.log("");
});
================================================
FILE: package.json
================================================
{
"name": "weekly",
"version": "1.0.0",
"description": "前端界的好文精读,每周更新!",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dt-fe/weekly.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/dt-fe/weekly/issues"
},
"homepage": "https://github.com/dt-fe/weekly#readme",
"dependencies": {
"esm": "^3.2.25",
"husky": "^3.0.4",
"lint-md-cli": "^0.1.1"
},
"husky": {
"hooks": {
"pre-commit": "npx lint-md ./"
}
}
}
================================================
FILE: readme.md
================================================
# 前端精读
<a href="https://travis-ci.org/ascoders/weekly">
<img src="https://travis-ci.org/ascoders/weekly.svg?branch=v2" alt="CircleCI Status">
</a>
前端界的好文精读,每周更新!
最新精读:<a href="./数学之美/296.%E6%89%8B%E5%8A%A8%E7%AE%97%E6%A0%B9%E5%8F%B7.md">296.手动算根号</a>
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
现已涵盖:
- 结合大厂工作经验解读的 [前沿技术](./前沿技术),[源码解读](./源码解读)。
- 逐渐加入一些后端技术解读。
- 一些 [商业思考](./商业思考)。
- 已完成 [编译原理](./编译原理)、[设计模式](./设计模式) 两大基础模块。
### 前沿技术
- <a href="./前沿技术/1.%E7%B2%BE%E8%AF%BB%E3%80%8Ajs%20%E6%A8%A1%E5%9D%97%E5%8C%96%E5%8F%91%E5%B1%95%E3%80%8B.md">1.精读《js 模块化发展》</a>
- <a href="./前沿技术/2.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%A8%A1%E6%80%81%E6%A1%86%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">2.精读《模态框的最佳实践》</a>
- <a href="./前沿技术/3.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E5%90%8E%E7%AB%AF%E6%B8%B2%E6%9F%93%E4%B9%8B%E4%BA%89%E3%80%8B.md">3.精读《前后端渲染之争》</a>
- <a href="./前沿技术/4.%E7%B2%BE%E8%AF%BB%E3%80%8AAsyncAwait%20%E4%BC%98%E8%B6%8A%E4%B9%8B%E5%A4%84%E3%80%8B.md">4.精读《AsyncAwait 优越之处》</a>
- <a href="./前沿技术/5.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B0%91%E5%B7%A5%E5%8F%94%E5%8D%95%E9%A1%B5%E6%95%B0%E6%8D%AE%E6%B5%81%E6%96%B9%E6%A1%88%E3%80%8B.md">5.精读《民工叔单页数据流方案》</a>
- <a href="./前沿技术/6.%E7%B2%BE%E8%AF%BB%E3%80%8AJavaScript%20%E9%94%99%E8%AF%AF%E5%A0%86%E6%A0%88%E5%A4%84%E7%90%86%E3%80%8B.md">6.精读《JavaScript 错误堆栈处理》</a>
- <a href="./前沿技术/7.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AF%B7%E5%81%9C%E6%AD%A2%20css-in-js%20%E7%9A%84%E8%A1%8C%E4%B8%BA%E3%80%8B.md">7.精读《请停止 css-in-js 的行为》</a>
- <a href="./前沿技术/8.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A5%E5%9D%91%20React%20%E5%89%8D%E6%B2%A1%E6%9C%89%E4%BA%BA%E4%BC%9A%E5%91%8A%E8%AF%89%E4%BD%A0%E7%9A%84%E4%BA%8B%E3%80%8B.md">8.精读《入坑 React 前没有人会告诉你的事》</a>
- <a href="./前沿技术/9.%E7%B2%BE%E8%AF%BB%E3%80%8AImmutable%20%E7%BB%93%E6%9E%84%E5%85%B1%E4%BA%AB%E3%80%8B.md">9.精读《Immutable 结构共享》</a>
- <a href="./前沿技术/10.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20Components%20%E7%9A%84%E5%9B%B0%E5%A2%83%E3%80%8B.md">10.精读《Web Components 的困境》</a>
- <a href="./前沿技术/11.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7%E3%80%8B.md">11.精读《前端调试技巧》</a>
- <a href="./前沿技术/12.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">12.精读《React 高阶组件》</a>
- <a href="./前沿技术/13.%E7%B2%BE%E8%AF%BB%E3%80%8AThis%20%E5%B8%A6%E6%9D%A5%E7%9A%84%E5%9B%B0%E6%83%91%E3%80%8B.md">13.精读《This 带来的困惑》</a>
- <a href="./前沿技术/14.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B9%8B%20DCI%E3%80%8B.md">14.精读《架构设计之 DCI》</a>
- <a href="./前沿技术/15.%E7%B2%BE%E8%AF%BB%E3%80%8ATC39%20%E4%B8%8E%20ECMAScript%20%E6%8F%90%E6%A1%88%E3%80%8B.md">15.精读《TC39 与 ECMAScript 提案》</a>
- <a href="./前沿技术/16.%E7%B2%BE%E8%AF%BB%E3%80%8ACSS%20Animations%20vs%20Web%20Animations%20API%E3%80%8B.md">16.精读《CSS Animations vs Web Animations API》</a>
- <a href="./前沿技术/17.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%AE%89%E5%85%A8%E5%9C%B0%E4%BD%BF%E7%94%A8%20React%20context%E3%80%8B.md">17.精读《如何安全地使用 React context》</a>
- <a href="./前沿技术/18.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E5%AE%8C%E7%BE%8E%E7%9A%84%E6%97%A5%E6%9C%9F%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">18.精读《设计完美的日期选择器》</a>
- <a href="./前沿技术/19.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9C%80%E4%BD%B3%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98%E3%80%8B%E5%8F%8A%E9%9D%A2%E8%AF%95%E5%AE%98%E6%8A%80%E5%B7%A7.md">19.精读《最佳前端面试题》及面试官技巧</a>
- <a href="./前沿技术/20.%E7%B2%BE%E8%AF%BB%E3%80%8ANestjs%E3%80%8B%E6%96%87%E6%A1%A3.md">20.精读《Nestjs》文档</a>
- <a href="./前沿技术/21.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20fonts%3A%20when%20you%20need%20them%2C%20when%20you%20don%E2%80%99t%E3%80%8B.md">21.精读《Web fonts: when you need them, when you don’t》</a>
- <a href="./前沿技术/22.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%E7%89%B9%E6%80%A7%E5%B8%A6%E6%9D%A5%E7%9A%84%E7%9A%84%20JS%20%E6%80%A7%E8%83%BD%E5%8F%98%E5%8C%96%E3%80%8B.md">22.精读《V8 引擎特性带来的的 JS 性能变化》</a>
- <a href="./前沿技术/23.%E7%B2%BE%E8%AF%BB%E3%80%8AAPI%20%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%E3%80%8B.md">23.精读《API 设计原则》</a>
- <a href="./前沿技术/24.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20JavaScript%20%E6%A6%82%E8%A7%88%E3%80%8B.md">24.精读《现代 JavaScript 概览》</a>
- <a href="./前沿技术/25.%E7%B2%BE%E8%AF%BB%E3%80%8Anull%20%3E%3D%200%3F%E3%80%8B.md">25.精读《null >= 0?》</a>
- <a href="./前沿技术/26.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8A%A0%E5%AF%86%E5%AA%92%E4%BD%93%E6%89%A9%E5%B1%95%E3%80%8B.md">26.精读《加密媒体扩展》</a>
- <a href="./前沿技术/27.%E7%B2%BE%E8%AF%BB%E3%80%8Acss-in-js%20%E6%9D%80%E9%B8%A1%E7%94%A8%E7%89%9B%E5%88%80%E3%80%8B.md">27.精读《css-in-js 杀鸡用牛刀》</a>
- <a href="./前沿技术/28.%E7%B2%BE%E8%AF%BB%E3%80%8A2017%20%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%A4%87%E5%BF%98%E5%BD%95%E3%80%8B.md">28.精读《2017 前端性能优化备忘录》</a>
- <a href="./前沿技术/29.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E4%B8%AD%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E3%80%8B.md">29.精读《JS 中的内存管理》</a>
- <a href="./前沿技术/30.%E7%B2%BE%E8%AF%BB%E3%80%8AJavascript%20%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E4%B8%8E%E5%BC%82%E6%AD%A5%E3%80%8B.md">30.精读《Javascript 事件循环与异步》</a>
- <a href="./前沿技术/31.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E4%B8%8D%E5%86%8D%E4%BD%BF%E7%94%A8%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">31.精读《我不再使用高阶组件》</a>
- <a href="./前沿技术/32.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router4.0%20%E8%BF%9B%E9%98%B6%E6%A6%82%E5%BF%B5%E3%80%8B.md">32.精读《React Router4.0 进阶概念》</a>
- <a href="./前沿技术/33.%E7%B2%BE%E8%AF%BB%E3%80%8A30%20%E8%A1%8C%20js%20%E4%BB%A3%E7%A0%81%E5%88%9B%E5%BB%BA%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E3%80%8B.md">33.精读《30 行 js 代码创建神经网络》</a>
- <a href="./前沿技术/34.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93%E3%80%8B.md">34.精读《React 代码整洁之道》</a>
- <a href="./前沿技术/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md">35.精读《dob - 框架实现》</a>
- <a href="./前沿技术/36.%E7%B2%BE%E8%AF%BB%E3%80%8AWhen%20You%20%E2%80%9CGit%E2%80%9D%20in%20Trouble-%20a%20Version%20Control%20Story%E3%80%8B.md">36.精读《When You “Git” in Trouble- a Version Control Story》</a>
- <a href="./前沿技术/37.%E7%B2%BE%E8%AF%BB%E3%80%8Ahow%20we%20position%20and%20what%20we%20compare%E3%80%8B.md">37.精读《how we position and what we compare》</a>
- <a href="./前沿技术/38.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E4%BD%BF%E7%94%A8%E3%80%8B.md">38.精读《dob - 框架使用》</a>
- <a href="./前沿技术/39.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A8%E9%93%BE%E8%B7%AF%E4%BD%93%E9%AA%8C%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8C%96%E7%9F%BF%E3%80%8B.md">39.精读《全链路体验浏览器挖矿》</a>
- <a href="./前沿技术/40.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%9D%E6%8E%A2%20Reason%20%E4%B8%8E%20GraphQL%E3%80%8B.md">40.精读《初探 Reason 与 GraphQL》</a>
- <a href="./前沿技术/41.%E7%B2%BE%E8%AF%BB%E3%80%8AAnt%20Design%203.0%20%E8%83%8C%E5%90%8E%E7%9A%84%E6%95%85%E4%BA%8B%E3%80%8B.md">41.精读《Ant Design 3.0 背后的故事》</a>
- <a href="./前沿技术/42.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81%E5%93%B2%E5%AD%A6%E3%80%8B.md">42.精读《前端数据流哲学》</a>
- <a href="./前沿技术/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md">43.精读《增强现实与可视化》</a>
- <a href="./前沿技术/44.%E7%B2%BE%E8%AF%BB%E3%80%8ARekit%20Studio%E3%80%8B.md">44.精读《Rekit Studio》</a>
- <a href="./前沿技术/45.%E7%B2%BE%E8%AF%BB%E3%80%8AReact's%20new%20Context%20API%E3%80%8B.md">45.精读《React's new Context API》</a>
- <a href="./前沿技术/46.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-rxjs%E3%80%8B.md">46.精读《react-rxjs》</a>
- <a href="./前沿技术/47.%E7%B2%BE%E8%AF%BB%E3%80%8Awebpack4.0%20%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97%E3%80%8B.md">47.精读《webpack4.0 升级指南》</a>
- <a href="./前沿技术/49.%E7%B2%BE%E8%AF%BB%E3%80%8ACompilers%20are%20the%20New%20Frameworks%E3%80%8B.md">49.精读《Compilers are the New Frameworks》</a>
- <a href="./前沿技术/50.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E6%9E%84%E5%BB%BA%20ARKit%20%E5%BA%94%E7%94%A8%E3%80%8B.md">50.精读《快速上手构建 ARKit 应用》</a>
- <a href="./前沿技术/51.%E7%B2%BE%E8%AF%BB%E3%80%8AElements%20of%20Web%20Dev%E3%80%8B.md">51.精读《Elements of Web Dev》</a>
- <a href="./前沿技术/52.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BE%E8%A7%A3%20ES%20%E6%A8%A1%E5%9D%97%E3%80%8B.md">52.精读《图解 ES 模块》</a>
- <a href="./前沿技术/53.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md">53.精读《插件化思维》</a>
- <a href="./前沿技术/54.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E8%BF%90%E8%A1%8C%20serverRender%E3%80%8B.md">54.精读《在浏览器运行 serverRender》</a>
- <a href="./前沿技术/55.%E7%B2%BE%E8%AF%BB%E3%80%8Aasync%20await%20%E6%98%AF%E6%8A%8A%E5%8F%8C%E5%88%83%E5%89%91%E3%80%8B.md">55.精读《async await 是把双刃剑》</a>
- <a href="./前沿技术/56.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%20Redux%E3%80%8B.md">56.精读《重新思考 Redux》</a>
- <a href="./前沿技术/57.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20js%20%E6%A1%86%E6%9E%B6%E5%AD%98%E5%9C%A8%E7%9A%84%E6%A0%B9%E6%9C%AC%E5%8E%9F%E5%9B%A0%E3%80%8B.md">57.精读《现代 js 框架存在的根本原因》</a>
- <a href="./前沿技术/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md">58.精读《Typescript2.0 - 2.9》</a>
- <a href="./前沿技术/59.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20Nodejs%20%E7%9B%91%E5%90%AC%E6%96%87%E4%BB%B6%E5%A4%B9%E3%80%8B.md">59.精读《如何利用 Nodejs 监听文件夹》</a>
- <a href="./前沿技术/60.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%9C%A8%20nodejs%20%E4%BD%BF%E7%94%A8%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E3%80%8B.md">60.精读《如何在 nodejs 使用环境变量》</a>
- <a href="./前沿技术/61.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E5%85%AB%E7%A7%8D%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93%E3%80%8B.md">61.精读《React 八种条件渲染》</a>
- <a href="./前沿技术/62.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E5%BC%95%E6%93%8E%E5%9F%BA%E7%A1%80%E4%B9%8B%20Shapes%20and%20Inline%20Caches%E3%80%8B.md">62.精读《JS 引擎基础之 Shapes and Inline Caches》</a>
- <a href="./前沿技术/63.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E7%9A%84%E5%A4%9A%E6%80%81%E6%80%A7%E3%80%8B.md">63.精读《React 的多态性》</a>
- <a href="./前沿技术/68.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%A1%A1%E9%87%8F%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E3%80%8B.md">68.精读《衡量用户体验》</a>
- <a href="./前沿技术/69.%E7%B2%BE%E8%AF%BB%E3%80%8ASQL%20vs%20Flux%E3%80%8B.md">69.精读《SQL vs Flux》</a>
- <a href="./前沿技术/72.%E7%B2%BE%E8%AF%BB%E3%80%8AREST%2C%20GraphQL%2C%20Webhooks%2C%20%26%20gRPC%20%E5%A6%82%E4%BD%95%E9%80%89%E5%9E%8B%E3%80%8B.md">72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》</a>
- <a href="./前沿技术/74.%E7%B2%BE%E8%AF%BB%E3%80%8A12%20%E4%B8%AA%E8%AF%84%E4%BC%B0%20JS%20%E5%BA%93%E4%BD%A0%E9%9C%80%E8%A6%81%E5%85%B3%E5%BF%83%E7%9A%84%E4%BA%8B%E3%80%8B.md">74.精读《12 个评估 JS 库你需要关心的事》</a>
- <a href="./前沿技术/76.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%B0%88%E8%B0%88%20Web%20Workers%E3%80%8B.md">76.精读《谈谈 Web Workers》</a>
- <a href="./前沿技术/77.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Reduce%20%E5%AE%9E%E7%8E%B0%20Promise%20%E4%B8%B2%E8%A1%8C%E6%89%A7%E8%A1%8C%E3%80%8B.md">77.精读《用 Reduce 实现 Promise 串行执行》</a>
- <a href="./前沿技术/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md">79.精读《React Hooks》</a>
- <a href="./前沿技术/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md">80.精读《怎么用 React Hooks 造轮子》</a>
- <a href="./前沿技术/81.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20CSS%20%E5%B1%9E%E6%80%A7%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">81.精读《使用 CSS 属性选择器》</a>
- <a href="./前沿技术/83.%E7%B2%BE%E8%AF%BB%E3%80%8AReact16%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">83.精读《React16 新特性》</a>
- <a href="./前沿技术/84.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">84.精读《Typescript 3.2 新特性》</a>
- <a href="./前沿技术/86.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BD%E9%99%85%E5%8C%96%E5%B8%83%E5%B1%80%20-%20Logical%20Properties%E3%80%8B.md">86.精读《国际化布局 - Logical Properties》</a>
- <a href="./前沿技术/87.%E7%B2%BE%E8%AF%BB%E3%80%8AsetState%20%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">87.精读《setState 做了什么》</a>
- <a href="./前沿技术/88.%E7%B2%BE%E8%AF%BB%E3%80%8ACaches%20API%E3%80%8B.md">88.精读《Caches API》</a>
- <a href="./前沿技术/89.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E7%BC%96%E8%AF%91%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE%E4%B8%8E%E7%BB%84%E4%BB%B6%E3%80%8B.md">89.精读《如何编译前端项目与组件》</a>
- <a href="./前沿技术/91.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E5%88%99%20ES2018%E3%80%8B.md">91.精读《正则 ES2018》</a>
- <a href="./前沿技术/94.%E7%B2%BE%E8%AF%BB%E3%80%8AServerless%20%E7%BB%99%E5%89%8D%E7%AB%AF%E5%B8%A6%E6%9D%A5%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">94.精读《Serverless 给前端带来了什么》</a>
- <a href="./前沿技术/95.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20VS%20Class%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">95.精读《Function VS Class 组件》</a>
- <a href="./前沿技术/96.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md">96.精读《useEffect 完全指南》</a>
- <a href="./前沿技术/97.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BC%96%E5%86%99%E6%9C%89%E5%BC%B9%E6%80%A7%E7%9A%84%E7%BB%84%E4%BB%B6%E3%80%8B.md">97.精读《编写有弹性的组件》</a>
- <a href="./前沿技术/99.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md">99.精读《Scheduling in React》</a>
- <a href="./前沿技术/100.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%20Lazy%20Parsing%E3%80%8B.md">100.精读《V8 引擎 Lazy Parsing》</a>
- <a href="./前沿技术/101.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90%20vs%20%E6%8C%81%E7%BB%AD%E4%BA%A4%E4%BB%98%20vs%20%E6%8C%81%E7%BB%AD%E9%83%A8%E7%BD%B2%E3%80%8B.md">101.精读《持续集成 vs 持续交付 vs 持续部署》</a>
- <a href="./前沿技术/102.%E7%B2%BE%E8%AF%BB%E3%80%8AMonorepo%20%E7%9A%84%E4%BC%98%E5%8A%BF%E3%80%8B.md">102.精读《Monorepo 的优势》</a>
- <a href="./前沿技术/104.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20Component%20%E5%85%A5%E9%97%A8%E3%80%8B.md">104.精读《Function Component 入门》</a>
- <a href="./前沿技术/105.%E7%B2%BE%E8%AF%BB%E3%80%8AWhat's%20new%20in%20javascript%E3%80%8B.md">105.精读《What's new in javascript》</a>
- <a href="./前沿技术/107.%E7%B2%BE%E8%AF%BB%E3%80%8AOptional%20chaining%E3%80%8B.md">107.精读《Optional chaining》</a>
- <a href="./前沿技术/109.%E7%B2%BE%E8%AF%BB%E3%80%8AVue3.0%20Function%20API%E3%80%8B.md">109.精读《Vue3.0 Function API》</a>
- <a href="./前沿技术/111.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%E3%80%8B.md">111.精读《前端未来展望》</a>
- <a href="./前沿技术/112.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E3%80%8B.md">112.精读《源码学习》</a>
- <a href="./前沿技术/113.%E7%B2%BE%E8%AF%BB%E3%80%8ANodejs%20V12%E3%80%8B.md">113.精读《Nodejs V12》</a>
- <a href="./前沿技术/117.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E6%8E%A2%E7%B4%A2%E5%BC%8F%E6%A8%A1%E5%9E%8B%E3%80%8B.md">117.精读《Tableau 探索式模型》</a>
- <a href="./前沿技术/118.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20css%20%E5%8F%98%E9%87%8F%E7%94%9F%E6%88%90%E9%A2%9C%E8%89%B2%E4%B8%BB%E9%A2%98%E3%80%8B.md">118.精读《使用 css 变量生成颜色主题》</a>
- <a href="./前沿技术/119.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%B7%B1%E6%B0%B4%E5%8C%BA%E3%80%8B.md">119.精读《前端深水区》</a>
- <a href="./前沿技术/120.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">120.精读《React Hooks 最佳实践》</a>
- <a href="./前沿技术/121.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E4%B8%8E%20BI%E3%80%8B.md">121.精读《前端与 BI》</a>
- <a href="./前沿技术/123.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Babel%20%E5%88%9B%E9%80%A0%E8%87%AA%E5%AE%9A%E4%B9%89%20JS%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">123.精读《用 Babel 创造自定义 JS 语法》</a>
- <a href="./前沿技术/124.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20css%20grid%20%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%E5%B8%83%E5%B1%80%E3%80%8B.md">124.精读《用 css grid 重新思考布局》</a>
- <a href="./前沿技术/125.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%20-%20%E5%87%BD%E6%95%B0%E5%BC%8F%E4%B9%8B%E7%BE%8E%E3%80%8B.md">125.精读《深度学习 - 函数式之美》</a>
- <a href="./前沿技术/126.%E7%B2%BE%E8%AF%BB%E3%80%8ANuxtjs%E3%80%8B.md">126.精读《Nuxtjs》</a>
- <a href="./前沿技术/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md">127.精读《React Conf 2019 - Day1》</a>
- <a href="./前沿技术/129.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day2%E3%80%8B.md">129.精读《React Conf 2019 - Day2》</a>
- <a href="./前沿技术/132.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E4%BA%A4%E7%9A%84%20React%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">132.精读《正交的 React 组件》</a>
- <a href="./前沿技术/133.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%BB%E6%89%BE%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%B9%B3%E8%A1%A1%E7%82%B9%E3%80%8B.md">133.精读《寻找框架设计的平衡点》</a>
- <a href="./前沿技术/134.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E5%9C%A8%E9%98%BF%E9%87%8C%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%8F%B0%E5%A4%A7%E5%89%8D%E7%AB%AF%E3%80%8B.md">134.精读《我在阿里数据中台大前端》</a>
- <a href="./前沿技术/138.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%B2%BE%E9%80%9A%20console.log%E3%80%8B.md">138.精读《精通 console.log》</a>
- <a href="./前沿技术/139.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20JSON%20Parser%E3%80%8B.md">139.精读《手写 JSON Parser》</a>
- <a href="./前沿技术/140.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BB%93%E5%90%88%20React%20%E4%BD%BF%E7%94%A8%E5%8E%9F%E7%94%9F%20Drag%20Drop%20API%E3%80%8B.md">140.精读《结合 React 使用原生 Drag Drop API》</a>
- <a href="./前沿技术/141.%E7%B2%BE%E8%AF%BB%E3%80%8AuseRef%20%E4%B8%8E%20createRef%20%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">141.精读《useRef 与 createRef 的区别》</a>
- <a href="./前沿技术/142.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%81%9A%E5%A5%BD%20CodeReview%E3%80%8B.md">142.精读《如何做好 CodeReview》</a>
- <a href="./前沿技术/143.%E7%B2%BE%E8%AF%BB%E3%80%8ASuspense%20%E6%94%B9%E5%8F%98%E5%BC%80%E5%8F%91%E6%96%B9%E5%BC%8F%E3%80%8B.md">143.精读《Suspense 改变开发方式》</a>
- <a href="./前沿技术/144.%E7%B2%BE%E8%AF%BB%E3%80%8AWebpack5%20%E6%96%B0%E7%89%B9%E6%80%A7%20-%20%E6%A8%A1%E5%9D%97%E8%81%94%E9%82%A6%E3%80%8B.md">144.精读《Webpack5 新特性 - 模块联邦》</a>
- <a href="./前沿技术/145.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router%20v6%E3%80%8B.md">145.精读《React Router v6》</a>
- <a href="./前沿技术/146.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E3%80%8B.md">146.精读《React Hooks 数据流》</a>
- <a href="./前沿技术/147.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40types%20react%20%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%20TS%20%E6%8A%80%E5%B7%A7%E3%80%8B.md">147. 精读《@types react 值得注意的 TS 技巧》</a>
- <a href="./前沿技术/148.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Error%20Boundaries%E3%80%8B.md">148. 精读《React Error Boundaries》</a>
- <a href="./前沿技术/149.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E6%80%A7%E8%83%BD%E8%B0%83%E8%AF%95%E3%80%8B.md">149. 精读《React 性能调试》</a>
- <a href="./前沿技术/150.%20%E7%B2%BE%E8%AF%BB%E3%80%8ADeno%201.0%20%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E3%80%8B.md">150. 精读《Deno 1.0 你需要了解的》</a>
- <a href="./前沿技术/152.%20%E7%B2%BE%E8%AF%BB%E3%80%8Arecoil%E3%80%8B.md">152. 精读《recoil》</a>
- <a href="./前沿技术/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md">153. 精读《snowpack》</a>
- <a href="./前沿技术/154.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20React%20%E5%81%9A%E6%8C%89%E9%9C%80%E6%B8%B2%E6%9F%93%E3%80%8B.md">154. 精读《用 React 做按需渲染》</a>
- <a href="./前沿技术/157.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E6%AF%94%E8%BE%83%20Object%20%E5%AF%B9%E8%B1%A1%E3%80%8B.md">157. 精读《如何比较 Object 对象》</a>
- <a href="./前沿技术/158.%20%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204%E3%80%8B.md">158. 精读《Typescript 4》</a>
- <a href="./前沿技术/159.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E4%BD%8E%E4%BB%A3%E7%A0%81%E6%90%AD%E5%BB%BA%E7%9A%84%E7%90%86%E8%A7%A3%E3%80%8B.md">159. 精读《对低代码搭建的理解》</a>
- <a href="./前沿技术/160.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%87%BD%E6%95%B0%E7%BC%93%E5%AD%98%E3%80%8B.md">160. 精读《函数缓存》</a>
- <a href="./前沿技术/161.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E6%80%9D%E8%80%83%20-%20%E5%AF%8C%E6%96%87%E6%9C%AC%E6%90%AD%E5%BB%BA%E3%80%8B.md">161.精读《可视化搭建思考 - 富文本搭建》</a>
- <a href="./前沿技术/162.%E7%B2%BE%E8%AF%BB%E3%80%8ATasks%2C%20microtasks%2C%20queues%20and%20schedules%E3%80%8B.md">162.精读《Tasks, microtasks, queues and schedules》</a>
- <a href="./前沿技术/163.%E7%B2%BE%E8%AF%BB%E3%80%8ASpring%20%E6%A6%82%E5%BF%B5%E3%80%8B.md">163.精读《Spring 概念》</a>
- <a href="./前沿技术/164.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E8%AE%BE%E8%AE%A1%E5%99%A8%E3%80%8B.md">164.精读《数据搭建引擎 bi-designer API-设计器》</a>
- <a href="./前沿技术/165.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E7%BB%84%E4%BB%B6%E3%80%8B.md">165.精读《数据搭建引擎 bi-designer API-组件》</a>
- <a href="./前沿技术/166.%E7%B2%BE%E8%AF%BB%E3%80%8ABI%20%E6%90%AD%E5%BB%BA%20-%20%E7%AD%9B%E9%80%89%E6%9D%A1%E4%BB%B6%E3%80%8B.md">166.精读《BI 搭建 - 筛选条件》</a>
- <a href="./前沿技术/190.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3%E3%80%8B.md">190.精读《DOM diff 原理详解》</a>
- <a href="./前沿技术/191.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BD%E8%A1%A8%E6%A0%BC%E3%80%8B.md">191.精读《高性能表格》</a>
- <a href="./前沿技术/192.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E6%9C%80%E9%95%BF%E4%B8%8A%E5%8D%87%E5%AD%90%E5%BA%8F%E5%88%97%E3%80%8B.md">192.精读《DOM diff 最长上升子序列》</a>
- <a href="./前沿技术/193.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Server%20Component%E3%80%8B.md">193.精读《React Server Component》</a>
- <a href="./前沿技术/194.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E3%80%8B.md">194.精读《算法基础数据结构》</a>
- <a href="./前沿技术/195.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%96%B0%E4%B8%80%E4%BB%A3%E5%89%8D%E7%AB%AF%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7%E5%AF%B9%E6%AF%94%E3%80%8B.md">195.精读《新一代前端构建工具对比》</a>
- <a href="./前沿技术/196.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%81%8C%E4%B8%9A%E8%A7%84%E5%88%92%20-%202021%20%E5%B9%B4%E3%80%8B.md">196.精读《前端职业规划 - 2021 年》</a>
- <a href="./前沿技术/197.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%8E%E4%BB%A3%E7%A0%81%E9%80%BB%E8%BE%91%E7%BC%96%E6%8E%92%E3%80%8B.md">197.精读《低代码逻辑编排》</a>
- <a href="./前沿技术/202.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%2018%E3%80%8B.md">202.精读《React 18》</a>
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
- <a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
- <a href="./前沿技术/210.%E7%B2%BE%E8%AF%BB%E3%80%8Aclass%20static%20block%E3%80%8B.md">210.精读《class static block》</a>
- <a href="./前沿技术/211.%E7%B2%BE%E8%AF%BB%E3%80%8AMicrosoft%20Power%20Fx%E3%80%8B.md">211.精读《Microsoft Power Fx》</a>
- <a href="./前沿技术/212.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E7%BB%B4%E6%8A%A4%E6%80%A7%E6%80%9D%E8%80%83%E3%80%8B.md">212.精读《可维护性思考》</a>
- <a href="./前沿技术/213.%E7%B2%BE%E8%AF%BB%E3%80%8APrisma%20%E7%9A%84%E4%BD%BF%E7%94%A8%E3%80%8B.md">213.精读《Prisma 的使用》</a>
- <a href="./前沿技术/214.%E7%B2%BE%E8%AF%BB%E3%80%8Aweb%20streams%E3%80%8B.md">214.精读《web streams》</a>
- <a href="./前沿技术/215.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E3%80%8B.md">215.精读《什么是 LOD 表达式》</a>
- <a href="./前沿技术/216.%E7%B2%BE%E8%AF%BB%E3%80%8A15%20%E5%A4%A7%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%20-%20%E4%B8%8A%E3%80%8B.md">216.精读《15 大 LOD 表达式 - 上》</a>
- <a href="./前沿技术/217.%E7%B2%BE%E8%AF%BB%E3%80%8A15%20%E5%A4%A7%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%20-%20%E4%B8%8B%E3%80%8B.md">217.精读《15 大 LOD 表达式 - 下》</a>
- <a href="./前沿技术/218.%E7%B2%BE%E8%AF%BB%E3%80%8ARust%20%E6%98%AF%20JS%20%E5%9F%BA%E5%BB%BA%E7%9A%84%E6%9C%AA%E6%9D%A5%E3%80%8B.md">218.精读《Rust 是 JS 基建的未来》</a>
- <a href="./前沿技术/219.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%80%E3%80%8B.md">219.精读《深入了解现代浏览器一》</a>
- <a href="./前沿技术/220.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BA%8C%E3%80%8B.md">220.精读《深入了解现代浏览器二》</a>
- <a href="./前沿技术/221.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%89%E3%80%8B.md">221.精读《深入了解现代浏览器三》</a>
- <a href="./前沿技术/222.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E5%9B%9B%E3%80%8B.md">222.精读《深入了解现代浏览器四》</a>
- <a href="./前沿技术/223.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20%E6%8F%90%E6%A1%88%E3%80%8B.md">223.精读《Records & Tuples 提案》</a>
- <a href="./前沿技术/224.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20for%20React%E3%80%8B.md">224.精读《Records & Tuples for React》</a>
- <a href="./前沿技术/225.%E7%B2%BE%E8%AF%BB%E3%80%8AExcel%20JS%20API%E3%80%8B.md">225.精读《Excel JS API》</a>
- <a href="./前沿技术/226.%E7%B2%BE%E8%AF%BB%E3%80%8A2021%20%E5%89%8D%E7%AB%AF%E6%96%B0%E7%A7%80%E5%9B%9E%E9%A1%BE%E3%80%8B.md">226.精读《2021 前端新秀回顾》</a>
- <a href="./前沿技术/228.%E7%B2%BE%E8%AF%BB%E3%80%8Apipe%20operator%20for%20JavaScript%E3%80%8B.md">228.精读《pipe operator for JavaScript》</a>
- <a href="./前沿技术/230.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%20Markdown%20%E7%9A%84%E6%80%9D%E8%80%83%E3%80%8B.md">230.精读《对 Markdown 的思考》</a>
- <a href="./前沿技术/237.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.5-4.6%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">237.精读《Typescript 4.5-4.6 新特性》</a>
- <a href="./前沿技术/238.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%8D%E5%86%8D%E9%9C%80%E8%A6%81%20JS%20%E5%81%9A%E7%9A%84%205%20%E4%BB%B6%E4%BA%8B%E3%80%8B.md">238.精读《不再需要 JS 做的 5 件事》</a>
- <a href="./前沿技术/239.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E6%95%B0%E7%BB%84%E7%9A%84%E5%86%85%E9%83%A8%E5%AE%9E%E7%8E%B0%E3%80%8B.md">239.精读《JS 数组的内部实现》</a>
- <a href="./前沿技术/240.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20useEvent%20RFC%E3%80%8B.md">240.精读《React useEvent RFC》</a>
- <a href="./前沿技术/242.%E7%B2%BE%E8%AF%BB%E3%80%8Aweb%20reflow%E3%80%8B.md">242.精读《web reflow》</a>
- <a href="./前沿技术/253.%E7%B2%BE%E8%AF%BB%E3%80%8Apnpm%E3%80%8B.md">253.精读《pnpm》</a>
- <a href="./前沿技术/254.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E7%9A%84%E7%90%86%E8%A7%A3%20-%20%E5%88%86%E5%B1%82%E4%B8%8E%E6%8A%BD%E8%B1%A1%E3%80%8B.md">254.精读《对前端架构的理解 - 分层与抽象》</a>
- <a href="./前沿技术/255.%E7%B2%BE%E8%AF%BB%E3%80%8ASolidJS%E3%80%8B.md">255.精读《SolidJS》</a>
- <a href="./前沿技术/256.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%AE%80%E4%BB%8B%E3%80%8B.md">256.精读《依赖注入简介》</a>
- <a href="./前沿技术/257.%E7%B2%BE%E8%AF%BB%E3%80%8AState%20of%20CSS%202022%E3%80%8B.md">257.精读《State of CSS 2022》</a>
- <a href="./前沿技术/258.%E7%B2%BE%E8%AF%BB%E3%80%8Aproposal-extractors%E3%80%8B.md">258.精读《proposal-extractors》</a>
- <a href="./前沿技术/259.%E7%B2%BE%E8%AF%BB%E3%80%8AHeadless%20%E7%BB%84%E4%BB%B6%E7%94%A8%E6%B3%95%E4%B8%8E%E5%8E%9F%E7%90%86%E3%80%8B.md">259.精读《Headless 组件用法与原理》</a>
- <a href="./前沿技术/260.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E4%B8%BA%20TS%20%E7%B1%BB%E5%9E%8B%E5%86%99%E5%8D%95%E6%B5%8B%E3%80%8B.md">260.精读《如何为 TS 类型写单测》</a>
- <a href="./前沿技术/261.%E7%B2%BE%E8%AF%BB%E3%80%8ARest%20vs%20Spread%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">261.精读《Rest vs Spread 语法》</a>
- <a href="./前沿技术/262.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%BF%AD%E4%BB%A3%E5%99%A8%20Iterable%E3%80%8B.md">262.精读《迭代器 Iterable》</a>
- <a href="./前沿技术/263.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BD%95%E5%BC%83%E7%94%A8%20css-in-js%E3%80%8B.md">263.精读《我们为何弃用 css-in-js》</a>
- <a href="./前沿技术/264.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BB%B4%E6%8A%A4%E5%A5%BD%E4%B8%80%E4%B8%AA%E5%A4%8D%E6%9D%82%E9%A1%B9%E7%9B%AE%E3%80%8B.md">264.精读《维护好一个复杂项目》</a>
- <a href="./前沿技术/265.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E5%8A%9F%E8%83%BD%E5%88%86%E6%9E%90%E3%80%8B.md">265.精读《磁贴布局 - 功能分析》</a>
- <a href="./前沿技术/266.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0%E3%80%8B.md">266.精读《磁贴布局 - 功能实现》</a>
- <a href="./前沿技术/267.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E3%80%8B.md">267.精读《磁贴布局 - 性能优化》</a>
- <a href="./前沿技术/277.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%A9%E7%94%A8%20GPT%20%E8%A7%A3%E8%AF%BB%20PDF%E3%80%8B.md">277.精读《利用 GPT 解读 PDF》</a>
- <a href="./前沿技术/281.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%87%AA%E7%94%B1%20%2B%20%E7%A3%81%E8%B4%B4%E6%B7%B7%E5%90%88%E5%B8%83%E5%B1%80%E3%80%8B.md">281.精读《自由 + 磁贴混合布局》</a>
- <a href="./前沿技术/282.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%87%AA%E7%94%B1%E5%B8%83%E5%B1%80%E5%90%B8%E9%99%84%E7%BA%BF%E7%9A%84%E5%AE%9E%E7%8E%B0%E3%80%8B.md">282.精读《自由布局吸附线的实现》</a>
- <a href="./前沿技术/287.%E7%B2%BE%E8%AF%BB%E3%80%8AVisActor%20%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B7%A5%E5%85%B7%E3%80%8B.md">287.精读《VisActor 数据可视化工具》</a>
### TS 类型体操
- <a href="./TS 类型体操/243.%E7%B2%BE%E8%AF%BB%E3%80%8APick%2C%20Awaited%2C%20If...%E3%80%8B.md">243.精读《Pick, Awaited, If...》</a>
- <a href="./TS 类型体操/244.%E7%B2%BE%E8%AF%BB%E3%80%8AGet%20return%20type%2C%20Omit%2C%20ReadOnly...%E3%80%8B.md">244.精读《Get return type, Omit, ReadOnly...》</a>
- <a href="./TS 类型体操/245.%E7%B2%BE%E8%AF%BB%E3%80%8APromise.all%2C%20Replace%2C%20Type%20Lookup...%E3%80%8B.md">245.精读《Promise.all, Replace, Type Lookup...》</a>
- <a href="./TS 类型体操/246.%E7%B2%BE%E8%AF%BB%E3%80%8APermutation%2C%20Flatten%2C%20Absolute...%E3%80%8B.md">246.精读《Permutation, Flatten, Absolute...》</a>
- <a href="./TS 类型体操/247.%E7%B2%BE%E8%AF%BB%E3%80%8ADiff%2C%20AnyOf%2C%20IsUnion...%E3%80%8B.md">247.精读《Diff, AnyOf, IsUnion...》</a>
- <a href="./TS 类型体操/248.%E7%B2%BE%E8%AF%BB%E3%80%8AMinusOne%2C%20PickByType%2C%20StartsWith...%E3%80%8B.md">248.精读《MinusOne, PickByType, StartsWith...》</a>
- <a href="./TS 类型体操/249.%E7%B2%BE%E8%AF%BB%E3%80%8AObjectEntries%2C%20Shift%2C%20Reverse...%E3%80%8B.md">249.精读《ObjectEntries, Shift, Reverse...》</a>
- <a href="./TS 类型体操/250.%E7%B2%BE%E8%AF%BB%E3%80%8AFlip%2C%20Fibonacci%2C%20AllCombinations...%E3%80%8B.md">250.精读《Flip, Fibonacci, AllCombinations...》</a>
- <a href="./TS 类型体操/251.%E7%B2%BE%E8%AF%BB%E3%80%8ATrim%20Right%2C%20Without%2C%20Trunc...%E3%80%8B.md">251.精读《Trim Right, Without, Trunc...》</a>
- <a href="./TS 类型体操/252.%E7%B2%BE%E8%AF%BB%E3%80%8AUnique%2C%20MapTypes%2C%20Construct%20Tuple...%E3%80%8B.md">252.精读《Unique, MapTypes, Construct Tuple...》</a>
### 设计模式
- <a href="./设计模式/167.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Abstract%20Factory%20%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E3%80%8B.md">167.精读《设计模式 - Abstract Factory 抽象工厂》</a>
- <a href="./设计模式/168.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Builder%20%E7%94%9F%E6%88%90%E5%99%A8%E3%80%8B.md">168.精读《设计模式 - Builder 生成器》</a>
- <a href="./设计模式/169.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Factory%20Method%20%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E3%80%8B.md">169.精读《设计模式 - Factory Method 工厂方法》</a>
- <a href="./设计模式/170.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Prototype%20%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">170.精读《设计模式 - Prototype 原型模式》</a>
- <a href="./设计模式/171.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Singleton%20%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">171.精读《设计模式 - Singleton 单例模式》</a>
- <a href="./设计模式/172.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Adapter%20%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">172.精读《设计模式 - Adapter 适配器模式》</a>
- <a href="./设计模式/173.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Bridge%20%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">173.精读《设计模式 - Bridge 桥接模式》</a>
- <a href="./设计模式/174.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Composite%20%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">174.精读《设计模式 - Composite 组合模式》</a>
- <a href="./设计模式/175.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Decorator%20%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">175.精读《设计模式 - Decorator 装饰器模式》</a>
- <a href="./设计模式/176.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Facade%20%E5%A4%96%E8%A7%82%E6%A8%A1%E5%BC%8F%E3%80%8B.md">176.精读《设计模式 - Facade 外观模式》</a>
- <a href="./设计模式/177.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Flyweight%20%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F%E3%80%8B.md">177.精读《设计模式 - Flyweight 享元模式》</a>
- <a href="./设计模式/178.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Proxy%20%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E3%80%8B.md">178.精读《设计模式 - Proxy 代理模式》</a>
- <a href="./设计模式/179.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Chain%20of%20Responsibility%20%E8%81%8C%E8%B4%A3%E9%93%BE%E6%A8%A1%E5%BC%8F%E3%80%8B.md">179.精读《设计模式 - Chain of Responsibility 职责链模式》</a>
- <a href="./设计模式/180.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Command%20%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F%E3%80%8B.md">180.精读《设计模式 - Command 命令模式》</a>
- <a href="./设计模式/181.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Interpreter%20%E8%A7%A3%E9%87%8A%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">181.精读《设计模式 - Interpreter 解释器模式》</a>
- <a href="./设计模式/182.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Iterator%20%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">182.精读《设计模式 - Iterator 迭代器模式》</a>
- <a href="./设计模式/183.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Mediator%20%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">183.精读《设计模式 - Mediator 中介者模式》</a>
- <a href="./设计模式/184.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Memoto%20%E5%A4%87%E5%BF%98%E5%BD%95%E6%A8%A1%E5%BC%8F%E3%80%8B.md">184.精读《设计模式 - Memoto 备忘录模式》</a>
- <a href="./设计模式/185.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Observer%20%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">185.精读《设计模式 - Observer 观察者模式》</a>
- <a href="./设计模式/186.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20State%20%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E3%80%8B.md">186.精读《设计模式 - State 状态模式》</a>
- <a href="./设计模式/187.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Strategy%20%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">187.精读《设计模式 - Strategy 策略模式》</a>
- <a href="./设计模式/188.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Template%20Method%20%E6%A8%A1%E7%89%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">188.精读《设计模式 - Template Method 模版模式》</a>
- <a href="./设计模式/189.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Visitor%20%E8%AE%BF%E9%97%AE%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">189.精读《设计模式 - Visitor 访问者模式》</a>
### 编译原理
- <a href="./编译原理/64.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">64.精读《手写 SQL 编译器 - 词法分析》</a>
- <a href="./编译原理/65.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%96%87%E6%B3%95%E4%BB%8B%E7%BB%8D%E3%80%8B.md">65.精读《手写 SQL 编译器 - 文法介绍》</a>
- <a href="./编译原理/66.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">66.精读《手写 SQL 编译器 - 语法分析》</a>
- <a href="./编译原理/67.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md">67.精读《手写 SQL 编译器 - 回溯》</a>
- <a href="./编译原理/70.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E6%A0%91%E3%80%8B.md">70.精读《手写 SQL 编译器 - 语法树》</a>
- <a href="./编译原理/71.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E3%80%8B.md">71.精读《手写 SQL 编译器 - 错误提示》</a>
- <a href="./编译原理/78.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BC%93%E5%AD%98%E3%80%8B.md">78.精读《手写 SQL 编译器 - 性能优化之缓存》</a>
- <a href="./编译原理/85.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md">85.精读《手写 SQL 编译器 - 智能提示》</a>
### 源码解读
- <a href="./源码解读/48.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md">48.精读《Immer.js》源码</a>
- <a href="./源码解读/73.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md">73.精读《sqorn 源码》</a>
- <a href="./源码解读/75.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md">75.精读《Epitath 源码 - renderProps 新用法》</a>
- <a href="./源码解读/82.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md">82.精读《Htm - Hyperscript 源码》</a>
- <a href="./源码解读/92.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md">92.精读《React PowerPlug 源码》</a>
- <a href="./源码解读/93.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md">93.精读《syntax-parser 源码》</a>
- <a href="./源码解读/98.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md">98.精读《react-easy-state 源码》</a>
- <a href="./源码解读/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md">110.精读《Inject Instance 源码》</a>
- <a href="./源码解读/122.%E7%B2%BE%E8%AF%BB%E3%80%8Arobot%20%E6%BA%90%E7%A0%81%20-%20%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA%E3%80%8B.md">122.精读《robot 源码 - 有限状态机》</a>
- <a href="./源码解读/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md">128.精读《Hooks 取数 - swr 源码》</a>
- <a href="./源码解读/130.%E7%B2%BE%E8%AF%BB%E3%80%8Aunstated%20%E4%B8%8E%20unstated-next%20%E6%BA%90%E7%A0%81%E3%80%8B.md">130.精读《unstated 与 unstated-next 源码》</a>
- <a href="./源码解读/151.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40umijs%20use-request%E3%80%8B%E6%BA%90%E7%A0%81.md">151. 精读《@umijs use-request》源码</a>
- <a href="./源码解读/155.%20%E7%B2%BE%E8%AF%BB%E3%80%8Ause-what-changed%20%E6%BA%90%E7%A0%81%E3%80%8B.md">155. 精读《use-what-changed 源码》</a>
- <a href="./源码解读/156.%20%E7%B2%BE%E8%AF%BB%E3%80%8Areact-intersection-observer%20%E6%BA%90%E7%A0%81%E3%80%8B.md">156. 精读《react-intersection-observer 源码》</a>
- <a href="./源码解读/227.%20%E7%B2%BE%E8%AF%BB%E3%80%8Azustand%20%E6%BA%90%E7%A0%81%E3%80%8B.md">227. 精读《zustand 源码》</a>
- <a href="./源码解读/229.%E7%B2%BE%E8%AF%BB%E3%80%8Avue-lit%20%E6%BA%90%E7%A0%81%E3%80%8B.md">229.精读《vue-lit 源码》</a>
- <a href="./源码解读/241.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-snippets%20-%20Router%20%E6%BA%90%E7%A0%81%E3%80%8B.md">241.精读《react-snippets - Router 源码》</a>
### 商业思考
- <a href="./商业思考/90.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%202019%E3%80%8B.md">90.精读《极客公园 2019》</a>
- <a href="./商业思考/103.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%93%E5%AE%B6%E4%B8%8D%E5%86%8D%E5%85%B3%E5%BF%83%E6%8A%80%E6%9C%AF%E7%BB%86%E8%8A%82%E3%80%8B.md">103.精读《为什么专家不再关心技术细节》</a>
- <a href="./商业思考/106.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E4%B9%8B%E4%B8%8A%C2%B7%E6%99%BA%E6%85%A7%E4%B9%8B%E5%85%89%20-%202018%E3%80%8B.md">106.精读《数据之上·智慧之光 - 2018》</a>
- <a href="./商业思考/108.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%99%BA%E8%83%BD%E5%95%86%E4%B8%9A%E3%80%8B.md">108.精读《智能商业》</a>
- <a href="./商业思考/114.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%B0%81%E5%9C%A8%E4%B8%96%E7%95%8C%E4%B8%AD%E5%BF%83%E3%80%8B.md">114.精读《谁在世界中心》</a>
- <a href="./商业思考/115.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E5%85%A5%E9%97%A8%E3%80%8B.md">115.精读《Tableau 入门》</a>
- <a href="./商业思考/116.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%B7%E6%96%B0%E3%80%8B.md">116.精读《刷新》</a>
- <a href="./商业思考/131.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%8E%200%20%E5%88%B0%201%E3%80%8B.md">131.精读《从 0 到 1》</a>
- <a href="./商业思考/135.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%20IFX%20-%20%E4%B8%8A%E3%80%8B.md">135.精读《极客公园 IFX - 上》</a>
- <a href="./商业思考/136.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%20IFX%20-%20%E4%B8%8B%E3%80%8B.md">136.精读《极客公园 IFX - 下》</a>
- <a href="./商业思考/137.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%BD%93%E6%88%91%E5%9C%A8%E5%88%86%E4%BA%AB%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E6%88%91%E5%9C%A8%E5%81%9A%E4%BB%80%E4%B9%88%EF%BC%9F%E3%80%8B.md">137.精读《当我在分享的时候,我在做什么?》</a>
### 算法
- <a href="./算法/198.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E3%80%8B.md">198.精读《算法 - 动态规划》</a>
- <a href="./算法/199.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E3%80%8B.md">199.精读《算法 - 滑动窗口》</a>
- <a href="./算法/200.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md">200.精读《算法 - 回溯》</a>
- <a href="./算法/201.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E4%BA%8C%E5%8F%89%E6%A0%91%E3%80%8B.md">201.精读《算法 - 二叉树》</a>
- <a href="./算法/203.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E3%80%8B.md">203.精读《算法 - 二叉搜索树》</a>
- <a href="./算法/283.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E9%80%9A%E9%85%8D%E7%AC%A6%E5%8C%B9%E9%85%8D%E3%80%8B.md">283.精读《算法题 - 通配符匹配》</a>
- <a href="./算法/284.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E7%BB%9F%E8%AE%A1%E5%8F%AF%E4%BB%A5%E8%A2%AB%20K%20%E6%95%B4%E9%99%A4%E7%9A%84%E4%B8%8B%E6%A0%87%E5%AF%B9%E6%95%B0%E7%9B%AE%E3%80%8B.md">284.精读《算法题 - 统计可以被 K 整除的下标对数目》</a>
- <a href="./算法/285.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2%E3%80%8B.md">285.精读《算法题 - 最小覆盖子串》</a>
- <a href="./算法/286.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E5%9C%B0%E4%B8%8B%E5%9F%8E%E6%B8%B8%E6%88%8F%E3%80%8B.md">286.精读《算法题 - 地下城游戏》</a>
- <a href="./算法/288.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%E3%80%8B.md">288.精读《算法题 - 编辑距离》</a>
- <a href="./算法/289.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E9%A2%98%20-%20%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C%E3%80%8B.md">289.精读《算法题 - 二叉树中的最大路径和》</a>
### 可视化搭建
- <a href="./可视化搭建/268.%E5%A6%82%E4%BD%95%E6%8A%BD%E8%B1%A1%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA.md">268.如何抽象可视化搭建</a>
- <a href="./可视化搭建/269.%E7%BB%84%E4%BB%B6%E6%B3%A8%E5%86%8C%E4%B8%8E%E7%94%BB%E5%B8%83%E6%B8%B2%E6%9F%93.md">269.组件注册与画布渲染</a>
- <a href="./可视化搭建/270.%E7%94%BB%E5%B8%83%E4%B8%8E%E7%BB%84%E4%BB%B6%E5%85%83%E4%BF%A1%E6%81%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md">270.画布与组件元信息数据流</a>
- <a href="./可视化搭建/271.%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E5%86%85%E7%BD%AE%20API.md">271.可视化搭建内置 API</a>
- <a href="./可视化搭建/272.%E5%AE%B9%E5%99%A8%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1.md">272.容器组件设计</a>
- <a href="./可视化搭建/273.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">273.组件值与联动</a>
- <a href="./可视化搭建/274.%E5%AE%9A%E4%B9%89%E8%81%94%E5%8A%A8%E5%8D%8F%E8%AE%AE.md">274.定义联动协议</a>
- <a href="./可视化搭建/275.%E7%BB%84%E4%BB%B6%E5%80%BC%E6%A0%A1%E9%AA%8C.md">275.组件值校验</a>
- <a href="./可视化搭建/276.keepAlive%20%E6%A8%A1%E5%BC%8F.md">276.keepAlive 模式</a>
- <a href="./可视化搭建/278.ComponentLoader%20%E4%B8%8E%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6.md">278.ComponentLoader 与动态组件</a>
- <a href="./可视化搭建/279.%E8%87%AA%E5%8A%A8%E6%89%B9%E5%A4%84%E7%90%86%E4%B8%8E%E5%86%BB%E7%BB%93.md">279.自动批处理与冻结</a>
- <a href="./可视化搭建/280.%E5%9C%BA%E6%99%AF%E5%AE%9E%E6%88%98.md">280.场景实战</a>
### SQL
- <a href="./SQL/231.SQL%20%E5%85%A5%E9%97%A8.md">231.SQL 入门</a>
- <a href="./SQL/232.SQL%20%E8%81%9A%E5%90%88%E6%9F%A5%E8%AF%A2.md">232.SQL 聚合查询</a>
- <a href="./SQL/233.SQL%20%E5%A4%8D%E6%9D%82%E6%9F%A5%E8%AF%A2.md">233.SQL 复杂查询</a>
- <a href="./SQL/234.SQL%20CASE%20%E8%A1%A8%E8%BE%BE%E5%BC%8F.md">234.SQL CASE 表达式</a>
- <a href="./SQL/235.SQL%20%E7%AA%97%E5%8F%A3%E5%87%BD%E6%95%B0.md">235.SQL 窗口函数</a>
- <a href="./SQL/236.SQL%20grouping.md">236.SQL grouping</a>
### 机器学习
- <a href="./机器学习/291.%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AE%80%E4%BB%8B%3A%20%E5%AF%BB%E6%89%BE%E5%87%BD%E6%95%B0%E7%9A%84%E8%89%BA%E6%9C%AF.md">291.机器学习简介: 寻找函数的艺术</a>
- <a href="./机器学习/292.%E4%B8%87%E8%83%BD%E8%BF%91%E4%BC%BC%E5%AE%9A%E7%90%86%3A%20%E9%80%BC%E8%BF%91%E4%BB%BB%E4%BD%95%E5%87%BD%E6%95%B0%E7%9A%84%E7%90%86%E8%AE%BA.md">292.万能近似定理: 逼近任何函数的理论</a>
- <a href="./机器学习/293.%E5%AE%9E%E7%8E%B0%E4%B8%87%E8%83%BD%E8%BF%91%E4%BC%BC%E5%87%BD%E6%95%B0%3A%20%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.md">293.实现万能近似函数: 神经网络的架构设计</a>
- <a href="./机器学习/294.%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%3A%20%E6%8F%AD%E7%A7%98%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E7%9A%84%E5%AD%A6%E4%B9%A0%E6%9C%BA%E5%88%B6.md">294.反向传播: 揭秘神经网络的学习机制</a>
- <a href="./机器学习/295.%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%3A%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83.md">295.完整实现神经网络: 实战演练</a>
### 数学之美
- <a href="./数学之美/296.%E6%89%8B%E5%8A%A8%E7%AE%97%E6%A0%B9%E5%8F%B7.md">296.手动算根号</a>
### 生活
- <a href="./生活/290.%E4%B8%AA%E4%BA%BA%E5%85%BB%E8%80%81%E9%87%91%E5%88%A9%E4%B8%8E%E5%BC%8A.md">290.个人养老金利与弊</a>
## 关注前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
================================================
FILE: 前沿技术/1.精读《js 模块化发展》.md
================================================
这次是前端精读期刊与大家第一次正式碰面,我们每周会精读并分析若干篇精品好文,试图讨论出结论性观点。没错,我们试图通过观点的碰撞,争做无主观精品好文的意见领袖。
我是这一期的主持人 —— [黄子毅](https://github.com/ascoders)
本期精读的文章是:[evolutionOfJsModularity](https://github.com/myshov/history-of-javascript/tree/master/4_evolution_of_js_modularity)。
懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。
# 1 引言
<img src="https://img.alicdn.com/imgextra/i4/O1CN01mvDKCM1owPSsLDBmI_!!6000000005289-2-tps-475-297.png" alt="logo" width="500" />
> 如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了 2 年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
但为什么要了解 Javascript 模块化发展的历史呢?因为凡事都有两面性,了解 Javascript 模块化规范,有利于我们思考出更好的模块化方案,纵观历史,从 1999 年开始,模块化方案最多维持两年,就出现了新的替代方案,比原有的模块化更清晰、强壮,我们不能被现代模块化方式限制住思维,因为现在的 ES2015 模块化方案距离发布也仅仅过了两年。
# 2 内容概要
**直接定义依赖 (1999)**: 由于当时 js 文件非常简单,模块化方式非常简单粗暴 —— 通过全局方法定义、引用模块。这种定义方式与现在的 commonjs 非常神似,区别是 commonjs 以文件作为模块,而这种方法可以在任何文件中定义模块,模块不与文件关联。
**闭包模块化模式 (2003)**: 用闭包方式解决了变量污染问题,闭包内返回模块对象,只需对外暴露一个全局变量。
**模版依赖定义 (2006)**: 这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代码的时候不觉得,回头看看,还是挂在可维护性上。
**注释依赖定义 (2006)**: 几乎和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方式,而是终于以文件为单位定义模块了,通过 [lazyjs](https://github.com/bevacqua/lazyjs) 加载文件,同时读取文件注释,继续递归加载剩下的文件。
**外部依赖定义 (2007)**: 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。
**Sandbox 模式 (2009)**: 这种模块化方式很简单,暴力,将所有模块塞到一个 `sandbox` 变量中,硬伤是无法解决命名冲突问题,毕竟都塞到一个 `sandbox` 对象里,而 `Sandbox` 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
**依赖注入 (2009)**: 就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。
**CommonJS (2009)**: 真正解决模块化问题,从 node 端逐渐发力到前端,前端需要使用构建工具模拟。
**Amd (2009)**: 都是同一时期的产物,这个方案主要解决前端动态加载依赖,相比 commonJs,体积更小,按需加载。
**Umd (2011)**: 兼容了 CommonJS 与 Amd,其核心思想是,如果在 commonjs 环境(存在 `module.exports`,不存在 `define`),将函数执行结果交给 `module.exports` 实现 Commonjs,否则用 Amd 环境的 `define`,实现 Amd。
**Labeled Modules (2012)**: 和 Commonjs 很像了,没什么硬伤,但生不逢时,碰上 Commonjs 与 Amd,那只有被人遗忘的份了。
**YModules (2013)**: 既然都出了 Commonjs Amd,文章还列出了此方案,一定有其独到之处。其核心思想在于使用 `provide` 取代 `return`,可以控制模块结束时机,处理异步结果;拿到第二个参数 `module`,修改其他模块的定义(虽然很有拓展性,但用在项目里是个搅屎棍)。
**ES2015 Modules (2015)**: 就是我们现在的模块化方案,还没有被浏览器实现,大部分项目已通过 `babel` 或 `typescript` 提前体验。
# 3 精读
本次提出独到观点的同学有:[流形](https://github.com/arcthur),[黄子毅](https://github.com/ascoders),[苏里约](https://github.com/javie007),[camsong](https://github.com/camsong),[杨森](https://github.com/jasonslyvia),[淡苍](https://github.com/BlackGanglion),[留影](https://github.com/fanhc019),精读由此归纳。
### 从语言层面到文件层面的模块化
> 从 1999 年开始,模块化探索都是基于语言层面的优化,真正的革命从 2009 年 CommonJS 的引入开始,前端开始大量使用预编译。
这篇文章所提供的模块化历史的方案都是逻辑模块化,**从 CommonJS 方案开始前端把服务端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化**。但之后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了很多额外的成本。
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在 10 年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
```javascript
YUI().use('node', 'event', function (Y) {
// The Node and Event modules are loaded and ready to use.
// Your code goes here!
});
```
YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo 在前端圈的影响力还是很大的,而 requirejs 到 2011 年才诞生,因此圈子不是用着 YUI 要不就自己封装一套 sandbox,内部使用 jQuery。
为什么模块化方案这么晚才成型,可能早期应用的复杂度都在后端,前端都是非常简单逻辑。后来 Ajax 火了之后,web app 概念的开始流行,前端的复杂度也呈指数级上涨,到今天几乎和后端接近一个量级。**工程发展到一定阶段,要出现的必然会出现。**
### 前端三剑客的模块化展望
> 从 js 模块化发展史,我们还看到了 css html 模块化方面的严重落后,如今依赖编译工具的模块化增强在未来会被标准所替代。
原生支持的模块化,**解决 html 与 css 模块化问题正是以后的方向。**
再回到 JS 模块化这个主题,开头也说到是为了构建 scope,实则提供了业务规范标准的输入输出的方式。但文章中的 JS 的模块化还不等于前端工程的模块化,Web 界面是由 HTML、CSS 和 JS 三种语言实现,不论是 CommonJS 还是 AMD 包括之后的方案都无法解决 CSS 与 HTML 模块化的问题。
对于 CSS 本身它就是 global scope,因此开发样式可以说是喜忧参半。近几年也涌现把 HTML、CSS 和 JS 合并作模块化的方案,其中 react/css-modules 和 vue 都为人熟知。当然,这一点还是非常依赖于 webpack/rollup 等构建工具,让我们意识到在 browser 端还有很多本质的问题需要推进。
对于 css 模块化,目前不依赖预编译的方式是 `styled-component`,通过 js 动态创建 class。而目前 css 也引入了[与 js 通信的机制 与 原生变量支持](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Using_CSS_variables)。未来 css 模块化也很可能是运行时的,所以目前比较看好 `styled-component` 的方向。
对于 html 模块化,小尤最近爆出与 chrome 小组调研 html Modules,如果 html 得到了浏览器,编辑器的模块化支持,未来可能会取代 jsx 成为最强大的模块化、模板语言。
对于 js 模块化,最近出现的 `<script type="module">` 方式,虽然还没有得到浏览器原生支持,但也是我比较看好的未来趋势,这样就连 webpack 的拆包都不需要了,直接把源代码传到服务器,配合 http2.0 完美抛开预编译的枷锁。
上述三种方案都不依赖预编译,分别实现了 html、css、js 模块化,相信这就是未来。
### 模块化标准推进速度仍然缓慢
> 2015 年提出的标准,在 17 年依然没有得到实现,即便在 nodejs 端。
这几年 TC39 对语言终于重视起来了,慢慢有动作了,但针对模块标准制定的速度,与落实都非常缓慢,与 javascript 越来越流行的趋势逐渐脱节。nodejs 至今也没有实现 ES2015 模块化规范,所有 jser 都处在构建工具的阴影下。
### Http 2.0 对 js 模块化的推动
> js 模块化定义的再美好,浏览器端的支持粒度永远是瓶颈,http 2.0 正是考虑到了这个因素,大力支持了 ES 2015 模块化规范。
幸运的是,模块化构建将来可能不再需要。随着 HTTP/2 流行起来,请求和响应可以并行,一次连接允许多个请求,对于前端来说宣告不再需要在开发和上线时再做编译这个动作。
几年前,模块化几乎是每个流行库必造的轮子(YUI、Dojo、Angular),大牛们自己爽的同时其实造成了社区的分裂,很难积累。有了 ES2015 Modules 之后,JS 开发者终于可以像 Java 开始者十年前一样使用一致的方式愉快的互相引用模块。
不过 ES2015 Modules 也只是解决了开发的问题,由于浏览器的特殊性,还是要经过繁琐打包的过程,等 Import,Export 和 HTTP 2.0 被主流浏览器支持,那时候才是彻底的模块化。
### Http 2.0 后就不需要构建工具了吗?
> 看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunk 不需要重新下载。
可见,**即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。**
### 模块化是大型项目的银弹吗?
> 只要遵循了最新模块化规范,就可以使项目具有最好的可维护性吗? Js 模块化的目的是支持前端日益上升的复杂度,但绝不是唯一的解决方案。
分析下 JavaScript 为什么没有模块化,为什么又需要模块化:这个 95 年被设计出来的时候,语言的开发者根本没有想到它会如此的大放异彩,也没有将它设计成一种模块化语言。按照文中的说法,99 年也就是 4 年后开始出现了模块化的需求。如果只有几行代码用模块化是扯,初始的 web 开发业务逻辑都写在 server 端,js 的作用小之又小。而现在 spa 都出现了,几乎所有的渲染逻辑都在前端,如果还是没有模块化的组织,开发过程会越来越难,维护也是更痛苦。
文中已经详细说明了模块化的发展和优劣,这里不准备做过多的讨论。我想说的是,**在模块化之后还有一个模块间耦合的问题,如果模块间耦合度大也会降低代码的可重用性或者说复用性**。所以也出现了降低耦合的观察者模式或者发布/订阅模式。这对于提升代码重用,复用性和避免单点故障等都很重要。说到这里,还想顺便提一下最近流行起来的响应式编程(RxJS),响应式编程中有一个很核心的概念就是 observable,也就是 Rx 中的流(stream)。它可以被 subscribe,其实也就是观察者设计模式。
### 补充阅读
- [JavaScript 模块化七日谈](https://huangxuan.me/2015/07/09/js-module-7day/)
- [JavaScript 模块化编程简史(2009-2016)](https://yuguo.us/weblog/javascript-module-development-history/)
# 总结
未来前端复杂度不断增加已成定论,随着后端成熟,自然会将焦点转移到前端领域,而且服务化、用户体验越来越重要,前端体验早不是当初能看就行,任何网页的异常、视觉的差异,或文案的模糊,都会导致用户流失,支付中断。前端对公司营收的影响,渐渐与后端服务宕机同等严重,所以前端会越来越重,异常监控,性能检测,工具链,可视化等等都是这几年大家逐渐重视起来的。
我们早已不能将 javascript 早期玩具性质的模块化方案用于现代越来越重要的系统中,前端界必然出现同等重量级的模块化管理方案,感谢 TC39 制定的 ES2015 模块化规范,我们已经离不开它,哪怕所有人必须使用 babel。
话说回来,标准推进的太慢,我们还是把编译工具当作常态,抱着哪怕支持了 ES2015 所有特性,babel 依然还有用的心态,将预编译进行到底。一句话,模块化仍在路上。js 模块化的矛头已经对准了 css 与 html,这两位元老也该向前卫的 js 学习学习了。
未来 css、html 的模块化会自立门户,还是赋予 js 更强的能力,让两者的模块化依附于 js 的能力呢?目前 html 有自立门户的苗头(htmlModules),而 css 迟迟没有改变,社区出现的 `styled-component` 已经用 js 将 css 模块化得很好了,最新 css 规范也支持了与 js 的变量通信,难道希望依附于 js 吗?这里希望得到大家更广泛的讨论。
我也认同,毕竟压缩、混淆、md5、或者利用 [nonce](https://www.thepolyglotdeveloper.com/2015/03/create-a-random-nonce-string-using-javascript/) 属性对 script 标签加密,都离不开本地构建工具。
据说 http2 的优化中,有个最佳文件大小与数量的比例,那么还是脱离不了构建工具,前端未来会越来越复杂,同时也越来越美好。
至此,对于 javascript 模块化讨论已接近尾声,对其优缺点也基本达成了一致。前端复杂度不断提高,促使着模块化的改进,代理(浏览器、node) 的支持程度,与前端特殊性(流量、缓存)可能前端永远也离不开构建工具,新的标准会让这些工作做的更好,同时取代、增强部分特征,前端的未来是更加美好的,复杂度也更高。
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
================================================
FILE: 前沿技术/10.精读《Web Components 的困境》.md
================================================
本期精读的文章是:[The broken promise of Web Components
](https://dmitriid.com/blog/2017/03/the-broken-promise-of-web-components/)
以及对这篇文章的回应: [Regarding the broken promise of Web Components](https://robdodson.me/regarding-the-broken-promise-of-web-components/)
# 1 引言
我为什么要选这篇文章呢?
就在前几天的 Google I/O 2017 上, Polymer 正式发布了 [Polymer 2.0](https://www.polymer-project.org/blog/2017-05-15-time-for-two) 版本.
来看一下 Polymer 2.0 的一些变化:
- 使用 Shadow DOM v1 代替 Polymer.dom. Shady DOM 从 Polymer 中分离出来。
- 使用 标准的 ES6 类和 Custom Elements v1 来自定义元素.
- 还有数据系统的改进和生命周期的变更.
可以看到, Polymer 的这次升级主要是将 Shadow Dom 和 Custom Elements 升级到 v1 版本, 以获得更多浏览器的原生支持. 下一 代 Web Components - v1 规范,Chrome 已经支持了,Web Components 规范中的 2 个主要部分 - [Shadow Dom](https://www.chromestatus.com/feature/4667415417847808) 和 [Custom Elements](https://www.chromestatus.com/feature/4696261944934400). Safari 在 10 版本中, 支持了 [Shadow DOM v1](https://webkit.org/status/#feature-shadow-dom) 规范并且完成了在 Webkit 内核中对 [Custom Elements v1](https://webkit.org/blog/7027/introducing-custom-elements/) 规范的实现;Firefox 对 [Shadow DOM](https://platform-status.mozilla.org/#shadow-dom) 和 [Custom Elements v1 规范](https://platform-status.mozilla.org/#custom-elements) 支持正在开发中;Edge 也将对 [Shadow DOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/) 和 [Custom Elements](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/) 支持规划到他们的开发 roadmap 中。
这段时间, 大家都在讨论 react, vue, angular, 这些框架. 或者 该使用 redux 还 是 mobx 做数据管理. 在这个契机下, 我想我们可以不单单去思考这些框架, 也可以更多地去思考和了解 Web Components 标准. 对于 Web Components 标准有一些思考. 所以我选了一篇关于 Web Components 的文章, 想让大家对于 Web Components 的发展, 和 Web Componets 与现在的主流框架如何协作有更多的思考和讨论.
# 2 内容概要
**The broken promise of Web Components**
原文作者 dmitriid 主要是在喷 Web Components 从 2011 年到 2017 年这 6 年间毫无进展, 一共产出了 6 份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.

- Web Components 这些规范强依赖 JS 的实现
- Custom Elements 是 JS 脚本的一部分
- HTML Templates 的出现就是为了被 JS 脚本使用
- Shadow Dom 也需要配合 JS 脚本使用
- 只有 HTML imports 可以脱离 JS 脚本使用
- Web Components 操作 DOM
- 属性都是字符串
- 元素的内容模型(Content Model)比较奇怪
- 为了突破限制使用不同的方法来传递数据
- CSS 作用域, 可以见上次精读[《请停止 css-in-js 的行为》](https://github.com/dt-fe/weekly/issues/12)
**来看一下 Polymer 的 核心成员 Rob Dodson 对于本文的回应: Regarding the broken promise of Web Components**
- Web Components 特性需要被浏览器支持,必须有平缓的过渡,良好的兼容,以及成熟的方案,因此推进速度会比较慢一些。
- React 很棒, 但是也不要忽略其他基于 Web Components 的优秀库比如 [Amp](https://www.ampproject.org/)
- 对于 DOM 更新的抽象比如 React/JSX 很赞, 但是也带来了一些损耗. 在旧的移动设备上, 加载一个大的 js 包性能依旧不理想, 最佳的做法是拆分你的 JS 包, 按需加载.
- 使用 JSX 和 虚拟 DOM 是很酷, 也可以直接把 JSX 用在 Web Components 内, 像[SkateJS](https://github.com/skatejs/skatejs)库, 已经在做这个事情了.
- 没有标准的数据绑定, Polymer 的数据绑定, 现在是基于[MDV](https://github.com/toolkitchen/mdv), 很多开发者更倾向于基于 Observables 或者 ES6 Proxies 的数据绑定方案.
- 处理组件的字符串属性是很烦人, 但是由于每一个组件都是一个类的实例, 可以利用 ES6 的 getters/setters 来改变属性.
Rob Dodson 对于 Web Components 依然充满信心, 但是也承认推进标准总会有各种阻碍, 不可能像推荐框架一样快速把事情解决.
# 3 精读
本次提出独到观点的同学有:
[@camsong](https://www.zhihu.com/people/078cc0fb15845759ad8295b0f0e50099) [@黄子毅](https://github.com/ascoders) [@杨森](https://www.zhihu.com/people/c93b7957f6308990c7e3b16103c9356b) [@rccoder](https://github.com/rccoder) [@alcat2008](https://github.com/alcat2008)精读由此归纳。
### 标准与框架
Web Components 作为一个标准,骨子里的进度就会落后于当前可行的技术体系。正如文中所说,浏览器厂商 ship 一个新功能是很严肃的,很可能会影响到一票的线上业务,甚至会影响到一个产业(遥想当年 [Chrome Extension 禁用 NPAPI](https://blog.chromium.org/2013/09/saying-goodbye-to-our-old-friend-npapi.html)时的一片哀鸿遍野,许多返利插件都使用了这种技术)。那么 Web Components 的缓慢推进也在情理之中了.
即使真的有一天这个标准建立起来,Web Components 作为浏览器底层特性不应该拿出来和 React 这类应用层框架相比较. 未来 Web Components 会做为浏览器非常重要的特性存在。API 偏低层操作,会易用性不够. 在很长时间内开发者依旧会使用 React/Vue/Angular/Polymer 这样的框架,Web Components 可能会做为这些框架的底层做一些 浏览器层面上的支持.
### 不需要 vendor 的自定义组件间调用
在 Webpack 大行其道的时代,想在运行时做到组件即引即用变得很困难,因为这些组件大多是通过 React/Vue/Angular 开发的。不得不考虑引入一大堆 Vendor 包,这些 Vendor 里可能还必须包含 React 这类两个版本不能同时使用的库。目前我们团队在做组件化方案时就遇到这个问题,只能想办法避免两个版本的出现。你可以说这是 React 或 Webpack 引入的问题,但并没有看到 Web Compnents 标准化的解决方案。我想未来 Web Components 可能会作为浏览器的底层, 出现基于底层的标准方案来做组件间的相互应用的方法.
### 为什么对 Web components 讨论不断
俗话说,成也萧何,败也萧何。正如原文提及的,现在网页规模越来越大,需求也越来越灵活,html 与 css 的能力已经严重不足,我们才孤注一掷的上了 js 的贼船:JSX 和 Css module,因为 Web components 依托在 html 模版语言上,当然没办法与 js 的灵活性媲美。
但使用前端框架的问题也日益暴露,随着前端框架种类的增多,同一个框架不同版本之间无法共存,导致组件无法跨框架复用,甚至只能固定在框架的某个版本,这与前端未来的模块化发展是相违背的,我们越是与之抗衡,就越希望 Web components 能站出来解决这个问题,因为浏览器原生支持模块化,相当于将 react angular vue 的能力内置在浏览器中,而且一定会向前兼容(这也是 Web components 推进缓慢的原因)。
# 4 总结
我觉得 Web Components 作为浏览器底层特性不应该拿出来和 React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.
> 讨论地址是:[精读《Web Components 的困境》 · Issue #15 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/15)
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
================================================
FILE: 前沿技术/100.精读《V8 引擎 Lazy Parsing》.md
================================================
# 1. 引言
本周精读的文章是 [V8 引擎 Lazy Parsing](https://v8.dev/blog/preparser),看看 V8 引擎为了优化性能,做了怎样的尝试吧!
这篇文章介绍的优化技术叫 [preparser](https://cs.chromium.org/chromium/src/v8/src/parsing/preparser.h?l=921&rcl=e3b2feb3aade83c02e4bd2fa46965a69215cd821),是通过跳过不必要函数编译的方式优化性能。
# 2. 概述 & 精读
解析 Js 发生在网页运行的关键路径上,因此加速对 JS 的解析,就可以加速网页运行效率。
然而并不是所有 Js 都需要在初始化时就被执行,因此也不需要在初始化时就解析所有的 Js!因为编译 Js 会带来三个成本问题:
1. 编译不必要的代码会占用 CPU 资源。
2. 在 GC 前会占用不必要的内存空间。
3. 编译后的代码会缓存在磁盘,占用磁盘空间。
因此所有主流浏览器都实现了 Lazy Parsing(延迟解析),它会将不必要的函数进行预解析,也就是只解析出外部函数需要的内容,而全量解析在调用这个函数时才发生。
## 预解析的挑战
本来预解析也不难,因为只要判断一个函数是否会立即执行就可以了,只有立即执行的函数才需要被完全解析。
使得预解析变复杂的是变量分配问题。原文通过了堆栈调用的例子说明原因:
Js 代码的执行在堆栈上完成,比如下面这个函数:
```js
function f(a, b) {
const c = a + b;
return c;
}
function g() {
return f(1, 2);
// The return instruction pointer of `f` now points here
// (because when `f` `return`s, it returns here).
}
```
这段函数的调用堆栈如下:
<img width=200 src="https://img.alicdn.com/tfs/TB1gNCsRVYqK1RjSZLeXXbXppXa-173-333.svg">
首先是全局 This `globalThis`,然后执行到函数 `f`,再对 `a` `b` 进行赋值。在执行 `f` 函数时,通过 `<rip g>`(return instruction pointer) 保存 g 堆栈状态,再保存堆栈跳出后返回位置的指针 `<save fp>`(frame pointer),最后对变量 `c` 赋值。
这看上去没有问题,只要将值存在堆栈就搞定了。但是将变量定义到函数内部就不一样了:
```js
function make_f(d) {
// ← declaration of `d`
return function inner(a, b) {
const c = a + b + d; // ← reference to `d`
return c;
};
}
const f = make_f(10);
function g() {
return f(1, 2);
}
```
将变量 `d` 申明在函数 `make_f` 中,且在返回函数 `inner` 中用到了 `d`。那么函数的调用栈就变成了这样:
<img width=500 src="https://img.alicdn.com/tfs/TB1HiuGR4YaK1RjSZFnXXa80pXa-428-292.svg">
需要创建一个 `context` 存储函数 `f` 中变量 `d` 的值。
也就是说,如果一个在函数内部定义的变量被子 Scope 使用时,Js 引擎需要识别这种情况,并将这个变量值存储在 `context` 中。
所以对于函数定义的每一个入参,我们需要知道其是否会被子函数引用。**也就是说,在 `preparser` 阶段,我们只要少能分析出哪些变量被内部函数引用了。**
## 难以分辨的引用
预处理器中跟踪变量的申明与引用很复杂,因为 Js 的语法导致了无法从部分表达式推断含义,比如下面的函数:
```js
function f(d) {
function g() {
const a = ({ d }
```
我们不清楚第三行的 `d` 到底是不是指代第一行的 `d`。它可能是:
```js
function f(d) {
function g() {
const a = ({ d } = { d: 42 });
return a;
}
return g;
}
```
也可能只是一个自定义函数参数,与上面的 `d` 无关:
```js
function f(d) {
function g() {
const a = ({ d }) => d;
return a;
}
return [d, g];
}
```
## 惰性 parse
在执行函数时,只会将最外层执行的函数完全编译并生成 AST,而对内部模块只进行 `preparser`。
```js
// This is the top-level scope.
function outer() {
// preparsed
function inner() {
// preparsed
}
}
outer(); // Fully parses and compiles `outer`, but not `inner`.
```
为了允许惰性编译函数,上下文指针指向了 [ScopeInfo](https://cs.chromium.org/chromium/src/v8/src/objects/scope-info.h?rcl=ce2242080787636827dd629ed5ee4e11a4368b9e&l=36) 的对象(从代码中可以看到,ScopeInfo 包含上下文信息,比如当前上下文是否有函数名,是否在一个函数内等等),当编译内部函数时,可以利用 ScopeInfo 继续编译子函数。
但是为了判断惰性编译函数自身是否需要一个上下文,我们需要再次解析内部的函数:比如我们需要知道某个子函数是否对外层函数定义的变量有所引用。
这样就会产生递归遍历:
<img width=800 src="https://img.alicdn.com/tfs/TB1uCOPR7voK1RjSZFwXXciCFXa-960-540.svg">
由于代码总会包含一些嵌套,而编译工具更会产生 IIFE(立即调用函数) 这种多层嵌套的表达式,使得递归性能比较差。
而下面有一种办法可以将时间复杂度简化为线性:将变量分配的位置序列化为一个密集的数组,当惰性解析函数时,变量会按照原先的顺序重新创建,这样就不需要因为子函数可能引用外层定义变量的原因,对所有子函数进行递归惰性解析了。
按照这种方式优化后的时间复杂度是线性的:
<img width=800 src="https://img.alicdn.com/tfs/TB1VS5LR7voK1RjSZFNXXcxMVXa-960-540.svg">
## 针对模块化打包的优化
由于现代代码几乎都是模块化编写的,构建起在打包时会将模块化代码封装在 IIFE(立即调用的闭包)中,以保证模拟模块化环境运行。比如 `(function(){....})()`。
这些代码看似在函数中应该惰性编译,但其实这些模块化代码从一开始就要被编译,否则反而会影响性能,因此 V8 有两种机制识别这些可能被立即调用的函数:
1. 如果函数是带括号的,比如 `(function(){...})`,就假设它会被立即调用。
2. 从 V8 v5.7 / Chrome 57 开始,还会识别 uglifyJS 的 `!function(){...}(), function(){...}(), function(){...}()` 这种模式。
然而在浏览器引擎解析环境比较复杂,很难对函数进行完整字符串匹配,因此只能对函数头进行简单判断。所以对于下面这种匿名函数的行为,浏览器是不识别的:
```js
// pre-parser
function run(func) {
func()
}
run(function(){}) // 在这执行它,进行 full parser
```
上面的代码看上去没毛病,但由于浏览器只检测被括号括住的函数,因此这个函数不被认为是立即执行函数,因此在后续执行时会被重复 full-parse。
也有一些代码辅助转换工具帮助 V8 正确识别,比如 [optimize-js](https://github.com/nolanlawson/optimize-js),会将代码做如下转换。
转换前:
```js
!function (){}()
function runIt(fun){ fun() }
runIt(function (){})
```
转换后:
```js
!(function (){})()
function runIt(fun){ fun() }
runIt((function (){}))
```
然而在 V8 v7.5+ 已经很大程度解决了这个问题,因此现在其实不需要使用 [optimize-js](https://github.com/nolanlawson/optimize-js) 这种库了~
# 4. 总结
JS 解析引擎在性能优化做了不少工作,但同时也要应对代码编译器产生的特殊 IIFE 闭包,防止对这种立即执行闭包进行重复 parser。
最后,不要试图总是将函数用括号括起来,因为这样会导致惰性编译的特性无法启用。
> 讨论地址是:[精读《V8 引擎 Lazy Parsing》 · Issue #148 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/148)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
**special Sponsors**
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: 前沿技术/101.精读《持续集成 vs 持续交付 vs 持续部署》.md
================================================
# 一、摘要
相信大家以前应该接触过持续集成(Continuous integration)持续交付(continuous delivery)持续发布(continuous deployment)的概念,下面我们来说说三者的差异以及团队如何入手 CI/CD。
作者:猫神。
# 二、差异
### 2.1 CI 持续集成
开发者尽量时时刻刻合并开发分支至主干分支。避免直到发布日才开始合并,掉入集成地狱。无论何时新分支集成至项目,持续集成可以自动化测试持续验证应用是否正常。
### 2.2 CD 持续交付
持续交付是持续集成的扩展,可以保证稳定的发布产品新特性。这意味着基于自动化测试,你可以也可以一键自动化发布。理论上,持续交付可以决定是按天,按周,按双周发布产品。如果确实希望能够享受持续交付的好处,那么应该尽快发布到新产品中。一旦出现问题时能尽早排除。
### 2.3 CD 持续部署
持续部署是持续交付的下一步。通过这一步,每个新特性都自动的部署到产品中。但是如果出现未通过的测试用例将会终止自动部署。持续部署可以加速用户反馈新特性,避免发布日带来的压力。开发可以着力于开发系统,开发结束后几分钟就可以触达到用户。
# 三、协作
CI/CD 具体是个什么样的流程呢,如下图所示,差异仅在于是否自动部署。
<img src="https://img.alicdn.com/tfs/TB1sNtaTrPpK1RjSZFFXXa5PpXa-884-426.png" />
现在开发都讲究投入产出比,那么 CI/CD 具体需要做些什么呢?
### Continuous Intergretion 持续集成
投入:
- 需要为每个新特性编写测试用例
- 需要搭建持续集成服务器,监控主干仓库,并自动运行测试用例
- 开发需要尽量频繁的合并分支,至少一天一次
产出:
- 更少的 bug,因为自动化测试可以回归测试产品
- 编译部署产品更简化,因为集成的问题都尽早的解决了
- 开发者可以尽量减少上下文切换,因为构建的时候就暴露问题,尽早解决了
- 测试成本降低,因为 CI 服务器可以一秒运行几百个测试用例
- 测试团队花更少的时间测试,可以重点关注测试上的改进。
### Continuous delivery 持续交付
投入:
- 需要有持续集成的基础,测试用例需要覆盖足够的代码
- 部署需要自动化,用户只需要手动触发,剩余的部署应该自动化
- 团队需要增加新特性标志,避免未完成的新特性进入待发布的产品
产出:
- 部署软件变得非常简单。团队不需要花费 n 天准备发布。
- 可以提高发布频率,加速新特性触达用户进程。
- 小的更改,对决策的压力要小得多,可以更快地迭代。
### Continuous deployment 持续部署
投入:
- 测试必须要做到足够。测试的质量将决定发布的质量。
- 文档建设需要和产品部署保持同步。
- 新特性的发布需要协调其他部门,包括售后支持&市场&推广等。
产出:
- 快速的发布节奏,因为每个新特性一旦完成都会自动的发布给用户。
- 发布风险降低,修复问题更容易,因为每次变更都是小步迭代发布。
- 用户可以看到持续性的优化和质量提升,而不是非要等到按月,按季度,甚至按年
如果开发的是一个新项目,暂时还没有任何用户,那么每次提交代码后发布将会特别简单,可以随时随地发布。一旦产品开始开发后,就需要提高测试文化,并确保在构建应用程序时增加代码覆盖率。当您准备好面向用户发布时,您将有一个非常好的连续部署过程,在该过程中,所有新的更改都将在自动发布到生产环境之前进行测试。
如果正在开发的是一个老系统,就需要放慢节奏,开始打造持续集成&持续交付。首先可以完成一些简单可自动化执行的单元测试,不需要考虑复杂的端到端的测试。另外,应该尽快尝试自动化部署,搭建可以自动化部署的临时环境。因为自动化部署,可以让开发者去优化测试用例,而不是停下来联调发布。
一旦开始按日发布产品,我们可以考虑持续部署,但一定要保证团队已经准备好这种方式,文档 & 售后支持 & 市场。这些步骤都需要加入到新产品发布节奏中,因为和用户直接打交道的是他们。
# 四、如何开始持续集成
## 4.1 了解测试类型
为了获得 CI 的所有好处,每次代码变更后,我们需要自动运行测试用例。我们需要在每个分支运行测试用例,而不是仅仅在主干分支。这样可以最快速的找到问题,最小化问题影响面。
在初始阶段并不需要实现所有的测试类型。一开始可以以单元测试入手,随着时间扩展覆盖面。
- 单元测试:范围非常小,验证每个独立方法级别的操作。
- 集成测试:保证模块间运行正常,包括多个模块、多个服务。
- 验收测试:与集成测试类似,但是仅关注业务 case,而不是模块内部本身。
- UI 测试:从用户的角度保证呈现正确运行。
并不是所有的测试都是对等的,实际运行中可以做些取舍。
<img src="https://img.alicdn.com/tfs/TB18278TmrqK1RjSZK9XXXyypXa-346-269.png" />
单元测试实现起来既快成本又低,因为它们主要是对小代码块进行检查。另一方面,UI 测试实施起来很复杂,运行起来很慢,因为它们通常需要启动一个完整的环境以及多个服务来模拟浏览器或移动行为。因此,实际情况可能希望限制复杂的 UI 测试的数量,并依赖基础上良好的单元测试来快速构建,并尽快获得开发人员的反馈。
### 4.2 自动运行测试
要采用持续集成,您需要对推回到主分支的每个变更运行测试。要做到这一点,您需要有一个服务来监视您的存储库,并听取对代码库的新推送。您可以从企业预置型解决方案和云端解决方案中进行选择。您需要考虑以下因素来选择服务器:
- 代码托管在哪里?CI 服务可以访问您的代码库吗?您对代码的生存位置有特殊的限制吗?
- 应用程序需要哪些操作系统和资源?应用程序环境是否受支持?能安装正确的依赖项来构建和测试软件吗?
- 测试需要多少资源?一些云应用程序可能对您可以使用的资源有限制。如果软件消耗大量资源,可能希望将 CI 服务器宿主在防火墙后面。
- 团队中有多少开发人员?当团队实践 CI 时,每天都会将许多更改推回到主存储库中。对于开发人员来说,要获得快速的反馈,您需要减少构建的队列时间,并且您需要使用能够提供正确并发性的服务或服务器。
在过去,通常需要安装一个独立的 CI 服务器,如 Bamboo 或 Jenkins,但现在您可以在云端找到更简单的解决方案。例如,如果您的代码托管在 BitBucket 云上,那么您可以使用存储库中的 Pipelines 功能在每次推送时运行测试,而无需配置单独的服务器或构建代理,也无需限制并发性。
- 使用代码覆盖率查找未测试的代码。一旦您采用了自动化测试,最好将它与一个测试覆盖工具结合起来,帮助了解测试套件覆盖了多少代码库。代码覆盖率定在 80%以上是很好的,但要注意不要将高覆盖率与良好的测试套件混淆。代码覆盖工具将帮助您找到未经测试的代码,但在一天结束的时候,测试的质量会产生影响。如果刚开始,不要急于获得代码库的 100%覆盖率,而是使用测试覆盖率工具来找出应用程序的关键部分,这些部分还没有测试并从那里开始。
- 重构是一个添加测试的机会。如果您将要对应用程序进行重大更改,那么应该首先围绕可能受到影响的特性编写验收测试。这将为您提供一个安全网,以确保在重构代码或添加新功能后,原始行为不会受到影响。
# 五、接受 CI 文化
自动化测试是 CI 的关键,但同时也需要团队成员接受 CI 文化,并不是心血来潮晒两天鱼,并且需要保证编译畅通无阻。QA 可以帮助团队建设测试文化。他们不再需要手动测试应用程序的琐碎功能,现在他们可以投入更多的时间来提供支持开发人员的工具,并帮助他们采用正确的测试策略。一旦开始采用持续集成,QA 工程师将能够专注于使用更好的工具和数据集促进测试,并帮助开发人员提高编写更好代码的能力。
- 尽早集成。如果很长时间不合并代码,代码冲突的风险就越高,代码冲突的范围就越广。如果发现某些分支会影响已经存在的分支,需要增加发布关闭标签,避免发布时两个分支冲突。
- 保证编译时时刻刻畅通。一旦发现任何编译问题,立刻修复,否则可能会带来更多的错误。测试套件需要尽快反馈测试结果,或者优先返回短时间测试(单元测试)的结果,否则开发者可能就切换回开发了。一旦编译出错,需要通知给开发者,或者更进一步给出一个 dashboard,每个人都可以在这里查看编译结果。
- 把测试用例纳入流程的一部分。确保每个分支都有自动化测试用例。似乎编写测试用例拖慢了项目节奏,但是它可以减少回归时间,减少每次迭代带来的 bug。而且每次测试通过后,将会非常有信息合并到主干分支,因为新增的内容不影响以前的功能。
- 修 bug 的时候编写测试用例。把 bug 的每个场景都编写成测试用例,避免再次出现。
# 六、集成测试 5 个步骤
1. 从最严格的代码部分入手测试
2. 搭建一个自动构建的服务自动运行测试用例,在每次提交代码后。
3. 确保团队成员每天合并变更
4. 代码出现问题及时修复
5. 为每个新实现的操作编写测试用例。
可能看着很简单,但是要求团队能够真正落地。一开始你需要放慢发布的脚步,需要和 pd、用户沟通确保不上线没有测试用例的新功能。我们的建议是从小处入手,通过简单的测试来适应新的例程,然后再着手实现更复杂更难管理的测试套件。
# 七、说说笔者的团队
以上文章主要是说明团队实现 CI/CD 的取舍和可行性步骤。下面来说说希望 CI/CD 给笔者团队带来什么样的变化。目前笔者团队已经实现前端项目发布编译工程化,采用的是基于 webpack 的自建工具云构建模式。但现在面临的问题是 1. 交互的系统比较多,交互系统提供的接入源变更后,需要人工通知其他系统手动触发编译,而且每次手动编译都需要在本地切换到指定分支,然后手动触发云构建,2. 多人协作,分支拆分较细,需要手动合并分支,触发编译。整个流程冗长,而且中间存在人力沟通成本,容易产生沟通误差。所以首先希望解决的是 CI 自动化,当依赖变更后或者分支合并后,自动集成,自动编译。当然生产环境暂时还不敢瞎搞,但大部分重复编译的工作量主要集中在预发环境,所以手动部署生产环境的成本还是可以接受的。CI 自动化之前,需要提供系统之间交互的单元测试用例,每次 CI 后自动运行单元测试用例,最好能打通 QA 的测试用例,进行回归测试。流程对比如下:
<img src="https://img.alicdn.com/tfs/TB1jIw.TgDqK1RjSZSyXXaxEVXa-1950-662.png"/>
可以看出引入CI后,我们的成本是需要搭建CI服务器,新增单元测试、打通回归测试案例,但前者可以加快系统编译效率,后者可以进一步的提升代码质量,减少回归测试时间,这些成本都是可以接受的。市面上已有很多开源持续集成工具,例如我们熟悉的Jenkins,还有TeamCity、Travis CI、GO CD、Bamboo、Gitlab CI、CircleCI……等等等等。目前还在继续调研中,这片文章应该会有第二篇,说说后续的实践和CD。
> 讨论地址是:[精读《持续集成 vs 持续交付 vs 持续部署》 · Issue #147 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/147)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
**special Sponsors**
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: 前沿技术/102.精读《Monorepo 的优势》.md
================================================
# 1. 引言
本周精读的文章是 [The many Benefits of Using a Monorepo](https://pspdfkit.com/blog/2019/benefits-of-a-monorepo/)。
现在介绍 Monorepo 的文章很多,可以分为如下几类:直接介绍 [Lerna](https://github.com/lerna/lerna) API 的;介绍如何从独立仓库迁移到 Lerna 的;通过举例子说明 Monorepo 重要性的。
本文属于第三种,从 Android 与 IOS 的开发故事说明了 Monorepo 的重要性。
笔者之所以选择这篇文章,不是因为其故事写的好,而是认可这种具有普适性的解决思路。毕竟 Lerna 作为 Monorepo 的实现之一也并不尽善尽美,而不同场景对 Monorepo 依赖的原因、功能也有所不同,所以希望借这篇文章,从理论上解释清楚为什么会产生 Monorepo,以及 Monorepo 可以解决哪些问题,这样在工作遇到问题时,才能想清楚自己要的是什么。
# 2. 概述
作者的一个项目是 PDF 服务,简称 PSPDFKit,需要同时兼顾 Android 与 IOS 平台,项目的发展经历了如下几个阶段。
## 初始阶段
在 2011 到 2013 年间,PSPDFKit 仅支持 IOS 平台,但最终项目需要支持 Android,因此开了一个新仓库放置 Android 代码。Android 仓库的代码不仅在 UI 上不同,同时解析 PDF 文档的核心代码也不同,这是因为 IOS 平台上使用内置 PDF 渲染引擎同时做了一些业务拓展,但使用的 OC 代码无法在 Android 使用。
最终新建了两个仓库 `PSPDFKit-Android` 与 `Core` 。
仓库 Core 中代码依赖 Android 平台 JNI 的支持,所以并不能实现 Core 一处修改,两处都生效的愿望,而我们又希望两边功能始终兼容,且减少分支过多带来的潜在的冲突,因此花了很久才意识到应该将这两个仓库合并起来。
## 考虑使用 Monorepo
由于 Android 的整套流程自己控制的,因此总是可以快速修复用户提出的 BUG,然而 IOS 提供的 CGPDF 总会遇上各种问题。所以在 2014 年,我们开启了一个庞大的项目,重写 IOS 的 Core 库。有三中方式可供选择:
1. 在 IOS 代码中引用 `PSPDFKit-Android`。
2. 将 `PSPDFKit-Android` 提取到 `Core` 仓库中并分别维护。
3. 将 IOS 与 Android 代码合并到一个仓库中。
经过讨论,最终作者的团队选择了第三种方案,因此目录结构类似如下:
```bash
- ios-platform
- android-platform
- core
```
## 特例
Web 与后台服务代码一直是一个特例,我们认为这些内容相对独立,所以没有将其代码放置到 Monorepo 中。
直到一年后,开始探索 WebAssembly 时,PSPDFKit-web 模块就出现了,因为可以利用 WebAssembly 将 Core 的代码编译并在 Web 平台使用,因此 Core 仓库与 Web 仓库的关系变得非常紧密,最终,我们将 Web、Server 也都迁移到 Monorepo 中了。
## 问题
Monorepo 瑕不掩瑜,但作者还是列举了一些缺陷。
由于源码在一起,仓库变更非常常见,存储空间也变得很大,甚至几 GB,CI 测试运行时间也会变长。即便如此,团队中任何人都不想回到 git submodules 多仓库的方式。
# 3. 精读
总的来说,**虽然拆分子仓库、拆分子 NPM 包(For web)是进行项目隔离的天然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一起更高效。**
**工程化的最终目的是让业务开发可以 100% 聚焦在业务逻辑上**,那么这不仅仅是脚手架、框架需要从自动化、设计上解决的问题,这涉及到仓库管理的设计。
一个理想的开发环境可以抽象成这样:
“只关心业务代码,可以直接跨业务复用而不关心复用方式,调试时所有代码都在源码中。”
在前端开发环境中,多 Git Repo,多 Npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 Npm Link。
另外对于多仓库的缺点,文中还有一些没有提到的因素,这里一并列举出来:
**管理、调试困难**
多个 git 仓库管理起来天然是麻烦的。对于功能类似的模块,如果拆成了多个仓库,无论对于多人协作还是独立开发,都需要打开多个仓库页面。
虽然 vscode 通过 [Workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces) 解决多仓库管理的问题,但在多人协作的场景下,无法保证每个人的环境配置一致。
对于共用的包通过 Npm 安装,如果不能接受调试编译后的代码,或每次 npm link 一下,就没有办法调试依赖的子包。
**分支管理混乱**
假如一个仓库提供给 A、B 两个项目用,而 B 项目优先开发了功能 b,无法与 A 项目兼容,此时就要在这个仓库开一个 `feature/b` 的分支支持这个功能,并且在未来合并到主干同步到项目 A。
一旦需要开分支的组件变多了,且之间出来依赖关联,分支管理复杂度就会呈指数上升。
**依赖关系复杂**
独立仓库间组件版本号的维护需要手动操作,因为源代码不在一起,所以没有办法整体分析依赖,自动化管理版本号的依赖。
**三方依赖版本可能不一致**
一个独立的包拥有一套独立的开发环境,难以保证子模块的版本和主项目完全一直,就存在运行结果不一致的风险。
**占用总空间大**
正常情况下,一个公司的业务项目只有一个主干,多 git repo 的方式浪费了大量存储空间重复安装比如 React 等大型模块,时间久了可能会占用几十 GB 的额外空间,对于没有外接硬盘的同学来说,定期清理不用的项目下 `node_modules` 也是一件麻烦事。
**不利于团队协作**
一个大项目可能会用到数百个二方包,不同二方包的维护频率不同,权限不同,仓库位置也不同,主仓库对它们的依赖方式也不同。
一旦其中一个包进行了非正常改动,就会影响到整个项目,而我们精力有限,只盯着主仓库,往往会栽在不起眼的二方包发布上。
所以对于一个非常复杂,又具有技术挑战的大型系统在协作人员多的情况下出现问题的概率非常大,需要通过 Review 制度避免错误的发生,那么将所有相关的源码聚合在一个仓库下,是更好管理的。
## 理想 monorepo 的设计
参考 [Lerna](https://github.com/lerna/lerna) 的规范,以 `packages` 作为子模块根文件夹,笔者设计一个理想的 monorepo 结构:
```bash
.
├── packages
│ ├─ module-a
│ │ ├─ src # 模块 a 的源码
│ │ └─ package.json # 自动生成的,仅模块 a 的依赖
│ └─ module-b
│ ├─ src # 模块 b 的源码
│ └─ package.json # 自动生成的,仅模块 b 的依赖
├── tsconfig.json # 配置文件,对整个项目生效
├── .eslintrc # 配置文件,对整个项目生效
├── node_modules # 整个项目只有一个外层 node_modules
└── package.json # 包含整个项目所有依赖
```
所有全局配置文件只有一个,这样不会导致 IDE 遇到子文件夹中的配置文件,导致全局配置失效或异常。`node_modules` 也只有一个,既保证了项目依赖的一致性,又避免了依赖被重复安装,节省空间的同时还提高了安装速度。
**兄弟模块之间通过模块 `package.json` 定义的 `name` 相互引用,保证模块之间的独立性,但又不需要真正发布或安装这个模块,通过 `tsconfig.json` 的 `paths` 与 `webpack` 的 `alias` 共同实现虚拟模块路径的效果。**
再结合 [Lerna](https://github.com/lerna/lerna) 根据联动发布功能,使每个子模块都可以独立发布。
# 4. 总结
[Lerna](https://github.com/lerna/lerna) 是业界知名度最高的 Monorepo 管理工具,功能完整。但由于通用性要求非常高,需要支持任意项目间 Monorepo 的组合,因此在 `packages` 文件夹下的配置文件还是与独立仓库保持一致,这样在 TS 环境下会造成配置截断的问题。同时包之间的引用也通过更通用的 [symlink](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_symlink_target_path_type_callback) 完成,这导致了还是要在子模块目录存在 `node_modules` 文件夹,而且效果依赖项目初始化命令。
如果加一些限定条件,比如基于 Webpack + Typescript 环境的 Monorepo,可以换一套思路,利用这些工具自身运行时功能,减少更多模版代码或配置文件,进一步提升 Monorepo 的效果。
对于别名映射,对 [symlink](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_symlink_target_path_type_callback) 与 [alias](https://webpack.js.org/configuration/resolve/#resolvealias) 进行对比:
- symlink: 更通用,适合任何构建器。但需要初始化,且在每个关联模块下新增 `node_modules` 文件夹。
- alias: 限定构建器。但不需要初始化,不新增文件夹,甚至可以运行时动态修改别名配置。
可见如果限定了构建器,别名映射可以做得更轻量,且无需初始化。
今天的问题是,你的项目需要使用 Monorepo 吗?你对 Monorepo 有其他要求吗?
> 讨论地址是:[精读《Monorepo 的优势》 · Issue #151 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/151)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
**special Sponsors**
- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: 前沿技术/104.精读《Function Component 入门》.md
================================================
# 1. 引言
如果你在使用 React 16,可以尝试 Function Component 风格,享受更大的灵活性。但在尝试之前,最好先阅读本文,对 Function Component 的思维模式有一个初步认识,防止因思维模式不同步造成的困扰。
# 2. 精读
### 什么是 Function Component?
Function Component 就是以 Function 的形式创建的 React 组件:
```jsx
function App() {
return (
<div>
<p>App</p>
</div>
);
}
```
也就是,一个返回了 JSX 或 `createElement` 的 Function 就可以当作 React 组件,这种形式的组件就是 Function Component。
> 所以我已经学会 Function Component 了吗?
别急,故事才刚刚开始。
### 什么是 Hooks?
Hooks 是辅助 Function Component 的工具。比如 `useState` 就是一种 Hook,它可以用来管理状态:
```jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
```
`useState` 返回的结果是数组,数组的第一项是 **值**,第二项是 **赋值函数**,`useState` 函数的第一个参数就是 **默认值**,也支持回调函数。更详细的介绍可以参考 [Hooks 规则解读](https://reactjs.org/docs/hooks-rules.html#explanation)。
### 先赋值再 setTimeout 打印
我们再将 `useState` 与 `setTimeout` 结合使用,看看有什么发现。
创建一个按钮,点击后让计数器自增,**但是延时 3 秒后再打印出来**:
```jsx
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
```
如果我们 **在三秒内连续点击三次**,那么 `count` 的值最终会变成 `3`,而随之而来的输出结果是。。?
```plain
0
1
2
```
嗯,好像对,但总觉得有点怪?
### 使用 Class Component 方式实现一遍呢?
敲黑板了,回到我们熟悉的 Class Component 模式,实现一遍上面的功能:
```jsx
class Counter extends Component {
state = { count: 0 };
log = () => {
this.setState({
count: this.state.count + 1
});
setTimeout(() => {
console.log(this.state.count);
}, 3000);
};
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.log}>Click me</button>
</div>
);
}
}
```
嗯,结果应该等价吧?3 秒内快速点击三次按钮,这次的结果是:
```plain
3
3
3
```
怎么和 Function Component 结果不一样?
**这是用好 Function Component 必须迈过的第一道坎,请确认完全理解下面这段话:**
首先对 Class Component 进行解释:
1. 首先 state 是 Immutable 的,`setState` 后一定会生成一个全新的 state 引用。
2. 但 Class Component 通过 `this.state` 方式读取 state,**这导致了每次代码执行都会拿到最新的 state 引用**,所以快速点击三次的结果是 `3 3 3`。
那么对 Function Component 而言:
1. `useState` 产生的数据也是 Immutable 的,通过数组第二个参数 Set 一个新值后,原来的值会形成一个新的引用在下次渲染时。
2. 但由于对 state 的读取没有通过 `this.` 的方式,使得 **每次 `setTimeout` 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。**
为了更容易理解,我们来模拟三次 Function Component 模式下点击按钮时的状态:
第一次点击,共渲染了 2 次,`setTimeout` 生效在第 `1` 次渲染,此时状态为:
```js
function Counter() {
const [0, setCount] = useState(0);
const log = () => {
setCount(0 + 1);
setTimeout(() => {
console.log(0);
}, 3000);
};
return ...
}
```
第二次点击,共渲染了 3 次,`setTimeout` 生效在第 `2` 次渲染,此时状态为:
```js
function Counter() {
const [1, setCount] = useState(0);
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(1);
}, 3000);
};
return ...
}
```
第三次点击,共渲染了 4 次,`setTimeout` 生效在第 `3` 次渲染,此时状态为:
```js
function Counter() {
const [2, setCount] = useState(0);
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(2);
}, 3000);
};
return ...
}
```
可以看到,每一个渲染都是一个独立的闭包,在独立的三次渲染中,`count` 在每次渲染中的值分别是 `0 1 2`,所以无论 `setTimeout` 延时多久,打印出来的结果永远是 `0 1 2`。
理解了这一点,我们就能继续了。
### 如何让 Function Component 也打印 `3 3 3`?
所以这是不是代表 Function Component 无法覆盖 Class Component 的功能呢?完全不是,**我希望你读完本文后,不仅能解决这个问题,更能理解为什么用 Function Component 实现的代码更佳合理、优雅**。
第一种方案是借助一个新 Hook - `useRef` 的能力:
```js
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
```
这种方案的打印结果就是 `3 3 3`。
想要理解为什么,首先要理解 `useRef` 的功能:**通过 `useRef` 创建的对象,其值只有一份,而且在所有 Rerender 之间共享**。
所以我们对 `count.current` 赋值或读取,读到的永远是其最新值,而与渲染闭包无关,因此如果快速点击三下,必定会返回 `3 3 3` 的结果。
但这种方案有个问题,就是使用 `useRef` 替代了 `useState` 创建值,那么很自然的问题就是,如何不改变原始值的写法,达到同样的效果呢?
### 如何不改造原始值也打印 `3 3 3`?
一种最简单的做法,就是新建一个 `useRef` 的值给 `setTimeout` 使用,而程序其余部分还是用原始的 `count`:
```js
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() => {
currentCount.current = count;
});
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
```
通过这个例子,我们引出了一个新的,也是 **最重要的 Hook - `useEffect`**,请务必深入理解这个函数。
`useEffect` 是处理副作用的,其执行时机在 **每次 Render 渲染完毕后**,换句话说就是每次渲染都会执行,只是实际在真实 DOM 操作完毕后。
我们可以利用这个特性,在每次渲染完毕后,将 `count` 此时最新的值赋给 `currentCount.current`,这样就使 `currentCount` 的值自动同步了 `count` 的最新值。
为了确保大家准确理解 `useEffect`,笔者再啰嗦一下,将其执行周期拆解到每次渲染中。假设你在三秒内快速点击了三次按钮,那么你需要在大脑中模拟出下面这三次渲染都发生了什么:
第一次点击,共渲染了 2 次,`useEffect` 生效在第 `2` 次渲染:
```js
function Counter() {
const [1, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 1; // 第二次渲染完毕后执行一次
});
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
```
第二次点击,共渲染了 3 次,`useEffect` 生效在第 `3` 次渲染:
```js
function Counter() {
const [2, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 2; // 第三次渲染完毕后执行一次
});
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
```
第三次点击,共渲染了 4 次,`useEffect` 生效在第 `4` 次渲染:
```js
function Counter() {
const [3, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 3; // 第四次渲染完毕后执行一次
});
const log = () => {
setCount(3 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
```
注意对比与上面章节展开的 `setTimeout` 渲染时有什么不同。
要注意的是,`useEffect` 也随着每次渲染而不同的,**同一个组件不同渲染之间,`useEffect` 内闭包环境完全独立**。对于本次的例子,`useEffect` 共执行了 **四次**,经历了如下四次赋值最终变成 `3`:
```js
currentCount.current = 0; // 第 1 次渲染
currentCount.current = 1; // 第 2 次渲染
currentCount.current = 2; // 第 3 次渲染
currentCount.current = 3; // 第 4 次渲染
```
请确保理解了这句话再继续往下阅读:
- **`setTimeout` 的例子,三次点击触发了四次渲染,但 `setTimeout` 分别生效在第 1、2、3 次渲染中,因此值是 `0 1 2`**。
- **`useEffect` 的例子中,三次点击也触发了四次渲染,但 `useEffect` 分别生效在第 1、2、3、4 次渲染中,最终使 `currentCount` 的值变成 `3`**。
### 用自定义 Hook 包装 `useRef`
是不是觉得每次都写一堆 `useEffect` 同步数据到 `useRef` 很烦?是的,想要简化,就需要引出一个新的概念:**自定义 Hooks**。
首先介绍一下,自定义 Hooks 允许创建自定义 Hook,只要函数名遵循以 `use` 开头,且返回非 JSX 元素,就是 Hooks 啦!**自定义 Hooks 内还可以调用包括内置 Hooks 在内的所有自定义 Hooks**。
也就是我们可以将 `useEffect` 写到自定义 Hook 里:
```js
function useCurrentValue(value) {
const ref = useRef(0);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
```
这里又引出一个新的概念,**就是 `useEffect` 的第二个参数,dependences**。dependences 这个参数定义了 `useEffect` 的依赖,在新的渲染中,只要所有依赖项的引用都不发生变化,`useEffect` 就不会被执行,且当依赖项为 `[]` 时,`useEffect` 仅在初始化执行一次,后续的 Rerender 永远也不会被执行。
这个例子中,我们告诉 React:仅当 `value` 的值变化了,再将其最新值同步给 `ref.current`。
那么这个自定义 Hook 就可以在任何 Function Component 调用了:
```js
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useCurrentValue(count);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
```
封装以后代码清爽了很多,而且最重要的是将逻辑封装起来,我们只要理解 `useCurrentValue` 这个 Hook 可以产生一个值,其最新值永远与入参同步。
看到这里,也许有的小伙伴已经按捺不住迸发的灵感了:**将 `useEffect` 第二个参数设置为空数组,这个自定义 Hook 就代表了 `didMount` 生命周期!**
是的,但笔者建议大家 **不要再想生命周期的事情**,这样会阻碍你更好的理解 Function Component。因为下一个话题,就是要告诉你:**永远要对 `useEffect` 的依赖诚实,被依赖的参数一定要填上去,否则会产生非常难以察觉与修复的 BUG。**
### 将 `setTimeout` 换成 `setInterval` 会怎样
我们回到起点,将第一个 `setTimeout` Demo 中换成 `setInterval`,看看会如何:
```js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
```
**这个例子将引发学习 Function Component 的第二个拦路虎,理解了它,才深入理解了 Function Component 的渲染原理。**
首先介绍一下引入的新概念,**`useEffect` 函数的返回值**。它的返回值是一个函数,这个函数在 `useEffect` 即将重新执行时,会先执行上一次 Rerender `useEffect` 第一个回调的返回函数,再执行下一次渲染的 `useEffect` 第一个回调。
以两次连续渲染为例介绍,展开后的效果是这样的:
第一次渲染:
```js
function Counter() {
useEffect(() => {
// 第一次渲染完毕后执行
// 最终执行顺序:1
return () => {
// 由于没有填写依赖项,所以第二次渲染 useEffect 会再次执行,在执行前,第一次渲染中这个地方的回调函数会首先被调用
// 最终执行顺序:2
}
});
return ...
}
```
第二次渲染:
```js
function Counter() {
useEffect(() => {
// 第二次渲染完毕后执行
// 最终执行顺序:3
return () => {
// 依此类推
}
});
return ...
}
```
**然而本 Demo 将 `useEffect` 的第二个参数设置为了 `[]`,那么其返回函数只会在这个组件被销毁时执行**。
读懂了前面的例子,应该能想到,这个 Demo 希望利用 `[]` 依赖,将 `useEffect` 当作 `didMount` 使用,再结合 `setInterval` 每次时 `count` 自增,这样期望将 `count` 的值每秒自增 1。
然而结果是:
```js
1
1
1
...
```
理解了 `setTimeout` 例子的读者应该可以自行推导出原因:`setInterval` 永远在第一次 Render 的闭包中,`count` 的值永远是 `0`,也就是等价于:
```js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(0 + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
```
然而罪魁祸首就是 **没有对依赖诚实** 导致的。例子中 `useEffect` 明明依赖了 `count`,依赖项却非要写 `[]`,所以产生了很难理解的错误。
所以改正的办法就是 **对依赖诚实**。
### 永远对依赖项诚实
一旦我们对依赖诚实了,就可以得到正确的效果:
```js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
```
我们将 `count` 作为了 `useEffect` 的依赖项,就得到了正确的结果:
```plain
1
2
3
...
```
既然漏写依赖的风险这么大,自然也有保护措施,那就是 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 这个插件,会自动订正你的代码中的依赖,想不对依赖诚实都不行!
然而对这个例子而言,代码依然存在 BUG:每次计数器都会重新实例化,如果换成其他费事操作,性能成本将不可接受。
### 如何不在每次渲染时重新实例化 `setInterval`?
最简单的办法,就是利用 `useState` 的第二种赋值用法,不直接依赖 `count`,而是以函数回调方式进行赋值:
```js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
```
这这写法真正做到了:
1. 不依赖 `count`,所以对依赖诚实。
2. 依赖项为 `[]`,只有初始化会对 `setInterval` 进行实例化。
而之所以输出还是正确的 `1 2 3 ...`,原因是 `setCount` 的回调函数中,`c` 值永远指向最新的 `count` 值,因此没有逻辑漏洞。
但是聪明的同学仔细一想,就会发现一个新问题:如果存在两个以上变量需要使用时,这招就没有用武之地了。
### 同时使用两个以上变量时?
如果同时需要对 `count` 与 `step` 两个变量做累加,那 `useEffect` 的依赖必然要写上一种某一个值,频繁实例化的问题就又出现了:
```js
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return <h1>{count}</h1>;
}
```
这个例子中,由于 `setCount` 只能拿到最新的 `count` 值,而为了每次都拿到最新的 `step` 值,就必须将 `step` 申明到 `useEffect` 依赖中,导致 `setInterval` 被频繁实例化。
这个问题自然也困扰了 React 团队,所以他们拿出了一个新的 Hook 解决问题:`useReducer`。
### 什么是 useReducer
先别联想到 Redux。只考虑上面的场景,看看为什么 React 团队要将 `useReducer` 列为内置 Hooks 之一。
先介绍一下 `useReducer` 的用法:
```js
const [state, dispatch] = useReducer(reducer, initialState);
```
`useReducer` 返回的结构与 `useState` 很像,只是数组第二项是 `dispatch`,而接收的参数也有两个,初始值放在第二位,第一位就是 `reducer`。
`reducer` 定义了如何对数据进行变换,比如一个简单的 `reducer` 如下:
```js
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: state.count + 1
};
default:
return state;
}
}
```
这样就可以通过调用 `dispatch({ type: 'increment' })` 的方式实现 `count` 自增了。
那么回到这个例子,我们只需要稍微改写一下用法即可:
```js
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
function reducer(state, action) {
switch (action.type) {
case "tick":
return {
...state,
count: state.count + state.step
};
}
}
```
可以看到,我们通过 `reducer` 的 `tick` 类型完成了对 `count` 的累加,而在 `useEffect` 的函数中,竟然完全绕过了 `count`、`step` 这两个变量。所以 `useReducer` 也被称为解决此类问题的 “黑魔法”。
其实不管被怎么称呼也好,其本质是让函数与数据解耦,**函数只管发出指令,而不需要关心使用的数据被更新时,需要重新初始化自身。**
仔细的读者会发现这个例子还是有一个依赖的,那就是 `dispatch`,然而 `dispatch` 引用永远也不会变,因此可以忽略它的影响。**这也体现了无论如何都要对依赖保持诚实。**
这也引发了另一个注意项:**尽量将函数写在 `useEffect` 内部**。
### 将函数写在 `useEffect` 内部
为了避免遗漏依赖,必须将函数写在 `useEffect` 内部,这样 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 才能通过静态分析补齐依赖项:
```js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
function getFetchUrl() {
return "https://v?query=" + count;
}
getFetchUrl();
}, [count]);
return <h1>{count}</h1>;
}
```
`getFetchUrl` 这个函数依赖了 `count`,而如果将这个函数定义在 `useEffect` 外部,无论是机器还是人眼都难以看出 `useEffect` 的依赖项包含 `count`。
然而这就引发了一个新问题:将所有函数都写在 `useEffect` 内部岂不是非常难以维护?
### 如何将函数抽到 `useEffect` 外部?
为了解决这个问题,我们要引入一个新的 Hook:`useCallback`,它就是解决将函数抽到 `useEffect` 外部的问题。
我们先看 `useCallback` 的用法:
```js
function Counter() {
const [count, setCount] = useState(0);
const getFetchUrl = useCallback(() => {
return "https://v?query=" + count;
}, [count]);
useEffect(() => {
getFetchUrl();
}, [getFetchUrl]);
return <h1>{count}</h1>;
}
```
可以看到,`useCallback` 也有第二个参数 - 依赖项,我们将 `getFetchUrl` 函数的依赖项通过 `useCallback` 打包到新的 `getFetchUrl` 函数中,那么 **`useEffect` 就只需要依赖 `getFetchUrl` 这个函数,就实现了对 `count` 的间接依赖。**
换句话说,我们利用了 `useCallback` 将 `getFetchUrl` 函数抽到了 `useEffect` 外部。
### 为什么 `useCallback` 比 `componentDidUpdate` 更好用
回忆一下 Class Component 的模式,我们是如何在函数参数变化时进行重新取数的:
```js
class Parent extends Component {
state = {
count: 0,
step: 0
};
fetchData = () => {
const url =
"https://v?query=" + this.state.count + "&step=" + this.state.step;
};
render() {
return <Child fetchData={this.fetchData} count={count} step={step} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (
this.props.count !== prevProps.count &&
this.props.step !== prevProps.step // 别漏了!
) {
this.props.fetchData();
}
}
render() {
// ...
}
}
```
上面的代码经常用 Class Component 的人应该很熟悉,然而暴露的问题可不小。
我们需要理解 **`props.count` `props.step` 被 `props.fetchData` 函数使用了,因此在 `componentDidUpdate` 时,判断这两个参数发生了变化就触发重新取数。**
然而问题是,这种理解成本是不是过高了?如果父级函数 `fetchData` 不是我写的,在不读源码的情况下,我怎么知道它依赖了 `props.count` 与 `props.step` 呢?**更严重的是,如果某一天 `fetchData` 多依赖了 `params` 这个参数,下游函数将需要全部在 `componentDidUpdate` 覆盖到这个逻辑,否则 `params` 变化时将不会重新取数**。可以想象,这种方式维护成本巨大,甚至可以说几乎无法维护。
换成 Function Component 的思维吧!试着用上刚才提到的 `useCallback` 解决问题:
```js
function Parent() {
const [ count, setCount ] = useState(0);
const [ step, setStep ] = useState(0);
const fetchData = useCallback(() => {
const url = 'https://v/search?query=' + count + "&step=" + step;
}, [count, step])
return (
<Child fetchData={fetchData} />
)
}
function Child(props) {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
}
```
可以看出来,当 `fetchData` 的依赖变化后,按下保存键,[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 会自动补上更新后的依赖,**而下游的代码不需要做任何改变,下游只需要关心依赖了 `fetchData` 这个函数即可,至于这个函数依赖了什么,已经封装在 `useCallback` 后打包透传下来了。**
不仅解决了维护性问题,而且对于 **只要参数变化,就重新执行某逻辑,是特别适合用 `useEffect` 做的,使用这种思维思考问题会让你的代码更 “智能”,而使用分裂的生命周期进行思考,会让你的代码四分五裂,而且容易漏掉各种时机。**
`useEffect` 对业务的抽象非常方便,笔者举几个例子:
1. 依赖项是查询参数,那么 `useEffect` 内可以进行取数请求,那么只要查询参数变化了,列表就会自动取数刷新。注意我们将取数时机从触发端改成了接收端。
2. 当列表更新后,重新注册一遍拖拽响应事件。也是同理,依赖参数是列表,只要列表变化,拖拽响应就会重新初始化,这样我们可以放心的修改列表,而不用担心拖拽事件失效。
3. 只要数据流某个数据变化,页面标题就同步修改。同理,也不需要在每次数据变化时修改标题,而是通过 `useEffect` “监听” 数据的变化,这是一种 **“控制反转”** 的思维。
说了这么多,其本质还是利用了 `useCallback` 将函数独立抽离到 `useEffect` 外部。
那么进一步思考,**可以将函数抽离到整个组件的外部吗?**
这也是可以的,需要灵活运用自定义 Hooks 实现。
### 将函数抽到组件外部
以上面的 `fetchData` 函数为例,如果要抽到整个组件的外部,就不是利用 `useCallback` 做到了,而是利用自定义 Hooks 来做:
```js
function useFetch(count, step) {
return useCallback(() => {
const url = "https://v/search?query=" + count + "&step=" + step;
}, [count, step]);
}
```
可以看到,我们将 `useCallback` 打包搬到了自定义 Hook `useFetch` 中,那么函数中只需要一行代码就能实现一样的效果了:
```js
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const fetch = useFetch(count, step); // 封装了 useFetch
useEffect(() => {
fetch();
}, [fetch]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>setCount {count}</button>
<button onClick={() => setStep(c => c + 1)}>setStep {step}</button>
<button onClick={() => setOther(c => c + 1)}>setOther {other}</button>
</div>
);
}
```
随着使用越来越方便,我们可以将精力放到性能上。观察可以发现,`count` 与 `step` 都会频繁变化,每次变化就会导致 `useFetch` 中 `useCallback` 依赖的变化,进而导致重新生成函数。然而实际上这种函数是没必要每次都重新生成的,反复生成函数会造成大量性能损耗。
换一个例子就可以看得更清楚:
```js
function Parent(props) {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const drag = useDraggable(props.dom, count, step); // 封装了拖拽函数
useEffect(() => {
// dom 变化时重新实例化
drag()
}, [drag])
}
```
假设我们使用 [Sortablejs](https://github.com/SortableJS/Sortable) 对某个区域进行拖拽监听,这个函数每次都重复执行的性能损耗非常大,**然而这个函数内部可能因为仅仅要上报一些日志,所以依赖了没有实际被使用的 `count` `step` 变量:**
```js
function useDraggable(dom, count, step) {
return useCallback(() => {
// 上报日志
report(count, step);
// 对区域进行初始化,非常耗时
// ... 省略耗时代码
}, [dom, count, step]);
}
```
这种情况,函数的依赖就特别不合理。**虽然依赖变化应该触发函数重新执行,但如果函数重新执行的成本非常高,而依赖只是可有可无的点缀,得不偿失。**
### 利用 Ref 保证耗时函数依赖不变
一种办法是通过将依赖转化为 Ref:
```js
function useFetch(count, step) {
const countRef = useRef(count);
const stepRef = useRef(step);
useEffect(() => {
countRef.current = count;
stepRef.current = step;
});
return useCallback(() => {
const url =
"https://v/search?query=" + countRef.current + "&step=" + stepRef.current;
}, [countRef, stepRef]); // 依赖不会变,却能每次拿到最新的值
}
```
这种方式比较取巧,**将需要更新的区域与耗时区域分离,**再将需更新的内容通过 Ref 提供给耗时的区域,实现性能优化。
然而这样做对函数的改动成本比较高,有一种更通用的做法解决此类问题。
### 通用的自定义 Hooks 解决函数重新实例化问题
我们可以利用 `useRef` 创造一个自定义 Hook 代替 `useCallback`,**使其依赖的值变化时,回调不会重新执行,却能拿到最新的值!**
这个神奇的 Hook 写法如下:
```js
function useEventCallback(fn, dependencies) {
const ref = useRef(null);
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
```
再次体会到自定义 Hook 的无所不能。
首先看这一段:
```js
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
```
当 `fn` 回调函数变化时, `ref.current` 重新指向最新的 `fn` 这个逻辑中规中矩。重点是,当依赖 `dependencies` 变化时,也重新为 `ref.current` 赋值,此时 `fn` 内部的 `dependencies` 值是最新的,而下一段代码:
```js
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
```
又仅执行一次(ref 引用不会改变),所以每次都可以返回 `dependencies` 是最新的 `fn`,并且 `fn` 还不会重新执行。
假设我们对 `useEventCallback` 传入的回调函数称为 X,**则这段代码的含义,就是使每次渲染的闭包中,回调函数 X 总是拿到的总是最新 Rerender 闭包中的那个,所以依赖的值永远是最新的,而且函数不会重新初始化。**
> **React [官方不推荐使用此范式](https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback),因此对于这种场景,利用 `useReducer`,将函数通过 `dispatch` 中调用。** 还记得吗?`dispatch` 是一种可以绕过依赖的黑魔法,我们在 “什么是 useReducer” 小节提到过。
随着对 Function Component 的使用,你也渐渐关心到函数的性能了,这很棒。那么下一个重点自然是关注 Render 的性能。
### 用 memo 做 PureRender
在 Fucntion Component 中,Class Component 的 `PureComponent` 等价的概念是 `React.memo`,我们介绍一下 `memo` 的用法:
```js
const Child = memo((props) => {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
})
```
使用 `memo` 包裹的组件,会在自身重渲染时,对每一个 `props` 项进行浅对比,如果引用没有变化,就不会触发重渲染。所以 `memo` 是一种很棒的性能优化工具。
下面就介绍一个看似比 `memo` 难用,但真正理解后会发现,其实比 `memo` 更好用的渲染优化函数:`useMemo`。
### 用 useMemo 做局部 PureRender
相比 `React.memo` 这个异类,`React.useMemo` 可是正经的官方 Hook:
```js
const Child = (props) => {
useEffect(() => {
props.fetchData()
gitextract_q9gvtcfs/
├── .gitignore
├── .lintmdrc
├── .travis.yml
├── SQL/
│ ├── 231.SQL 入门.md
│ ├── 232.SQL 聚合查询.md
│ ├── 233.SQL 复杂查询.md
│ ├── 234.SQL CASE 表达式.md
│ ├── 235.SQL 窗口函数.md
│ └── 236.SQL grouping.md
├── TS 类型体操/
│ ├── 243.精读《Pick, Awaited, If...》.md
│ ├── 244.精读《Get return type, Omit, ReadOnly...》.md
│ ├── 245.精读《Promise.all, Replace, Type Lookup...》.md
│ ├── 246.精读《Permutation, Flatten, Absolute...》.md
│ ├── 247.精读《Diff, AnyOf, IsUnion...》.md
│ ├── 248.精读《MinusOne, PickByType, StartsWith...》.md
│ ├── 249.精读《ObjectEntries, Shift, Reverse...》.md
│ ├── 250.精读《Flip, Fibonacci, AllCombinations...》.md
│ ├── 251.精读《Trim Right, Without, Trunc...》.md
│ └── 252.精读《Unique, MapTypes, Construct Tuple...》.md
├── helper.js
├── package.json
├── readme.md
├── 前沿技术/
│ ├── 1.精读《js 模块化发展》.md
│ ├── 10.精读《Web Components 的困境》.md
│ ├── 100.精读《V8 引擎 Lazy Parsing》.md
│ ├── 101.精读《持续集成 vs 持续交付 vs 持续部署》.md
│ ├── 102.精读《Monorepo 的优势》.md
│ ├── 104.精读《Function Component 入门》.md
│ ├── 105.精读《What's new in javascript》.md
│ ├── 107.精读《Optional chaining》.md
│ ├── 109.精读《Vue3.0 Function API》.md
│ ├── 11.精读《前端调试技巧》.md
│ ├── 111.精读《前端未来展望》.md
│ ├── 112.精读《源码学习》.md
│ ├── 113.精读《Nodejs V12》.md
│ ├── 117.精读《Tableau 探索式模型》.md
│ ├── 118.精读《使用 css 变量生成颜色主题》.md
│ ├── 119.精读《前端深水区》.md
│ ├── 12.精读《React 高阶组件》.md
│ ├── 120.精读《React Hooks 最佳实践》.md
│ ├── 121.精读《前端与 BI》.md
│ ├── 123.精读《用 Babel 创造自定义 JS 语法》.md
│ ├── 124.精读《用 css grid 重新思考布局》.md
│ ├── 125.精读《深度学习 - 函数式之美》.md
│ ├── 126.精读《Nuxtjs》.md
│ ├── 127.精读《React Conf 2019 - Day1》.md
│ ├── 129.精读《React Conf 2019 - Day2》.md
│ ├── 13.精读《This 带来的困惑》.md
│ ├── 132.精读《正交的 React 组件》.md
│ ├── 133.精读《寻找框架设计的平衡点》.md
│ ├── 134.精读《我在阿里数据中台大前端》.md
│ ├── 138.精读《精通 console.log》.md
│ ├── 139.精读《手写 JSON Parser》.md
│ ├── 14.精读《架构设计之 DCI》.md
│ ├── 140.精读《结合 React 使用原生 Drag Drop API》.md
│ ├── 141.精读《useRef 与 createRef 的区别》.md
│ ├── 142.精读《如何做好 CodeReview》.md
│ ├── 143.精读《Suspense 改变开发方式》.md
│ ├── 144.精读《Webpack5 新特性 - 模块联邦》.md
│ ├── 145.精读《React Router v6》.md
│ ├── 146.精读《React Hooks 数据流》.md
│ ├── 147. 精读《@types react 值得注意的 TS 技巧》.md
│ ├── 148. 精读《React Error Boundaries》.md
│ ├── 149. 精读《React 性能调试》.md
│ ├── 15.精读《TC39 与 ECMAScript 提案》.md
│ ├── 150. 精读《Deno 1.0 你需要了解的》.md
│ ├── 152. 精读《recoil》.md
│ ├── 153. 精读《snowpack》.md
│ ├── 154. 精读《用 React 做按需渲染》.md
│ ├── 157. 精读《如何比较 Object 对象》.md
│ ├── 158. 精读《Typescript 4》.md
│ ├── 159. 精读《对低代码搭建的理解》.md
│ ├── 16.精读《CSS Animations vs Web Animations API》.md
│ ├── 160. 精读《函数缓存》.md
│ ├── 161.精读《可视化搭建思考 - 富文本搭建》.md
│ ├── 162.精读《Tasks, microtasks, queues and schedules》.md
│ ├── 163.精读《Spring 概念》.md
│ ├── 164.精读《数据搭建引擎 bi-designer API-设计器》.md
│ ├── 165.精读《数据搭建引擎 bi-designer API-组件》.md
│ ├── 166.精读《BI 搭建 - 筛选条件》.md
│ ├── 17.精读《如何安全地使用 React context》.md
│ ├── 18.精读《设计完美的日期选择器》.md
│ ├── 19.精读《最佳前端面试题》及面试官技巧.md
│ ├── 190.精读《DOM diff 原理详解》.md
│ ├── 191.精读《高性能表格》.md
│ ├── 192.精读《DOM diff 最长上升子序列》.md
│ ├── 193.精读《React Server Component》.md
│ ├── 194.精读《算法基础数据结构》.md
│ ├── 195.精读《新一代前端构建工具对比》.md
│ ├── 196.精读《前端职业规划 - 2021 年》.md
│ ├── 197.精读《低代码逻辑编排》.md
│ ├── 2.精读《模态框的最佳实践》.md
│ ├── 20.精读《Nestjs》文档.md
│ ├── 202.精读《React 18》.md
│ ├── 204.精读《默认、命名导出的区别》.md
│ ├── 205.精读《JS with 语法》.md
│ ├── 206.精读《一种 Hooks 数据流管理方案》.md
│ ├── 207.精读《Typescript infer 关键字》.md
│ ├── 208.精读《Typescript 4.4》.md
│ ├── 209.精读《捕获所有异步 error》.md
│ ├── 21.精读《Web fonts: when you need them, when you don’t》.md
│ ├── 210.精读《class static block》.md
│ ├── 211.精读《Microsoft Power Fx》.md
│ ├── 212.精读《可维护性思考》.md
│ ├── 213.精读《Prisma 的使用》.md
│ ├── 214.精读《web streams》.md
│ ├── 215.精读《什么是 LOD 表达式》.md
│ ├── 216.精读《15 大 LOD 表达式 - 上》.md
│ ├── 217.精读《15 大 LOD 表达式 - 下》.md
│ ├── 218.精读《Rust 是 JS 基建的未来》.md
│ ├── 219.精读《深入了解现代浏览器一》.md
│ ├── 22.精读《V8 引擎特性带来的的 JS 性能变化》.md
│ ├── 220.精读《深入了解现代浏览器二》.md
│ ├── 221.精读《深入了解现代浏览器三》.md
│ ├── 222.精读《深入了解现代浏览器四》.md
│ ├── 223.精读《Records & Tuples 提案》.md
│ ├── 224.精读《Records & Tuples for React》.md
│ ├── 225.精读《Excel JS API》.md
│ ├── 226.精读《2021 前端新秀回顾》.md
│ ├── 228.精读《pipe operator for JavaScript》.md
│ ├── 23.精读《API 设计原则》.md
│ ├── 230.精读《对 Markdown 的思考》.md
│ ├── 237.精读《Typescript 4.5-4.6 新特性》.md
│ ├── 238.精读《不再需要 JS 做的 5 件事》.md
│ ├── 239.精读《JS 数组的内部实现》.md
│ ├── 24.精读《现代 JavaScript 概览》.md
│ ├── 240.精读《React useEvent RFC》.md
│ ├── 242.精读《web reflow》.md
│ ├── 25.精读《null >= 0?》.md
│ ├── 253.精读《pnpm》.md
│ ├── 254.精读《对前端架构的理解 - 分层与抽象》.md
│ ├── 255.精读《SolidJS》.md
│ ├── 256.精读《依赖注入简介》.md
│ ├── 257.精读《State of CSS 2022》.md
│ ├── 258.精读《proposal-extractors》.md
│ ├── 259.精读《Headless 组件用法与原理》.md
│ ├── 26.精读《加密媒体扩展》.md
│ ├── 260.精读《如何为 TS 类型写单测》.md
│ ├── 261.精读《Rest vs Spread 语法》.md
│ ├── 262.精读《迭代器 Iterable》.md
│ ├── 263.精读《我们为何弃用 css-in-js》.md
│ ├── 264.精读《维护好一个复杂项目》.md
│ ├── 265.精读《磁贴布局 - 功能分析》.md
│ ├── 266.精读《磁贴布局 - 功能实现》.md
│ ├── 267.精读《磁贴布局 - 性能优化》.md
│ ├── 27.精读《css-in-js 杀鸡用牛刀》.md
│ ├── 277.精读《利用 GPT 解读 PDF》.md
│ ├── 28.精读《2017 前端性能优化备忘录》.md
│ ├── 281.精读《自由 + 磁贴混合布局》.md
│ ├── 282.精读《自由布局吸附线的实现》.md
│ ├── 287.精读《VisActor 数据可视化工具》.md
│ ├── 29.精读《JS 中的内存管理》.md
│ ├── 3.精读《前后端渲染之争》.md
│ ├── 30.精读《Javascript 事件循环与异步》.md
│ ├── 31.精读《我不再使用高阶组件》.md
│ ├── 32.精读《React Router4.0 进阶概念》.md
│ ├── 33.精读《30 行 js 代码创建神经网络》.md
│ ├── 34.精读《React 代码整洁之道》.md
│ ├── 35.精读《dob - 框架实现》.md
│ ├── 36.精读《When You “Git” in Trouble- a Version Control Story》.md
│ ├── 37.精读《how we position and what we compare》.md
│ ├── 38.精读《dob - 框架使用》.md
│ ├── 39.精读《全链路体验浏览器挖矿》.md
│ ├── 4.精读《AsyncAwait 优越之处》.md
│ ├── 40.精读《初探 Reason 与 GraphQL》.md
│ ├── 41.精读《Ant Design 3.0 背后的故事》.md
│ ├── 42.精读《前端数据流哲学》.md
│ ├── 43.精读《增强现实与可视化》.md
│ ├── 44.精读《Rekit Studio》.md
│ ├── 45.精读《React's new Context API》.md
│ ├── 46.精读《react-rxjs》.md
│ ├── 47.精读《webpack4.0 升级指南》.md
│ ├── 49.精读《Compilers are the New Frameworks》.md
│ ├── 5.精读《民工叔单页数据流方案》.md
│ ├── 50.精读《快速上手构建 ARKit 应用》.md
│ ├── 51.精读《Elements of Web Dev》.md
│ ├── 52.精读《图解 ES 模块》.md
│ ├── 53.精读《插件化思维》.md
│ ├── 54.精读《在浏览器运行 serverRender》.md
│ ├── 55.精读《async await 是把双刃剑》.md
│ ├── 56.精读《重新思考 Redux》.md
│ ├── 57.精读《现代 js 框架存在的根本原因》.md
│ ├── 58.精读《Typescript2.0 - 2.9》.md
│ ├── 59.精读《如何利用 Nodejs 监听文件夹》.md
│ ├── 6.精读《JavaScript 错误堆栈处理》.md
│ ├── 60.精读《如何在 nodejs 使用环境变量》.md
│ ├── 61.精读《React 八种条件渲染》.md
│ ├── 62.精读《JS 引擎基础之 Shapes and Inline Caches》.md
│ ├── 63.精读《React 的多态性》.md
│ ├── 68.精读《衡量用户体验》.md
│ ├── 69.精读《SQL vs Flux》.md
│ ├── 7.精读《请停止 css-in-js 的行为》.md
│ ├── 72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》.md
│ ├── 74.精读《12 个评估 JS 库你需要关心的事》.md
│ ├── 76.精读《谈谈 Web Workers》.md
│ ├── 77.精读《用 Reduce 实现 Promise 串行执行》.md
│ ├── 79.精读《React Hooks》.md
│ ├── 8.精读《入坑 React 前没有人会告诉你的事》.md
│ ├── 80.精读《怎么用 React Hooks 造轮子》.md
│ ├── 81.精读《使用 CSS 属性选择器》.md
│ ├── 83.精读《React16 新特性》.md
│ ├── 84.精读《Typescript 3.2 新特性》.md
│ ├── 86.精读《国际化布局 - Logical Properties》.md
│ ├── 87.精读《setState 做了什么》.md
│ ├── 88.精读《Caches API》.md
│ ├── 89.精读《如何编译前端项目与组件》.md
│ ├── 9.精读《Immutable 结构共享》.md
│ ├── 91.精读《正则 ES2018》.md
│ ├── 94.精读《Serverless 给前端带来了什么》.md
│ ├── 95.精读《Function VS Class 组件》.md
│ ├── 96.精读《useEffect 完全指南》.md
│ ├── 97.精读《编写有弹性的组件》.md
│ └── 99.精读《Scheduling in React》.md
├── 可视化搭建/
│ ├── 268.如何抽象可视化搭建.md
│ ├── 269.组件注册与画布渲染.md
│ ├── 270.画布与组件元信息数据流.md
│ ├── 271.可视化搭建内置 API.md
│ ├── 272.容器组件设计.md
│ ├── 273.组件值与联动.md
│ ├── 274.定义联动协议.md
│ ├── 275.组件值校验.md
│ ├── 276.keepAlive 模式.md
│ ├── 278.ComponentLoader 与动态组件.md
│ ├── 279.自动批处理与冻结.md
│ └── 280.场景实战.md
├── 商业思考/
│ ├── 103.精读《为什么专家不再关心技术细节》.md
│ ├── 106.精读《数据之上·智慧之光 - 2018》.md
│ ├── 108.精读《智能商业》.md
│ ├── 114.精读《谁在世界中心》.md
│ ├── 115.精读《Tableau 入门》.md
│ ├── 116.精读《刷新》.md
│ ├── 131.精读《从 0 到 1》.md
│ ├── 135.精读《极客公园 IFX - 上》.md
│ ├── 136.精读《极客公园 IFX - 下》.md
│ ├── 137.精读《当我在分享的时候,我在做什么?》.md
│ └── 90.精读《极客公园 2019》.md
├── 数学之美/
│ └── 296.手动算根号.md
├── 数据技术专家能力模型.md
├── 机器学习/
│ ├── 291.机器学习简介: 寻找函数的艺术.md
│ ├── 292.万能近似定理: 逼近任何函数的理论.md
│ ├── 293.实现万能近似函数: 神经网络的架构设计.md
│ ├── 294.反向传播: 揭秘神经网络的学习机制.md
│ └── 295.完整实现神经网络: 实战演练.md
├── 源码解读/
│ ├── 110.精读《Inject Instance 源码》.md
│ ├── 122.精读《robot 源码 - 有限状态机》.md
│ ├── 128.精读《Hooks 取数 - swr 源码》.md
│ ├── 130.精读《unstated 与 unstated-next 源码》.md
│ ├── 151. 精读《@umijs use-request》源码.md
│ ├── 155. 精读《use-what-changed 源码》.md
│ ├── 156. 精读《react-intersection-observer 源码》.md
│ ├── 227. 精读《zustand 源码》.md
│ ├── 229.精读《vue-lit 源码》.md
│ ├── 241.精读《react-snippets - Router 源码》.md
│ ├── 48.精读《Immer.js》源码.md
│ ├── 73.精读《sqorn 源码》.md
│ ├── 75.精读《Epitath 源码 - renderProps 新用法》.md
│ ├── 82.精读《Htm - Hyperscript 源码》.md
│ ├── 92.精读《React PowerPlug 源码》.md
│ ├── 93.精读《syntax-parser 源码》.md
│ └── 98.精读《react-easy-state 源码》.md
├── 生活/
│ └── 290.个人养老金利与弊.md
├── 算法/
│ ├── 198.精读《算法 - 动态规划》.md
│ ├── 199.精读《算法 - 滑动窗口》.md
│ ├── 200.精读《算法 - 回溯》.md
│ ├── 201.精读《算法 - 二叉树》.md
│ ├── 203.精读《算法 - 二叉搜索树》.md
│ ├── 283.精读《算法题 - 通配符匹配》.md
│ ├── 284.精读《算法题 - 统计可以被 K 整除的下标对数目》.md
│ ├── 285.精读《算法题 - 最小覆盖子串》.md
│ ├── 286.精读《算法题 - 地下城游戏》.md
│ ├── 288.精读《算法题 - 编辑距离》.md
│ └── 289.精读《算法题 - 二叉树中的最大路径和》.md
├── 编译原理/
│ ├── 64.精读《手写 SQL 编译器 - 词法分析》.md
│ ├── 65.精读《手写 SQL 编译器 - 文法介绍》.md
│ ├── 66.精读《手写 SQL 编译器 - 语法分析》.md
│ ├── 67.精读《手写 SQL 编译器 - 回溯》.md
│ ├── 70.精读《手写 SQL 编译器 - 语法树》.md
│ ├── 71.精读《手写 SQL 编译器 - 错误提示》.md
│ ├── 78.精读《手写 SQL 编译器 - 性能优化之缓存》.md
│ └── 85.精读《手写 SQL 编译器 - 智能提示》.md
└── 设计模式/
├── 167.精读《设计模式 - Abstract Factory 抽象工厂》.md
├── 168.精读《设计模式 - Builder 生成器》.md
├── 169.精读《设计模式 - Factory Method 工厂方法》.md
├── 170.精读《设计模式 - Prototype 原型模式》.md
├── 171.精读《设计模式 - Singleton 单例模式》.md
├── 172.精读《设计模式 - Adapter 适配器模式》.md
├── 173.精读《设计模式 - Bridge 桥接模式》.md
├── 174.精读《设计模式 - Composite 组合模式》.md
├── 175.精读《设计模式 - Decorator 装饰器模式》.md
├── 176.精读《设计模式 - Facade 外观模式》.md
├── 177.精读《设计模式 - Flyweight 享元模式》.md
├── 178.精读《设计模式 - Proxy 代理模式》.md
├── 179.精读《设计模式 - Chain of Responsibility 职责链模式》.md
├── 180.精读《设计模式 - Command 命令模式》.md
├── 181.精读《设计模式 - Interpreter 解释器模式》.md
├── 182.精读《设计模式 - Iterator 迭代器模式》.md
├── 183.精读《设计模式 - Mediator 中介者模式》.md
├── 184.精读《设计模式 - Memoto 备忘录模式》.md
├── 185.精读《设计模式 - Observer 观察者模式》.md
├── 186.精读《设计模式 - State 状态模式》.md
├── 187.精读《设计模式 - Strategy 策略模式》.md
├── 188.精读《设计模式 - Template Method 模版模式》.md
└── 189.精读《设计模式 - Visitor 访问者模式》.md
Condensed preview — 303 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,001K chars).
[
{
"path": ".gitignore",
"chars": 24,
"preview": "/node_modules\n/yarn.lock"
},
{
"path": ".lintmdrc",
"chars": 99,
"preview": "{\n \"excludeFiles\": [],\n \"rules\": {\n \"no-long-code\": 0,\n \"no-trailing-punctuation\": 0\n }\n}\n"
},
{
"path": ".travis.yml",
"chars": 96,
"preview": "language: node_js\nnode_js:\n - \"10\"\nbefore_install:\n - npm i -g lint-md-cli\nscript: lint-md ./\n"
},
{
"path": "SQL/231.SQL 入门.md",
"chars": 4909,
"preview": "本系列是 SQL 系列的开篇,介绍一些宏观与基础的内容。\n\n## SQL 是什么?\n\nSQL 是一种结构化查询语言,用于管理关系型数据库,我们 90% 接触的都是查询语法,但其实它包含完整的增删改查和事物处理功能。\n\n## 声明式特性\n\nS"
},
{
"path": "SQL/232.SQL 聚合查询.md",
"chars": 4023,
"preview": "SQL 为什么要支持聚合查询呢?\n\n这看上去是个幼稚的问题,但我们还是一步步思考一下。数据以行为粒度存储,最简单的 SQL 语句是 `select * from test`,拿到的是整个二维表明细,但仅做到这一点远远不够,出于以下两个目的,"
},
{
"path": "SQL/233.SQL 复杂查询.md",
"chars": 4149,
"preview": "SQL 复杂查询指的就是子查询。\n\n为什么子查询叫做复杂查询呢?因为子查询相当于查询嵌套查询,因为嵌套导致复杂度几乎可以被无限放大(无限嵌套),因此叫复杂查询。下面是一个最简单的子查询例子:\n\n```sql\nSELECT pv FROM ("
},
{
"path": "SQL/234.SQL CASE 表达式.md",
"chars": 2898,
"preview": "CASE 表达式分为简单表达式与搜索表达式,其中搜索表达式可以覆盖简单表达式的全部能力,我也建议只写搜索表达式,而不要写简单表达式。\n\n简单表达式:\n\n```sql\nSELECT CASE city\nWHEN '北京' THEN 1\nWHE"
},
{
"path": "SQL/235.SQL 窗口函数.md",
"chars": 2961,
"preview": "窗口函数形如:\n\n```sql\n表达式 OVER (PARTITION BY 分组字段 ORDER BY 排序字段)\n```\n\n有两个能力:\n\n1. 当表达式为 `rank()` `dense_rank()` `row_number()` "
},
{
"path": "SQL/236.SQL grouping.md",
"chars": 3478,
"preview": "SQL grouping 解决 OLAP 场景总计与小计问题,其语法分为几类,但要解决的是同一个问题:\n\nROLLUP 与 CUBE 是封装了规则的 GROUPING SETS,而 GROUPING SETS 则是最原始的规则。\n\n为了方便"
},
{
"path": "TS 类型体操/243.精读《Pick, Awaited, If...》.md",
"chars": 9521,
"preview": "TS 强类型非常好用,但在实际运用中,免不了遇到一些难以描述,反复看官方文档也解决不了的问题,至今为止也没有任何一篇文档,或者一套教材可以解决所有犄角旮旯的类型问题。为什么会这样呢?因为 TS 并不是简单的注释器,而是一门图灵完备的语言,所"
},
{
"path": "TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md",
"chars": 7793,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 1~8 题。\n\n## 精读\n\n##"
},
{
"path": "TS 类型体操/245.精读《Promise.all, Replace, Type Lookup...》.md",
"chars": 5975,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 9~16 题。\n\n## 精读\n\n#"
},
{
"path": "TS 类型体操/246.精读《Permutation, Flatten, Absolute...》.md",
"chars": 7161,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 17~24 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/247.精读《Diff, AnyOf, IsUnion...》.md",
"chars": 8188,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 25~32 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/248.精读《MinusOne, PickByType, StartsWith...》.md",
"chars": 10207,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 33~40 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/249.精读《ObjectEntries, Shift, Reverse...》.md",
"chars": 7730,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 41~48 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/250.精读《Flip, Fibonacci, AllCombinations...》.md",
"chars": 9906,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 49~56 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/251.精读《Trim Right, Without, Trunc...》.md",
"chars": 4491,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 57~62 题。\n\n## 精读\n\n"
},
{
"path": "TS 类型体操/252.精读《Unique, MapTypes, Construct Tuple...》.md",
"chars": 9272,
"preview": "解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 63~68 题。\n\n## 精读\n\n"
},
{
"path": "helper.js",
"chars": 595,
"preview": "/**\n * 发布辅助脚本\n * @author 黄子毅\n */\n\nconst fs = require(\"fs\");\n\nconst dirs = [\n \"前沿技术\",\n \"TS 类型体操\",\n \"设计模式\",\n \"编译原理\",\n "
},
{
"path": "package.json",
"chars": 610,
"preview": "{\n \"name\": \"weekly\",\n \"version\": \"1.0.0\",\n \"description\": \"前端界的好文精读,每周更新!\",\n \"main\": \"index.js\",\n \"scripts\": {\n "
},
{
"path": "readme.md",
"chars": 44883,
"preview": "# 前端精读\n\n<a href=\"https://travis-ci.org/ascoders/weekly\">\n <img src=\"https://travis-ci.org/ascoders/weekly.svg?branch=v2"
},
{
"path": "前沿技术/1.精读《js 模块化发展》.md",
"chars": 7137,
"preview": "这次是前端精读期刊与大家第一次正式碰面,我们每周会精读并分析若干篇精品好文,试图讨论出结论性观点。没错,我们试图通过观点的碰撞,争做无主观精品好文的意见领袖。\n\n我是这一期的主持人 —— [黄子毅](https://github.com/a"
},
{
"path": "前沿技术/10.精读《Web Components 的困境》.md",
"chars": 4873,
"preview": "本期精读的文章是:[The broken promise of Web Components\n](https://dmitriid.com/blog/2017/03/the-broken-promise-of-web-components/"
},
{
"path": "前沿技术/100.精读《V8 引擎 Lazy Parsing》.md",
"chars": 4686,
"preview": "# 1. 引言\n\n本周精读的文章是 [V8 引擎 Lazy Parsing](https://v8.dev/blog/preparser),看看 V8 引擎为了优化性能,做了怎样的尝试吧!\n\n这篇文章介绍的优化技术叫 [preparser]"
},
{
"path": "前沿技术/101.精读《持续集成 vs 持续交付 vs 持续部署》.md",
"chars": 5217,
"preview": "# 一、摘要\n\n相信大家以前应该接触过持续集成(Continuous integration)持续交付(continuous delivery)持续发布(continuous deployment)的概念,下面我们来说说三者的差异以及团队如"
},
{
"path": "前沿技术/102.精读《Monorepo 的优势》.md",
"chars": 5043,
"preview": "# 1. 引言\n\n本周精读的文章是 [The many Benefits of Using a Monorepo](https://pspdfkit.com/blog/2019/benefits-of-a-monorepo/)。\n\n现在介绍"
},
{
"path": "前沿技术/104.精读《Function Component 入门》.md",
"chars": 30465,
"preview": "# 1. 引言\n\n如果你在使用 React 16,可以尝试 Function Component 风格,享受更大的灵活性。但在尝试之前,最好先阅读本文,对 Function Component 的思维模式有一个初步认识,防止因思维模式不同步"
},
{
"path": "前沿技术/105.精读《What's new in javascript》.md",
"chars": 8563,
"preview": "# 1. 引言\n\n本周精读的内容是:[Google I/O 19](https://www.youtube.com/watch?v=c0oy0vQKEZE)。\n\n2019 年 Google I/O 介绍了一些激动人心的 JS 新特性,这些特"
},
{
"path": "前沿技术/107.精读《Optional chaining》.md",
"chars": 10705,
"preview": "# 1. 引言\n\n备受开发者喜爱的特性 [Optional chaining](https://github.com/tc39/proposal-optional-chaining) 在 2019.6.5 进入了 stage2,让我们详细读"
},
{
"path": "前沿技术/109.精读《Vue3.0 Function API》.md",
"chars": 9618,
"preview": "# 1. 引言\n\nVue 3.0 的发布引起了轩然大波,让我们解读下它的 [function api RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/000"
},
{
"path": "前沿技术/11.精读《前端调试技巧》.md",
"chars": 4763,
"preview": "本期精读的文章是:[debugging-tips-tricks](https://css-tricks.com/debugging-tips-tricks/?utm_source=javascriptweekly&utm_medium=em"
},
{
"path": "前沿技术/111.精读《前端未来展望》.md",
"chars": 6422,
"preview": "# 1. 引言\n\n前端展望的文章越来越不好写了,随着前端发展的深入,需要拥有非常宽广的视野与格局才能看清前端的未来。\n\n笔者根据自身经验,结合下面几篇文章发表一些总结与感悟:\n\n- [A Look at JavaScript’s Futur"
},
{
"path": "前沿技术/112.精读《源码学习》.md",
"chars": 9497,
"preview": "# 1. 引言\n\n[javascript-knowledge-reading-source-code](https://www.smashingmagazine.com/2019/07/javascript-knowledge-readin"
},
{
"path": "前沿技术/113.精读《Nodejs V12》.md",
"chars": 3429,
"preview": "# 1. 引言\n\nNode12 发布有几个月了,让我们跟随 [Nodejs 12](https://blog.logrocket.com/node-js-12/) 一起看看 Node12 带来了哪些改变。\n\n# 2. 概述\n\nNode12 "
},
{
"path": "前沿技术/117.精读《Tableau 探索式模型》.md",
"chars": 27887,
"preview": "# 1. 引言\n\nTableau 探索式分析功能非常强大,各种功能组合似乎有着无限的可能性。\n\n今天笔者会分析这种探索式模型解题思路,一起看看这种探索式分析功能是如何做到的。\n\n# 2. 精读\n\n要掌握探索式分析,先要掌握探索式分析背后的思"
},
{
"path": "前沿技术/118.精读《使用 css 变量生成颜色主题》.md",
"chars": 3957,
"preview": "> 作者:五灵\n\n本周工作中遇到类似颜色主题的问题,在查资料的时候,看到这个视频,觉得讲得很清楚,而且趣味性丰富,所以想拿出来讲讲这个很有意思的主题。\n\n视频链接: [CSSconf EU 2018 | Dag-Inge Aas & Ida"
},
{
"path": "前沿技术/119.精读《前端深水区》.md",
"chars": 3001,
"preview": "> 作者:五灵\n\n## 简介\n\n其实关于前端深水区的讨论,已经有了很多,也有了很多相关的文章。我也想借这篇关于深水区的讨论文章,讲一下自己对于深水区的理解。\n原文链接:[技术路线:前端开发已进入深水区](https://www.yuque."
},
{
"path": "前沿技术/12.精读《React 高阶组件》.md",
"chars": 6202,
"preview": "本期精读文章是:[React Higher Order Components in depth](https://medium.com/@franleplant/react-higher-order-components-in-depth"
},
{
"path": "前沿技术/120.精读《React Hooks 最佳实践》.md",
"chars": 7279,
"preview": "## 简介\n\nReact 16.8 于 2019.2 正式发布,这是一个能提升代码质量和开发效率的特性,笔者就抛砖引玉先列出一些实践点,希望得到大家进一步讨论。\n\n然而需要理解的是,没有一个完美的最佳实践规范,对一个高效团队来说,稳定的规范"
},
{
"path": "前沿技术/121.精读《前端与 BI》.md",
"chars": 5648,
"preview": "## 简介\n\n商业智能(Business Intelligence)简称 BI,即通过数据挖掘与分析找到商业洞察,助力商业成功。\n\n一个完整的 BI 链路包含数据采集、数据清洗、数据挖掘、数据展现,其本质是对数据进行多维分析。前端的主要工作"
},
{
"path": "前沿技术/123.精读《用 Babel 创造自定义 JS 语法》.md",
"chars": 12815,
"preview": "## 1 引言\n\n在写这次精读之前,我想谈谈前端精读可以为读者带来哪些价值,以及如何评判这些价值。\n\n前端精读已经写到第 123 篇了,大家已经不必担心它突然停止更新,因为我已养成每周写一篇文章的习惯,而读者也养成了每周看一篇的习惯。所以我"
},
{
"path": "前沿技术/124.精读《用 css grid 重新思考布局》.md",
"chars": 8445,
"preview": "## 1 引言\n\nFlex 与 Grid 相比就像功能键盘和触摸屏。触摸屏的控制力相比功能键盘来说就像是降维打击,因为功能键盘只能上下左右控制(x、y 轴),而触摸屏打破了布局障碍,直接从(z 轴)触达,这样 **无论 UI 内部布局再复杂"
},
{
"path": "前沿技术/125.精读《深度学习 - 函数式之美》.md",
"chars": 4476,
"preview": "## 1 引言\n\n函数式语言在深度学习领域应用很广泛,因为函数式与深度学习模型的契合度很高,[The Beauty of Functional Languages in Deep Learning — Clojure and Haskell"
},
{
"path": "前沿技术/126.精读《Nuxtjs》.md",
"chars": 6853,
"preview": "## 1 引言\n\n[Nuxt](https://github.com/nuxt/nuxt.js) 是基于 Vue 的前端开发框架,这次我们通过 [Introduction toNuxtJS](https://www.youtube.com/"
},
{
"path": "前沿技术/127.精读《React Conf 2019 - Day1》.md",
"chars": 15257,
"preview": "## 1 引言\n\n[React Conf 2019](https://www.youtube.com/watch?v=RCiccdQObpo) 在今年 10 月份举办,内容质量还是一如既往的高,如果想进一步学习前端或者 React,这个大会"
},
{
"path": "前沿技术/129.精读《React Conf 2019 - Day2》.md",
"chars": 15652,
"preview": "## 1 引言\n\n这是继 [精读《React Conf 2019 - Day1》](https://github.com/dt-fe/weekly/blob/v2/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%2"
},
{
"path": "前沿技术/13.精读《This 带来的困惑》.md",
"chars": 5446,
"preview": "# 1 引言\n\n<img src=\"https://img.alicdn.com/imgextra/i2/O1CN014VGV7a1x3ILYqK9OD_!!6000000006387-2-tps-1024-732.png\" width=\""
},
{
"path": "前沿技术/132.精读《正交的 React 组件》.md",
"chars": 6000,
"preview": "## 1 引言\n\n搭配了合适的设计模式的代码,才可拥有良好的可维护性,[The Benefits of Orthogonal React Components](https://dmitripavlutin.com/orthogonal-r"
},
{
"path": "前沿技术/133.精读《寻找框架设计的平衡点》.md",
"chars": 4820,
"preview": "## 1 引言\n\n[尤雨溪](https://github.com/yyx990803) 在 2019 JSConf 的分享 [Seeking the Balance in Framework Design](https://www.you"
},
{
"path": "前沿技术/134.精读《我在阿里数据中台大前端》.md",
"chars": 4077,
"preview": "## 1 引言\n\n当下互联网行业里面最流行的就是 ABC:\n\n> A: AI 人工智能 B: BIG DATA C: CLOUD\n\n而阿里经济体中的 ABC,其中的 BIG DATA,即是我们 DT https://dt.alibab"
},
{
"path": "前沿技术/138.精读《精通 console.log》.md",
"chars": 3545,
"preview": "## 1 引言\n\n本周精读的文章是 [Mastering JS console.log like a Pro](https://medium.com/javascript-in-plain-english/mastering-js-cons"
},
{
"path": "前沿技术/139.精读《手写 JSON Parser》.md",
"chars": 8264,
"preview": "## 1 引言\n\n`JSON.parse` 是浏览器内置的 API,但如果面试官让你实现一个怎么办?好在有人已经帮忙做了这件事,本周我们一起精读这篇 [JSON Parser with Javascript](https://lihauta"
},
{
"path": "前沿技术/14.精读《架构设计之 DCI》.md",
"chars": 7142,
"preview": "本期精读文章是:[The DCI Architecture](http://www.artima.com/articles/dci_vision.html)\n\n# 1 引言\n\n随着前端 ES6 ES7 的一路前行, 我们大前端借鉴和引进了各"
},
{
"path": "前沿技术/140.精读《结合 React 使用原生 Drag Drop API》.md",
"chars": 5681,
"preview": "## 1 引言\n\n拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。\n\n结合 [How To Use The HTML Drag-And-"
},
{
"path": "前沿技术/141.精读《useRef 与 createRef 的区别》.md",
"chars": 3030,
"preview": "## 1 引言\n\n`useRef` 是常用的 API,但还有一个 `createRef` 的 API,你知道他们的区别吗?通过 [React.useRef and React.createRef: The Difference](https"
},
{
"path": "前沿技术/142.精读《如何做好 CodeReview》.md",
"chars": 3774,
"preview": "## 1 引言\n\n任何软件都是协同开发的,所以 CodeReview 非常重要,它可以帮助你减少代码质量问题,提高开发效率,提升稳定性,同时还能保证软件架构的稳定性,防止代码结构被恶意破坏导致难以维护。\n\n所以 CodeReview 机制是"
},
{
"path": "前沿技术/143.精读《Suspense 改变开发方式》.md",
"chars": 7868,
"preview": "## 1 引言\n\n很多人都用过 React Suspense,但如果你认为它只是配合 React.lazy 实现异步加载的蒙层,就理解的太浅了。实际上,React Suspense 改变了开发规则,要理解这一点,需要作出思想上的改变。\n\n我"
},
{
"path": "前沿技术/144.精读《Webpack5 新特性 - 模块联邦》.md",
"chars": 4007,
"preview": "## 1 引言\n\n先说结论:Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!\n\n我们知道 Webpack 可以通过 DL"
},
{
"path": "前沿技术/145.精读《React Router v6》.md",
"chars": 8280,
"preview": "## 1 引言\n\n[React Router v6](https://github.com/ReactTraining/react-router) alpha 版本发布了,本周通过 [A Sneak Peek at React Router"
},
{
"path": "前沿技术/146.精读《React Hooks 数据流》.md",
"chars": 7664,
"preview": "## 1 引言\n\nReact Hooks 渐渐被国内前端团队所接受,但基于 Hooks 的数据流方案却还未固定,我们有 “100 种” 类似的选择,却各有利弊,让人难以取舍。\n\n本周笔者就深入谈一谈对 Hooks 数据流的理解,相信读完文章"
},
{
"path": "前沿技术/147. 精读《@types react 值得注意的 TS 技巧》.md",
"chars": 4329,
"preview": "## 1 引言\n\n从 [@types/react](https://unpkg.com/browse/@types/react@16.9.34/index.d.ts) 源码中挖掘一些 Typescript 使用技巧吧。\n\n## 2 精读\n\n"
},
{
"path": "前沿技术/148. 精读《React Error Boundaries》.md",
"chars": 3304,
"preview": "## 1 引言\n\nError Boundaries 是 React16 提出来用来捕获渲染时错误的概念,今天我们一起读一读 [A Simple Guide to Error Boundaries in React](https://alli"
},
{
"path": "前沿技术/149. 精读《React 性能调试》.md",
"chars": 5329,
"preview": "## 1 引言\n\n在数据中台做 BI 工具经常面对海量数据的渲染处理,除了组件本身性能优化之外,经常要排查整体页面性能瓶颈点,尤其是维护一些性能做得并不好的旧代码时。\n\nReact 性能调试是面对这种问题的必修课,借助 [Profiling"
},
{
"path": "前沿技术/15.精读《TC39 与 ECMAScript 提案》.md",
"chars": 22946,
"preview": "本期精读文章是:[TC39, ECMAScript, and the Future of JavaScript]( https://ponyfoo.com/articles/tc39-ecmascript-proposals-future-"
},
{
"path": "前沿技术/150. 精读《Deno 1.0 你需要了解的》.md",
"chars": 8819,
"preview": "## 1 引言\n\nDeno 是什么?Deno 和 Node 有什么关系?Deno 和我有什么关系?\n\nDeno 将于 2020-05-13 发布 1.0,如果你还有上面的疑惑,可以和我一起通过 [Deno 1.0: What you nee"
},
{
"path": "前沿技术/152. 精读《recoil》.md",
"chars": 6931,
"preview": "## 1 引言\n\n[Recoil](https://recoiljs.org/) 是 Facebook 公司出的数据流管理方案,有一定思考的价值。\n\nRecoil 是基于 Immutable 的数据流管理方案,这也是它值得被拿出来看的最重要"
},
{
"path": "前沿技术/153. 精读《snowpack》.md",
"chars": 5422,
"preview": "## 1 引言\n\n基于 webpack 构建的大型项目开发速度已经非常慢了,前端开发者已经逐渐习惯忍受超过 100 秒的启动时间,超过 30 秒的 reload 时间。即便被寄予厚望的 webpack5 内置了缓存机制也不会得到质的提升。但"
},
{
"path": "前沿技术/154. 精读《用 React 做按需渲染》.md",
"chars": 10294,
"preview": "## 1 引言\n\nBI 平台是阿里数据中台团队非常重要的平台级产品,要保证报表编辑与浏览的良好体验,性能优化是必不可少的。\n\n当前 BI 工具普遍是报表形态,要知道报表形态可不仅仅是一张张图表组件,与这些组件关联的筛选条件和联动关系错综复杂"
},
{
"path": "前沿技术/157. 精读《如何比较 Object 对象》.md",
"chars": 4207,
"preview": "## 1 引言\n\nObject 类型的比较是非常重要的基础知识,通过 [How to Compare Objects in JavaScript](https://dmitripavlutin.com/how-to-compare-obje"
},
{
"path": "前沿技术/158. 精读《Typescript 4》.md",
"chars": 10646,
"preview": "## 1 引言\n\n随着 [Typescript 4 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/) 的发布,又带来了许多新功能"
},
{
"path": "前沿技术/159. 精读《对低代码搭建的理解》.md",
"chars": 4575,
"preview": "## 1 引言\n\n在说低代码搭建之前,首先要理解什么是搭建(本文搭建指通过 Web 交互搭建一个自定义的新页面)。\n\n**我认为搭建的本质是提效** ,而提效又分为对研发人员的提效,以及对客户的提效:\n\n- 对研发人员的提效:相对于 Pro"
},
{
"path": "前沿技术/16.精读《CSS Animations vs Web Animations API》.md",
"chars": 3264,
"preview": "本期精读文章 [CSS Animations vs Web Animations API | CSS-Tricks](https://css-tricks.com/css-animations-vs-web-animations-api/)"
},
{
"path": "前沿技术/160. 精读《函数缓存》.md",
"chars": 8103,
"preview": "## 1 引言\n\n函数缓存是重要概念,本质上就是用空间(缓存存储)换时间(跳过计算过程)。\n\n对于无副作用的纯函数,在合适的场景使用函数缓存是非常必要的,让我们跟着 https://whatthefork.is/memoization 这篇"
},
{
"path": "前沿技术/161.精读《可视化搭建思考 - 富文本搭建》.md",
"chars": 3877,
"preview": "## 1 引言\n\n[「可视化搭建系统」——从设计到架构,探索前端的领域和意义](https://juejin.im/post/6854573220532748302) 这篇文章主要分析了现阶段可视化搭建的几种表现形式和实现原理,并重点介绍了"
},
{
"path": "前沿技术/162.精读《Tasks, microtasks, queues and schedules》.md",
"chars": 4721,
"preview": "## 1 引言\n\n本周跟着 [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-sched"
},
{
"path": "前沿技术/163.精读《Spring 概念》.md",
"chars": 7230,
"preview": "[spring](https://spring.io/) 是 Java 非常重要的框架,且蕴含了一系列设计模式,非常值得研究,本期就通过 [Spring 学习](https://www.cnblogs.com/wmyskxz/p/88203"
},
{
"path": "前沿技术/164.精读《数据搭建引擎 bi-designer API-设计器》.md",
"chars": 16318,
"preview": "bi-designer 是阿里数据中台团队自研的前端搭建引擎,基于它开发了阿里内部最大的数据分析平台,以及阿里云上的 QuickBI。\n\n> bi-designer 目前没有开源,因此文中使用的私有 npm 源 `@alife/bi-des"
},
{
"path": "前沿技术/165.精读《数据搭建引擎 bi-designer API-组件》.md",
"chars": 28479,
"preview": "bi-designer 是阿里数据中台团队自研的前端搭建引擎,基于它开发了阿里内部最大的数据分析平台,以及阿里云上的 QuickBI。\n\n> bi-designer 目前没有开源,因此文中使用的私有 npm 源 `@alife/bi-des"
},
{
"path": "前沿技术/166.精读《BI 搭建 - 筛选条件》.md",
"chars": 7203,
"preview": "筛选条件是 BI 搭建的核心概念,我们大部分所说的探索式分析、图表联动也都属于筛选条件的范畴,**其本质就是一个组件对另一个组件的数据查询起到筛选作用**。\n\n## 筛选组件是如何作用的\n\n我们最常见的筛选条件就是表单场景的查询控件,如下图"
},
{
"path": "前沿技术/17.精读《如何安全地使用 React context》.md",
"chars": 2508,
"preview": "# 精读《如何安全地使用 React context》\n\n本期精读文章是:[How to safely use React context](https://medium.com/@mweststrate/how-to-safely-use"
},
{
"path": "前沿技术/18.精读《设计完美的日期选择器》.md",
"chars": 2468,
"preview": "# 1. 摘要\n\n日期选择器作为基础组件重要不可或缺的一员,大家已经快习惯它一成不变的样子,输入框+日期选择弹出层。但到业务中,这种墨守成规的样子真的能百分百契合业务需求吗。这篇文章从多个网站的日期选择场景出发,企图归纳出日期选择器的最佳实"
},
{
"path": "前沿技术/19.精读《最佳前端面试题》及面试官技巧.md",
"chars": 3581,
"preview": "本期精读的文章是:[The-Best-Frontend-JavaScript-Interview-Questions](https://performancejs.com/post/hde6d32/The-Best-Frontend-Jav"
},
{
"path": "前沿技术/190.精读《DOM diff 原理详解》.md",
"chars": 6414,
"preview": "DOM diff 作为工程问题,需要具有一定算法思维,因此经常出现在面试场景中,毕竟这是难得出现在工程领域的算法问题。\n\n无论出于面试目的,还是深入学习目的,都有必要将这个问题搞懂,因此前端精读我们就专门用一个章节说清楚此问题。\n\n## 精"
},
{
"path": "前沿技术/191.精读《高性能表格》.md",
"chars": 5438,
"preview": "每个前端都想做一个完美的表格,业界也在持续探索不同的思路,比如钉钉表格、语雀表格。\n\n笔者所在数据中台团队也对表格有着极高的要求,尤其是自助分析表格,需要兼顾性能与交互功能,本文便是记录自助分析表格高性能的研发思路。\n\n## 精读\n\n要做表"
},
{
"path": "前沿技术/192.精读《DOM diff 最长上升子序列》.md",
"chars": 8071,
"preview": "在 [精读《DOM diff 原理》](https://github.com/ascoders/weekly/blob/v2/190.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E5%8E%9F%E7%"
},
{
"path": "前沿技术/193.精读《React Server Component》.md",
"chars": 10500,
"preview": "截止目前,React Server Component 还在开发与研究中,因此不适合投入生产环境使用。但其概念非常有趣,值得技术人学习。\n\n目前除了国内各种博客、知乎解读外,最一手的学习资料有下面两处:\n\n1. [Dan 的 Server "
},
{
"path": "前沿技术/194.精读《算法基础数据结构》.md",
"chars": 5147,
"preview": "掌握了不同数据结构的特点,可以让你在面对不同问题时,采用合适的数据结构处理,达到事半功倍的效果。\n\n所以这次我们详细介绍各类数据结构的特点,希望你可以融会贯通。\n\n## 精读\n\n### 数组\n\n<img width=200 src=\"htt"
},
{
"path": "前沿技术/195.精读《新一代前端构建工具对比》.md",
"chars": 4347,
"preview": "本周精读的文章是 [Comparing the New Generation of Build Tools](https://css-tricks.com/comparing-the-new-generation-of-build-tool"
},
{
"path": "前沿技术/196.精读《前端职业规划 - 2021 年》.md",
"chars": 7469,
"preview": "不知道你上次思考前端职业规划是什么时候?\n\n如果你是一位学生,你肯定对前端这个职业感到陌生,你虽然没有经验,但却对未来充满好奇,你有大把时间来思考,但可能摸不着方向,有种拳头打在棉花上的无力感。\n\n如果你已经参加了工作,不论是刚开始实习,还"
},
{
"path": "前沿技术/197.精读《低代码逻辑编排》.md",
"chars": 12676,
"preview": "逻辑编排是用可视化方式描述逻辑,在一般搭建场景中用于代替逻辑描述部分。\n\n更进一步的逻辑编排是前后端逻辑混排,一般出现在一站式 paas 平台,今天就介绍一个全面实现了逻辑编排的 paas 工具 [node-red](https://git"
},
{
"path": "前沿技术/2.精读《模态框的最佳实践》.md",
"chars": 4486,
"preview": "本期精读的文章是:[best practices for modals overlays dialog windows](https://uxplanet.org/best-practices-for-modals-overlays-dia"
},
{
"path": "前沿技术/20.精读《Nestjs》文档.md",
"chars": 7567,
"preview": "# 精读 《Nestjs 文档》\n\n本期精读的文章是:[Nestjs 文档](https://docs.nestjs.com/)\n\n体验一下 nodejs mvc 框架的优雅设计。\n\n# 1 引言\n\n<img src=\"https://im"
},
{
"path": "前沿技术/202.精读《React 18》.md",
"chars": 6189,
"preview": "React 18 带来了几个非常实用的新特性,同时也没有额外的升级成本,值得仔细看一看。\n\n下面是几个关键信息:\n\n- [React 18 工作小组](https://github.com/reactwg/react-18)。利用社区讨论 "
},
{
"path": "前沿技术/204.精读《默认、命名导出的区别》.md",
"chars": 4873,
"preview": "从代码可维护性角度出发,命名导出比默认导出更好,因为它减少了因引用产生重命名情况的发生。\n\n但命名导出与默认导出的区别不止如此,在逻辑上也有很大差异,为了减少开发时在这方面栽跟头,有必要提前了解它们的区别。\n\n本周找来了这方面很好的的文章:"
},
{
"path": "前沿技术/205.精读《JS with 语法》.md",
"chars": 2107,
"preview": "with 是一个不推荐使用的语法,因为它的作用是改变上下文,而上下文环境对开发者影响很大。\n\n本周通过 [JavaScript's Forgotten Keyword (with)](https://dev.to/mistval/javas"
},
{
"path": "前沿技术/206.精读《一种 Hooks 数据流管理方案》.md",
"chars": 5872,
"preview": "维护大型项目 OR UI 组件模块时,一定会遇到全局数据传递问题。\n\n维护项目时,像全局用户信息、全局项目配置、全局功能配置等等,都是跨模块复用的全局数据。\n\n维护 UI 组件时,调用组件的入口只有一个,但组件内部会继续拆模块,分文件,对于"
},
{
"path": "前沿技术/207.精读《Typescript infer 关键字》.md",
"chars": 3833,
"preview": "Infer 关键字用于条件中的类型推导。\n\nTypescript 官网也拿 `ReturnType` 这一经典例子说明它的作用:\n\n```typescript\ntype ReturnType<T> = T extends (...args:"
},
{
"path": "前沿技术/208.精读《Typescript 4.4》.md",
"chars": 6981,
"preview": "Typescript 4.4 正式发布了!距离 Typescript 4.5 发布还有三个月的时间,抓紧上车学习吧!\n\n本周精读的文章:[announcing-typescript-4-4](https://devblogs.microso"
},
{
"path": "前沿技术/209.精读《捕获所有异步 error》.md",
"chars": 5103,
"preview": "成熟的产品都有较高的稳定性要求,仅前端就要做大量监控、错误上报,后端更是如此,一个未考虑的异常可能导致数据错误、服务雪崩、内存溢出等等问题,轻则每天焦头烂额的处理异常,重则引发线上故障。\n\n假设代码逻辑没有错误,那么剩下的就是异常错误了。\n"
},
{
"path": "前沿技术/21.精读《Web fonts: when you need them, when you don’t》.md",
"chars": 2872,
"preview": "# 精读《Web fonts: when you need them, when you don’t》\n本期精读让我们来聊一聊 Web Fonts,文章地址:[https://hackernoon.com/web-fonts-when-yo"
},
{
"path": "前沿技术/210.精读《class static block》.md",
"chars": 3982,
"preview": "[class-static-block](https://github.com/tc39/proposal-class-static-block) 提案于 [2021.9.1](https://github.com/tc39/proposa"
},
{
"path": "前沿技术/211.精读《Microsoft Power Fx》.md",
"chars": 4257,
"preview": "Power Fx 是一门语言,虽然它被推荐的场景是低代码,但我们必须以一门语言角度看待它,才能更好的理解。\n\nPower Fx 的创建是为了更好的辅助非专业开发人员,因此这门语言被设计的足够简单,希望这门语言可以同时服务于专业与非专业开发者"
},
{
"path": "前沿技术/212.精读《可维护性思考》.md",
"chars": 4042,
"preview": "> PS: 所有没给原文链接的精读都是原创,本篇也是原创。\n\n前端精读之前写了 23 篇设计模式总结文,再加上 6 种设计原则,开闭、单一职责、依赖倒置、接口分离、迪米特法则、里氏替换原则,基本上对代码的可维护性有了全面深刻的理解。\n\n但你"
},
{
"path": "前沿技术/213.精读《Prisma 的使用》.md",
"chars": 11688,
"preview": "ORM(Object relational mappers) 的含义是,将数据模型与 Object 建立强力的映射关系,这样我们对数据的增删改查可以转换为操作 Object(对象)。\n\nPrisma 是一个现代 Nodejs ORM 库,根"
},
{
"path": "前沿技术/214.精读《web streams》.md",
"chars": 8947,
"preview": "Node stream 比较难理解,也比较难用,但 “流” 是个很重要而且会越来越常见的概念(`fetch` 返回值就是流),所以我们有必要认真学习 stream。\n\n好在继 node stream 之后,又推出了比较好用,好理解的 web"
},
{
"path": "前沿技术/215.精读《什么是 LOD 表达式》.md",
"chars": 3272,
"preview": "LOD 表达式在数据分析领域很常用,其全称为 Level Of Detail,即详细级别。\n\n## 精读\n\n什么是详细级别,为什么需要 LOD?你一定会有这个问题,我们来一步步解答。\n\n### 什么是详细级别\n\n可以尝试这么发问:你这个数据"
},
{
"path": "前沿技术/216.精读《15 大 LOD 表达式 - 上》.md",
"chars": 5572,
"preview": "通过上一篇 [精读《什么是 LOD 表达式》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/215.%E7%B2%B"
},
{
"path": "前沿技术/217.精读《15 大 LOD 表达式 - 下》.md",
"chars": 6719,
"preview": "接着上一篇 [精读《15 大 LOD 表达式 - 上》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/216.%E7"
},
{
"path": "前沿技术/218.精读《Rust 是 JS 基建的未来》.md",
"chars": 8793,
"preview": "[Rust Is The Future of JavaScript Infrastructure](https://leerob.io/blog/rust) 这篇文章讲述了 Rust 正在 JS 基建圈流行的事实:[Webpack](htt"
},
{
"path": "前沿技术/219.精读《深入了解现代浏览器一》.md",
"chars": 4336,
"preview": "[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part1) 是介绍浏览器实现原理的系"
},
{
"path": "前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md",
"chars": 2374,
"preview": "本期精读的文章是:[V8 引擎特性带来的的 JS 性能变化](https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/)\n\n\n# 1 引言\n\n<img "
},
{
"path": "前沿技术/220.精读《深入了解现代浏览器二》.md",
"chars": 4856,
"preview": "[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part2) 是介绍浏览器实现原理的系"
},
{
"path": "前沿技术/221.精读《深入了解现代浏览器三》.md",
"chars": 3923,
"preview": "[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part3) 是介绍浏览器实现原理的系"
},
{
"path": "前沿技术/222.精读《深入了解现代浏览器四》.md",
"chars": 5084,
"preview": "[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part4) 是介绍浏览器实现原理的系"
},
{
"path": "前沿技术/223.精读《Records & Tuples 提案》.md",
"chars": 15386,
"preview": "immutablejs、immer 等库已经让 js 具备了 immutable 编程的可能性,但还存在一些无解的问题,即 “怎么保证一个对象真的不可变”。\n\n如果不是拍胸脯担保,现在还真没别的办法。或许你觉得 `frozen` 是个 go"
},
{
"path": "前沿技术/224.精读《Records & Tuples for React》.md",
"chars": 6828,
"preview": "继前一篇 [精读《Records & Tuples 提案》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/223.%"
},
{
"path": "前沿技术/225.精读《Excel JS API》.md",
"chars": 4296,
"preview": "Excel 现在可利用 js 根据单元格数据生成图表、表格,或通过 js 拓展自定义函数拓展内置 Excel 表达式。\n\n我们来学习一下 Excel js API 开放是如何设计的,从中学习到一些开放 API 设计经验。\n\nAPI 文档:["
},
{
"path": "前沿技术/226.精读《2021 前端新秀回顾》.md",
"chars": 9449,
"preview": "[2021 JavaScript Rising Stars](https://risingstars.js.org/2021/en) 每年都会对前端开源项目进行点评,其依据是去年 Star 的增幅。Star 虽然只是一个维度,但至少反应了流"
},
{
"path": "前沿技术/228.精读《pipe operator for JavaScript》.md",
"chars": 6610,
"preview": "[Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-"
},
{
"path": "前沿技术/23.精读《API 设计原则》.md",
"chars": 2731,
"preview": "本期精读的文章是:[API 设计原则](https://coolshell.cn/articles/18024.html)\n\n# 1 引言\n\n<img src=\"https://img.alicdn.com/imgextra/i2/O1CN"
},
{
"path": "前沿技术/230.精读《对 Markdown 的思考》.md",
"chars": 5917,
"preview": "Markdown 即便在 2022 年也非常常用,比如这篇文章依然采用 Markdown 编写。\n\n但 Markdown 是否应该成为文本编辑领域的默认技术选型呢?答案是否定的。我找到了一篇批判无脑使用 Markdown 作为技术选型的好文"
},
{
"path": "前沿技术/237.精读《Typescript 4.5-4.6 新特性》.md",
"chars": 10419,
"preview": "## 新增 Awaited 类型\n\nAwaited 可以将 Promise 实际返回类型抽出来,按照名字可以理解为:等待 Promise resolve 了拿到的类型。下面是官方文档提供的 Demo:\n\n```ts\n// A = strin"
},
{
"path": "前沿技术/238.精读《不再需要 JS 做的 5 件事》.md",
"chars": 4021,
"preview": "关注 JS 太久,会养成任何功能都用 JS 实现的习惯,而忘记了 HTML 与 CSS 也具备一定的功能特征。其实有些功能用 JS 实现吃力不讨好,我们要综合使用技术工具,而不是只依赖 JS。\n\n[5 things you don't ne"
},
{
"path": "前沿技术/239.精读《JS 数组的内部实现》.md",
"chars": 8092,
"preview": "每个 JS 执行引擎都有自己的实现,我们这次关注 [V8](https://v8.dev/) 引擎是如何实现数组的。\n\n本周主要精读的文章是 [How JavaScript Array Works Internally?](https://"
},
{
"path": "前沿技术/24.精读《现代 JavaScript 概览》.md",
"chars": 5074,
"preview": "本期精读的文章是:\n\n[Glossary of Modern JavaScript Concepts: Part 1](https://auth0.com/blog/glossary-of-modern-javascript-concept"
},
{
"path": "前沿技术/240.精读《React useEvent RFC》.md",
"chars": 4474,
"preview": "useEvent 要解决一个问题:如何同时保持函数引用不变与访问到最新状态。\n\n本周我们结合 [RFC](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md"
},
{
"path": "前沿技术/242.精读《web reflow》.md",
"chars": 6418,
"preview": "网页重排(回流)是阻碍流畅性的重要原因之一,结合 [What forces layout / reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) 这篇文章与引用,整"
},
{
"path": "前沿技术/25.精读《null >= 0?》.md",
"chars": 3867,
"preview": "本期精读的文章是:[null >= 0?](https://blog.campvanilla.com/javascript-the-curious-case-of-null-0-7b131644e274)\n\n# 1 引言\n\n<img src"
},
{
"path": "前沿技术/253.精读《pnpm》.md",
"chars": 5794,
"preview": "[pnpm](https://pnpm.io/) 全称是 “Performant NPM”,即高性能的 npm。它结合软硬链接与新的依赖组织方式,大大提升了包管理的效率,也同时解决了 “幻影依赖” 的问题,让包管理更加规范,减少潜在风险发生"
},
{
"path": "前沿技术/254.精读《对前端架构的理解 - 分层与抽象》.md",
"chars": 5895,
"preview": "可能一些同学会认为前端比较简单而不需要架构,或者因为前端交互细节杂而乱难以统一抽象,所以没办法进行架构设计。这个理解是片面的,虽然一些前端项目是没有仔细考虑架构就堆起来的,但这不代表不需要架构设计。任何业务程序都可以通过代码堆砌的方式实现功"
},
{
"path": "前沿技术/255.精读《SolidJS》.md",
"chars": 6955,
"preview": "[SolidJS](https://github.com/solidjs/solid) 是一个语法像 React Function Component,内核像 Vue 的前端框架,本周我们通过阅读 [Introduction to Soli"
},
{
"path": "前沿技术/256.精读《依赖注入简介》.md",
"chars": 7022,
"preview": "精读文章:[Dependency Injection in JS/TS – Part 1](https://blog.codeminer42.com/dependency-injection-in-js-ts-part-1/)\n\n## 概述"
},
{
"path": "前沿技术/257.精读《State of CSS 2022》.md",
"chars": 7557,
"preview": "本周读一读 [State of CSS 2022](https://web.dev/state-of-css-2022/#color-spaces) 介绍的 CSS 特性。\n\n## 概述\n\n### 2022 已经支持的特性\n\n#### @l"
},
{
"path": "前沿技术/258.精读《proposal-extractors》.md",
"chars": 3606,
"preview": "[proposal-extractors](https://github.com/tc39/proposal-extractors) 是一个关于解构能力增强的提案,支持在直接解构时执行自定义逻辑。\n\n## 概述\n\n```ts\nconst ["
},
{
"path": "前沿技术/259.精读《Headless 组件用法与原理》.md",
"chars": 6370,
"preview": "Headless 组件即无 UI 组件,框架仅提供逻辑,UI 交给业务实现。这样带来的好处是业务有极大的 UI 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求。\n\n我们以 [headlessui"
},
{
"path": "前沿技术/26.精读《加密媒体扩展》.md",
"chars": 6899,
"preview": " 与 [Iteration protocols](https://developer.mozilla.org/en-US/docs/"
},
{
"path": "前沿技术/263.精读《我们为何弃用 css-in-js》.md",
"chars": 3830,
"preview": "[emotion](https://emotion.sh/docs/introduction) 排名第二的维护者 Sam 所在公司弃用了 css-in-js 方案,引起了不小的讨论:[Why We're Breaking Up with C"
},
{
"path": "前沿技术/264.精读《维护好一个复杂项目》.md",
"chars": 4060,
"preview": "现在许多国内互联网公司的项目都持续了五年左右,美国老牌公司如 IBM 的项目甚至持续维护了十五年,然而这些项目却有着截然不同的维护成本,有的公司项目运作几年后维护成本依然与初创期不大,可以保持较为高效的迭代速度,但有的项目甚至改几个文案都会"
},
{
"path": "前沿技术/265.精读《磁贴布局 - 功能分析》.md",
"chars": 6720,
"preview": "磁贴布局三部曲:功能分析、实现分析、性能优化的第一部 - 功能分析。\n\n因为需要做自由布局与磁贴布局混排,以及磁贴布局嵌套,所以要实现一套磁贴分析功能,所以本系列不是简单的介绍使用 [react-grid-layout](https://g"
},
{
"path": "前沿技术/266.精读《磁贴布局 - 功能实现》.md",
"chars": 7239,
"preview": "经过上一篇 [精读《磁贴布局 - 功能分析》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/265.%E7%B2%B"
},
{
"path": "前沿技术/267.精读《磁贴布局 - 性能优化》.md",
"chars": 4851,
"preview": "经过上一篇 [精读《磁贴布局 - 功能实现》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/266.%E7%B2%B"
},
{
"path": "前沿技术/27.精读《css-in-js 杀鸡用牛刀》.md",
"chars": 4691,
"preview": "本期精读的文章是:[css-in-js 杀鸡用牛刀](https://codeburst.io/css-in-js-is-like-replacing-a-broken-screwdriver-with-your-favorite-hamm"
},
{
"path": "前沿技术/277.精读《利用 GPT 解读 PDF》.md",
"chars": 5814,
"preview": "[ChatPDF](https://www.chatpdf.com/) 最近比较火,上传 PDF 文件后,即可通过问答的方式让他帮你总结内容,比如让它帮你概括核心观点、询问问题,或者做观点判断。\n\n背后用到了几个比较时髦的技术,还好有 [C"
},
{
"path": "前沿技术/28.精读《2017 前端性能优化备忘录》.md",
"chars": 15284,
"preview": "本期精读的文章是:[Front End Performance Checklist 2017](https://www.smashingmagazine.com/2016/12/front-end-performance-checklist"
},
{
"path": "前沿技术/281.精读《自由 + 磁贴混合布局》.md",
"chars": 2091,
"preview": "本篇精读来自笔者代码实践,没有原文出处请谅解。\n\n早些我们介绍过了 [磁贴布局 - 功能分析](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%"
},
{
"path": "前沿技术/282.精读《自由布局吸附线的实现》.md",
"chars": 2356,
"preview": "本篇精读来自笔者代码实践,没有原文出处请谅解。\n\n自由布局吸附线的效果如下图所示:\n\n<img width=600 src=\"https://user-images.githubusercontent.com/7970947/2518962"
},
{
"path": "前沿技术/287.精读《VisActor 数据可视化工具》.md",
"chars": 5283,
"preview": "最近数据可视化领域开源了一套 VisActor 方案,虽然刚开源没多久,但产品矩阵已经颇有特色,我们可以从中学习一下数据可视化领域是如何设计顶层架构的,以及对未来 3-5 年可视化领域的发展规划。\n\n目前 VisActor 开源出来的库有如"
},
{
"path": "前沿技术/29.精读《JS 中的内存管理》.md",
"chars": 5726,
"preview": "本期精读的文章是:\n\n[How JavaScript works: memory management + how to handle 4 common memory leaks](https://blog.sessionstack.com"
},
{
"path": "前沿技术/3.精读《前后端渲染之争》.md",
"chars": 6737,
"preview": "本期精读的文章是:[Here's why Client-side Rendering Won](https://medium.freecodecamp.com/heres-why-client-side-rendering-won-46a3"
},
{
"path": "前沿技术/30.精读《Javascript 事件循环与异步》.md",
"chars": 3087,
"preview": "本期精读的文章是:\n\n[How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/awai"
},
{
"path": "前沿技术/31.精读《我不再使用高阶组件》.md",
"chars": 4636,
"preview": "本期精读的文章是:[我不再使用高阶组件](https://medium.com/tandemly/im-breaking-up-with-higher-order-components-44b0df2db052)。\n\n懒得看文章?没关系,稍"
},
{
"path": "前沿技术/32.精读《React Router4.0 进阶概念》.md",
"chars": 4357,
"preview": "本期精读的文章是:[React Router 进阶:嵌套路由,代码分割,转场动画等等](https://blog.logrocket.com/advanced-react-router-concepts-code-splitting-ani"
},
{
"path": "前沿技术/33.精读《30 行 js 代码创建神经网络》.md",
"chars": 4985,
"preview": "本期精读的文章是:[30 行 js 代码创建神经网络](https://medium.freecodecamp.org/how-to-create-a-neural-network-in-javascript-in-only-30-line"
},
{
"path": "前沿技术/34.精读《React 代码整洁之道》.md",
"chars": 5198,
"preview": "本期精读的文章是:[React 代码整洁之道](http://americanexpress.io/clean-code-dirty-code/)。\n\n## 1 引言\n\n编程也是艺术行为,当我们思考代码复用、变量命名时,就是在进行艺术思考。"
},
{
"path": "前沿技术/35.精读《dob - 框架实现》.md",
"chars": 5192,
"preview": "本系列分三部曲:《框架实现》 《框架使用》 与 《跳出框架看哲学》,这三篇是我对数据流阶段性的总结,正好补充之前过时的文章。\n\n本篇是 《框架实现》。\n\n本周精读的文章是 [dob 文档](https://dobjs.github.io/d"
},
{
"path": "前沿技术/36.精读《When You “Git” in Trouble- a Version Control Story》.md",
"chars": 6562,
"preview": "本期精读的文章是:[When You “Git” in Trouble - a Version Control Story](https://hackernoon.com/when-you-git-in-trouble-a-version-"
},
{
"path": "前沿技术/37.精读《how we position and what we compare》.md",
"chars": 2397,
"preview": "# 摘要\n本期精读文章以一个简单的例子,抽丝剥茧细数讲述如何面向用户可视化设计,探索用户最终的目的,化繁为简,化多为少,揉和 N 张图至一张图,并传达更多的深意。本文原文:http://www.storytellingwithdata.co"
},
{
"path": "前沿技术/38.精读《dob - 框架使用》.md",
"chars": 9869,
"preview": "本系列分三部曲:《框架实现》 《框架使用》 与 《跳出框架看哲学》,这三篇是我对数据流阶段性的总结,正好补充之前过时的文章。\n\n本篇是 《框架使用》。\n\n## 1 引言\n\n现在我们团队也在重新思考数据流的价值,在业务不断发展,业务场景增多时"
},
{
"path": "前沿技术/39.精读《全链路体验浏览器挖矿》.md",
"chars": 7208,
"preview": "本期精读的文章是: [coinhive 官方文档](https://coinhive.com)及[Monero 官方文档](https://getmonero.org/)\n\n懒得看文章?没关系。\n\n咦,怎么是官方文档?\n\n本期精读有所不同,"
},
{
"path": "前沿技术/4.精读《AsyncAwait 优越之处》.md",
"chars": 6343,
"preview": "本期精读的文章是:[6 Reasons Why JavaScript’s Async/Await Blows Promises Away](https://hackernoon.com/6-reasons-why-javascripts-a"
},
{
"path": "前沿技术/40.精读《初探 Reason 与 GraphQL》.md",
"chars": 4365,
"preview": "本期精读的文章是:\n\n[Exploring Reason and GraphQL](https://dev-blog.apollodata.com/exploring-reason-and-graphql-ff877df60d2a)\n\n##"
},
{
"path": "前沿技术/41.精读《Ant Design 3.0 背后的故事》.md",
"chars": 2899,
"preview": "# 精读《Ant Design 3.0 背后的故事》\n## 引言\n2018 年初,蚂蚁金服 See Conf 上第一个分享《Ant Design 3.0 背后的故事》给很多人带来了启发。主题精彩又深刻,值得反复咀嚼。\n\n## 内容概要\n##"
},
{
"path": "前沿技术/42.精读《前端数据流哲学》.md",
"chars": 10543,
"preview": "本系列分三部曲:《框架实现》 《框架使用》 与 《数据流哲学》,这三篇是我对数据流阶段性的总结,正好补充之前过时的文章。\n\n本篇是收官之作 《前端数据流哲学》。\n\n## 1 引言\n\n写这篇文章时,很有压力,如有不妥之处,欢迎指正。\n\n同时,"
},
{
"path": "前沿技术/43.精读《增强现实与可视化》.md",
"chars": 2450,
"preview": "# 增强现实与可视化\n\n## 引言\n\n增强现实,Augmented Reality,简称 AR。在 VR 的热潮已经褪去,AI 当下正红的技术圈里,AR 似乎已经成为了过气网红。但似乎在现在,我们可以来冷静地看待一下增强现实这个概念。\n\n做"
},
{
"path": "前沿技术/44.精读《Rekit Studio》.md",
"chars": 6345,
"preview": "前端精读专栏,给大家拜年了!\n\n趁着过年,先聊几句咱们前端精读:\n\n前端精读不仅仅是知识的搬运工!前端精读不仅仅是知识的搬运工!重要的话重复两遍。\n\n论搬运知识量,我们比不上前端周刊;论翻译水平,我们比不上专业翻译计划。各位读者也一定不是冲"
},
{
"path": "前沿技术/45.精读《React's new Context API》.md",
"chars": 4085,
"preview": "本周精读的文章是 [React's new Context API](https://tinyletter.com/kentcdodds/letters/react-s-new-context-api)。\n\n## 1 引言\n\nReact 即"
},
{
"path": "前沿技术/46.精读《react-rxjs》.md",
"chars": 5580,
"preview": "本周精读的代码是 [react-rxjs](https://github.com/jarlah/react-rxjs)。\n\n## 1 引言\n\n本周精读的是 git 仓库 - react-rxjs,它给出了一个思路,让 rxjs 更好的与 r"
},
{
"path": "前沿技术/47.精读《webpack4.0 升级指南》.md",
"chars": 6302,
"preview": "本周精读的是 webpack4.0 一些变化,以及 typescript 该怎么做才能最大化利用 webpack4.0 的所有特性。\n\n## 1 引言\n\n前段时间尝试了 parcel 作为构建工具,就像农村人享受了都市的生活,就再也回不去了"
},
{
"path": "前沿技术/49.精读《Compilers are the New Frameworks》.md",
"chars": 4373,
"preview": "本期精读文章 [《Compilers are the New Frameworks》](https://tomdale.net/2017/09/compilers-are-the-new-frameworks/)\n\n## 1 引言\n\n本期文"
},
{
"path": "前沿技术/5.精读《民工叔单页数据流方案》.md",
"chars": 2954,
"preview": "本周精读文章:[单页应用的数据流方案探索](https://zhuanlan.zhihu.com/p/26426054)\n\n# 1 引言\n\n<img src=\"https://img.alicdn.com/imgextra/i1/O1CN0"
},
{
"path": "前沿技术/50.精读《快速上手构建 ARKit 应用》.md",
"chars": 2703,
"preview": "# 精读《快速上手构建 ARKit 应用》\n\n原文地址: [how-to-make-your-own-arkit-app-in-5-minutes-using-react-native](https://medium.com/@HippoA"
},
{
"path": "前沿技术/51.精读《Elements of Web Dev》.md",
"chars": 3550,
"preview": "# 1 引言\n\n本周精读, 来一起总结 web 开发的环节, 知识块和技能点. 是不是像 xx 速成班宣传的一样, 培训三个月, 经验顶三年, 入职 BAT, 年薪三十万?\n\n本文虽然是罗列知识点, 但我想很有意义. 对于学习的人来说, 提"
},
{
"path": "前沿技术/52.精读《图解 ES 模块》.md",
"chars": 5646,
"preview": "# 精读《图解 ES 模块》\n\nES 模块为 JavaScript 开发者带来了官方并且标准化的模块系统。模块标准化来之不易,用了近 10 年的时间。漫长的等待就要宣告结束了。随着五月份(2018)即将发布的 Firefox 60,几乎所有"
},
{
"path": "前沿技术/53.精读《插件化思维》.md",
"chars": 10553,
"preview": "本周精读内容是 《插件化思维》。没有参考文章,资料源自 webpack、fis、egg 以及笔者自身开发经验。\n\n## 1 引言\n\n用过构建工具的同学都知道,`grunt`, `webpack`, `gulp` 都支持插件开发。后端框架比如"
},
{
"path": "前沿技术/54.精读《在浏览器运行 serverRender》.md",
"chars": 4122,
"preview": "本周精读内容是 《在浏览器运行 serverRender》。\n\n这里是效果页,先睹为快:[client-ssr](https://ascoders.github.io/client-ssr/)。\n\n## 1 引言\n\n在服务端 ssr 成为常"
},
{
"path": "前沿技术/55.精读《async await 是把双刃剑》.md",
"chars": 3814,
"preview": "本周精读内容是 [《async/await 是把双刃剑》](https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c)。\n\n## 1 引言\n\n终于,"
},
{
"path": "前沿技术/56.精读《重新思考 Redux》.md",
"chars": 5919,
"preview": "本周精读内容是 [《重新思考 Redux》](https://hackernoon.com/redesigning-redux-b2baee8b8a38)。\n\n## 1 引言\n\n《重新思考 Redux》是 [rematch](https:/"
},
{
"path": "前沿技术/57.精读《现代 js 框架存在的根本原因》.md",
"chars": 3756,
"preview": "## 1 引言\n\n深入思考为何前端需要框架,以及 web components 是否可以代替前端框架?\n\n[原文地址](https://medium.com/dailyjs/the-deepest-reason-why-modern-jav"
},
{
"path": "前沿技术/58.精读《Typescript2.0 - 2.9》.md",
"chars": 11127,
"preview": "## 1 引言\n\n精读原文是 typescript 2.0-2.9 的文档:\n\n[2.0-2.8](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2"
},
{
"path": "前沿技术/59.精读《如何利用 Nodejs 监听文件夹》.md",
"chars": 4757,
"preview": "## 1 引言\n\n本期精读的文章是:[How to Watch for Files Changes in Node.js](http://thisdavej.com/how-to-watch-for-files-changes-in-nod"
},
{
"path": "前沿技术/6.精读《JavaScript 错误堆栈处理》.md",
"chars": 4752,
"preview": "本期精读文章:[JavaScript-Errors-and-Stack-Traces](http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html?ut"
},
{
"path": "前沿技术/60.精读《如何在 nodejs 使用环境变量》.md",
"chars": 3988,
"preview": "## 1 引言\n\n本期精读的文章是:[如何在 nodejs 使用环境变量](https://medium.freecodecamp.org/heres-how-you-can-actually-use-node-environment-va"
},
{
"path": "前沿技术/61.精读《React 八种条件渲染》.md",
"chars": 4918,
"preview": "## 1 引言\n\n本期精读的文章是:[8 React conditional rendering methods](https://blog.logrocket.com/conditional-rendering-in-react-c6b0"
},
{
"path": "前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md",
"chars": 8224,
"preview": "## 1 引言\n\n本期精读的文章是:[JS 引擎基础之 Shapes and Inline Caches](https://mathiasbynens.be/notes/shapes-ics)\n\n一起了解下 JS 引擎是如何运作的吧!\n\nJ"
},
{
"path": "前沿技术/63.精读《React 的多态性》.md",
"chars": 4058,
"preview": "## 1 引言\n\n本周精读的文章是:[surprising-polymorphism-in-react-applications](https://medium.com/@bmeurer/surprising-polymorphism-in"
},
{
"path": "前沿技术/68.精读《衡量用户体验》.md",
"chars": 2059,
"preview": "衡量用户体验已经不是一个新话题。最近也关注了一些话题来写写这方面的感受。\n\n## 前言\n\n从某种意义上说,定性反馈是 UX 设计师最常用的武器。今天从诸多交互出版物或文章中看到,Metrics-Driven Design 概念的发展和越来越"
},
{
"path": "前沿技术/69.精读《SQL vs Flux》.md",
"chars": 3403,
"preview": "## 1 引言\n\n\n\n对时序数据的处理有两种方式,如图所示,右边是 SQL,左边是自定义查询"
},
{
"path": "前沿技术/7.精读《请停止 css-in-js 的行为》.md",
"chars": 4031,
"preview": "本周精读文章:[请停止 css-in-js 的行为](https://hackernoon.com/stop-using-css-in-javascript-for-web-development-fa32fb873dcc)\n\n# 1 引言"
},
{
"path": "前沿技术/72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》.md",
"chars": 5988,
"preview": "## 1 引言\n\n每当项目进入联调阶段,或者提前约定接口时,前后端就会聚在一起热火朝天的讨论起来。可能 99% 的场景都在约定 Http 接口,讨论 URL 是什么,入参是什么,出参是什么。\n\n有的团队前后端接口约定更加高效,后端会拿出接口"
},
{
"path": "前沿技术/74.精读《12 个评估 JS 库你需要关心的事》.md",
"chars": 4156,
"preview": "## 1 引言\n\n作者给出了从 12 个角度全面分析 JS 库的可用性,分别是:\n\n1. 特性。\n2. 稳定性。\n3. 性能。\n4. 包生态。\n5. 社区。\n6. 学习曲线。\n7. 文档。\n8. 工具。\n9. 发展历史。\n10. 团队。\n1"
},
{
"path": "前沿技术/76.精读《谈谈 Web Workers》.md",
"chars": 4101,
"preview": "## 1 引言\n\n本周精读的文章是 [speedy-introduction-to-web-workers](https://auth0.com/blog/speedy-introduction-to-web-workers/),是一篇 W"
},
{
"path": "前沿技术/77.精读《用 Reduce 实现 Promise 串行执行》.md",
"chars": 3041,
"preview": "## 1 引言\n\n本周精读的文章是 [why-using-reduce-to-sequentially-resolve-promises-works](https://css-tricks.com/why-using-reduce-to-s"
},
{
"path": "前沿技术/79.精读《React Hooks》.md",
"chars": 9475,
"preview": "## 1 引言\n\nReact Hooks 是 React `16.7.0-alpha` 版本推出的新特性,想尝试的同学安装此版本即可。\n\n**React Hooks 要解决的问题是状态共享**,是继 [render-props](https"
},
{
"path": "前沿技术/8.精读《入坑 React 前没有人会告诉你的事》.md",
"chars": 5050,
"preview": "本期精读的文章是一个组合:\n\n一篇是 Gianluca Guarini 写的 《[Things nobody will tell you about React.js](https://medium.com/@gianluca.guarin"
},
{
"path": "前沿技术/80.精读《怎么用 React Hooks 造轮子》.md",
"chars": 18883,
"preview": "## 1 引言\n\n上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E"
},
{
"path": "前沿技术/81.精读《使用 CSS 属性选择器》.md",
"chars": 4752,
"preview": "# 1 引言\n\n虽然现在 Css Module 与 Css-in-js 更流行,但使用它们会导致过分依赖 **滥用 class 做唯一定位**,违背了 Css 选择器的初衷。\n\n本期精读的文章是:[attribute-selectors-s"
}
]
// ... and 103 more files (download for full content)
About this extraction
This page contains the full source code of the ascoders/weekly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 303 files (1.8 MB), approximately 912.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.