Compare commits

...
51 Commits
Author SHA1 Message Date
ascoders 4b1fa0c7c6 217 2021-11-15 09:36:07 +08:00
ascoders d0609b0e77 Merge branch 'master' of https://github.com/ascoders/weekly 2021-11-08 09:06:04 +08:00
ascoders 0cc317e9f1 216 2021-11-08 09:05:39 +08:00
黄子毅 8dad1ef1af Update 154. 精读《用 React 做按需渲染》.md 2021-11-07 21:17:32 +08:00
黄子毅 e81a26600f Update 85.精读《手写 SQL 编译器 - 智能提示》.md 2021-11-05 19:15:27 +08:00
ascoders 18a495ec39 Merge branch 'master' of https://github.com/ascoders/weekly 2021-11-01 09:07:05 +08:00
ascoders 713c88518f 215 2021-11-01 09:07:01 +08:00
黄子毅 a7fa58f620 Merge pull request #367 from XBIsland/fix#170
update 170.精读《设计模式\ -\ Prototype\ 原型模式》.md
2021-10-31 22:13:12 +08:00
黄子毅 50e042c90d Merge pull request #366 from XBIsland/fix#169
update 169.精读《设计模式 - Factory Method 工厂方法》.md
2021-10-31 22:12:58 +08:00
陈煜坚 b2c02f7892 update 170.精读《设计模式\ -\ Prototype\ 原型模式》.md 2021-10-31 16:53:16 +08:00
陈煜坚 b91e09f170 update 169.精读《设计模式 - Factory Method 工厂方法》.md 2021-10-31 15:42:46 +08:00
黄子毅 68cd8d1966 Merge pull request #364 from kongmoumou/patch-1
Update 214.精读《web streams》.md
2021-10-26 15:56:15 +08:00
kongmoumou fa28d1513c Update 214.精读《web streams》.md
根据上下文理解是否应该是「可读流」😂
2021-10-26 00:11:03 +08:00
ascoders 14f93ddd71 fix bad image 2021-10-25 21:59:17 +08:00
ascoders da81242d9b 214 2021-10-25 09:15:41 +08:00
ascoders 89c50fc032 Merge branch 'master' of https://github.com/ascoders/weekly 2021-10-18 09:19:33 +08:00
ascoders c81446eff5 213 2021-10-18 09:19:30 +08:00
黄子毅 d5f96ff5a3 Merge pull request #360 from sliuqin/patch-1
Update 212.精读《可维护性思考》.md
2021-10-14 10:36:12 +08:00
黄子毅 db3449ace1 Merge pull request #361 from kaichii/patch-2
错别字修改
2021-10-14 10:35:57 +08:00
Kai Qi 1dddebbb1f 错别字修改 2021-10-13 19:09:27 +08:00
蔡小伦 77e2da3f2d Update 212.精读《可维护性思考》.md
typo fixed
2021-10-13 15:44:04 +08:00
ascoders 019076e87f 212 2021-10-11 09:14:59 +08:00
ascoders 18e058af5b Merge branch 'master' of https://github.com/ascoders/weekly 2021-10-11 09:11:57 +08:00
ascoders fe7b930b95 212 2021-10-11 09:10:58 +08:00
黄子毅 1a9be6aaf8 Merge pull request #358 from xiangwan/patch-1
Update 2.精读《模态框的最佳实践》.md
2021-09-28 22:33:53 +08:00
向晚 3110990b90 Update 2.精读《模态框的最佳实践》.md
fixes  #357
2021-09-28 19:55:16 +08:00
黄子毅 e5e2d49ae1 Merge pull request #354 from fltenwall/master
fix:#103.精读《为什么专家不再关心技术细节》修正
2021-09-27 08:54:38 +08:00
ascoders 94381de60b update readme 2021-09-27 08:51:06 +08:00
fltenwall 70f20e6861 #103修正 2021-09-24 11:45:53 +08:00
ascoders fab11f31d2 210 2021-09-13 09:10:27 +08:00
ascoders 8af572d9f9 209 2021-09-06 08:08:29 +08:00
ascoders 0c542af0ab update readme 2021-08-30 09:53:37 +08:00
ascoders 4b3aae675d Merge branch 'master' of https://github.com/ascoders/weekly 2021-08-30 09:52:57 +08:00
ascoders 532d3bd6f0 208 2021-08-30 09:52:54 +08:00
黄子毅 cb20d65c2c Merge pull request #349 from imba-tjd/patch-1
fix 52 typo: 查分 -> 差分
2021-08-29 17:03:26 +08:00
谭九鼎 07b6017451 fix 52 typo: 查分 -> 差分 2021-08-28 21:10:30 +08:00
黄子毅 2c5f8ca4f3 Merge pull request #347 from feikerwu/patch-2
typo: fix typo in 207.精读《Typescript infer 关键字》.md
2021-08-26 10:08:32 +08:00
ascoders cd543b1fd4 fix typo 2021-08-26 10:06:13 +08:00
feikerwu 88560b6626 typo: fix typo in 207.精读《Typescript infer 关键字》.md 2021-08-23 12:01:17 +08:00
ascoders 1cf5e339c9 207 2021-08-23 10:05:52 +08:00
ascoders 4ac67508f4 206 2021-08-16 09:14:22 +08:00
ascoders d2044c4636 Merge branch 'master' of https://github.com/ascoders/weekly 2021-08-09 10:44:14 +08:00
ascoders 59629228f9 205 2021-08-09 10:44:03 +08:00
黄子毅 eeb4272d49 Merge pull request #339 from Trojan0523/patch-1
fix: typo
2021-08-06 10:29:21 +08:00
ascoders e8ae8557d2 fix link 2021-08-02 10:57:48 +08:00
ascoders f0ab10ac09 204 2021-08-02 10:51:49 +08:00
trojan0523 b7b0409cd5 fix: typo 2021-07-22 10:09:17 +08:00
ascoders 13963fbdd2 Merge branch 'master' of https://github.com/ascoders/weekly 2021-07-19 10:07:39 +08:00
ascoders 9b9d02b9de 203 2021-07-19 10:07:31 +08:00
黄子毅 ec65f684bb Merge pull request #334 from LiuL0703/patch-8
fix:typo
2021-07-12 18:34:20 +08:00
Linear-Enter 3646b36db5 fix:typo 2021-07-05 10:59:33 +08:00
29 changed files with 5470 additions and 240 deletions
+23 -10
View File
@@ -3,19 +3,32 @@
* @author 黄子毅
*/
const fs = require('fs')
const fs = require("fs");
const dirs = ['前沿技术', '设计模式', '编译原理', '源码解读', '商业思考', '算法']
const dirs = [
"前沿技术",
"设计模式",
"编译原理",
"源码解读",
"商业思考",
"算法",
];
dirs.forEach(dir => {
dirs.forEach((dir) => {
const readDir = fs.readdirSync(`./${dir}`);
console.log(`### ${dir}\n`)
console.log(`### ${dir}\n`);
readDir.sort((left, right) => left.split('.')[0] - right.split('.')[0]).forEach(dirName=>{
console.log(`- <a href="./${dir}/${dirName}">${dirName.replace('.md', '')}</a>`)
})
console.log('')
})
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("");
});
+2158
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -17,6 +17,7 @@
},
"homepage": "https://github.com/dt-fe/weekly#readme",
"dependencies": {
"esm": "^3.2.25",
"husky": "^3.0.4",
"lint-md-cli": "^0.1.1"
},
@@ -25,4 +26,4 @@
"pre-commit": "npx lint-md ./"
}
}
}
}
+218 -203
View File
@@ -6,7 +6,7 @@
前端界的好文精读,每周更新!
最新精读:<a href="./前沿技术/202.精读《React 18》.md">202.精读《React 18》</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>
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
@@ -19,223 +19,238 @@
### 前沿技术
- <a href="./前沿技术/1.精读《js 模块化发展》.md">1.精读《js 模块化发展》</a>
- <a href="./前沿技术/2.精读《模态框的最佳实践》.md">2.精读《模态框的最佳实践》</a>
- <a href="./前沿技术/3.精读《前后端渲染之争》.md">3.精读《前后端渲染之争》</a>
- <a href="./前沿技术/4.精读《AsyncAwait 优越之处》.md">4.精读《AsyncAwait 优越之处》</a>
- <a href="./前沿技术/5.精读《民工叔单页数据流方案》.md">5.精读《民工叔单页数据流方案》</a>
- <a href="./前沿技术/6.精读《JavaScript 错误堆栈处理》.md">6.精读《JavaScript 错误堆栈处理》</a>
- <a href="./前沿技术/7.精读《请停止 css-in-js 的行为》.md">7.精读《请停止 css-in-js 的行为》</a>
- <a href="./前沿技术/8.精读《入坑 React 前没有人会告诉你的事》.md">8.精读《入坑 React 前没有人会告诉你的事》</a>
- <a href="./前沿技术/9.精读《Immutable 结构共享》.md">9.精读《Immutable 结构共享》</a>
- <a href="./前沿技术/10.精读《Web Components 的困境》.md">10.精读《Web Components 的困境》</a>
- <a href="./前沿技术/11.精读《前端调试技巧》.md">11.精读《前端调试技巧》</a>
- <a href="./前沿技术/12.精读《React 高阶组件》.md">12.精读《React 高阶组件》</a>
- <a href="./前沿技术/13.精读《This 带来的困惑》.md">13.精读《This 带来的困惑》</a>
- <a href="./前沿技术/14.精读《架构设计之 DCI》.md">14.精读《架构设计之 DCI》</a>
- <a href="./前沿技术/15.精读《TC39 与 ECMAScript 提案》.md">15.精读《TC39 与 ECMAScript 提案》</a>
- <a href="./前沿技术/16.精读《CSS Animations vs Web Animations API》.md">16.精读《CSS Animations vs Web Animations API》</a>
- <a href="./前沿技术/17.精读《如何安全地使用 React context.md">17.精读《如何安全地使用 React context》</a>
- <a href="./前沿技术/18.精读《设计完美的日期选择器》.md">18.精读《设计完美的日期选择器》</a>
- <a href="./前沿技术/19.精读《最佳前端面试题》及面试官技巧.md">19.精读《最佳前端面试题》及面试官技巧</a>
- <a href="./前沿技术/20.精读《Nestjs》文档.md">20.精读《Nestjs》文档</a>
- <a href="./前沿技术/21.精读《Web fonts: when you need them, when you dont》.md">21.精读《Web fonts: when you need them, when you dont》</a>
- <a href="./前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md">22.精读《V8 引擎特性带来的的 JS 性能变化》</a>
- <a href="./前沿技术/23.精读《API 设计原则》.md">23.精读《API 设计原则》</a>
- <a href="./前沿技术/24.精读《现代 JavaScript 概览》.md">24.精读《现代 JavaScript 概览》</a>
- <a href="./前沿技术/25.精读《null >= 0?》.md">25.精读《null >= 0?》</a>
- <a href="./前沿技术/26.精读《加密媒体扩展》.md">26.精读《加密媒体扩展》</a>
- <a href="./前沿技术/27.精读《css-in-js 杀鸡用牛刀》.md">27.精读《css-in-js 杀鸡用牛刀》</a>
- <a href="./前沿技术/28.精读《2017 前端性能优化备忘录》.md">28.精读《2017 前端性能优化备忘录》</a>
- <a href="./前沿技术/29.精读《JS 中的内存管理》.md">29.精读《JS 中的内存管理》</a>
- <a href="./前沿技术/30.精读《Javascript 事件循环与异步》.md">30.精读《Javascript 事件循环与异步》</a>
- <a href="./前沿技术/31.精读《我不再使用高阶组件》.md">31.精读《我不再使用高阶组件》</a>
- <a href="./前沿技术/32.精读《React Router4.0 进阶概念》.md">32.精读《React Router4.0 进阶概念》</a>
- <a href="./前沿技术/33.精读《30 行 js 代码创建神经网络》.md">33.精读《30 行 js 代码创建神经网络》</a>
- <a href="./前沿技术/34.精读《React 代码整洁之道》.md">34.精读《React 代码整洁之道》</a>
- <a href="./前沿技术/35.精读《dob - 框架实现》.md">35.精读《dob - 框架实现》</a>
- <a href="./前沿技术/36.精读《When You “Git” in Trouble- a Version Control Story.md">36.精读《When You “Git” in Trouble- a Version Control Story》</a>
- <a href="./前沿技术/37.精读《how we position and what we compare》.md">37.精读《how we position and what we compare》</a>
- <a href="./前沿技术/38.精读《dob - 框架使用》.md">38.精读《dob - 框架使用》</a>
- <a href="./前沿技术/39.精读《全链路体验浏览器挖矿》.md">39.精读《全链路体验浏览器挖矿》</a>
- <a href="./前沿技术/40.精读《初探 Reason 与 GraphQL》.md">40.精读《初探 Reason 与 GraphQL》</a>
- <a href="./前沿技术/41.精读《Ant Design 3.0 背后的故事》.md">41.精读《Ant Design 3.0 背后的故事》</a>
- <a href="./前沿技术/42.精读《前端数据流哲学》.md">42.精读《前端数据流哲学》</a>
- <a href="./前沿技术/43.精读《增强现实与可视化》.md">43.精读《增强现实与可视化》</a>
- <a href="./前沿技术/44.精读《Rekit Studio.md">44.精读《Rekit Studio》</a>
- <a href="./前沿技术/45.精读《React's new Context API》.md">45.精读《React's new Context API》</a>
- <a href="./前沿技术/46.精读《react-rxjs.md">46.精读《react-rxjs》</a>
- <a href="./前沿技术/47.精读《webpack4.0 升级指南》.md">47.精读《webpack4.0 升级指南》</a>
- <a href="./前沿技术/49.精读《Compilers are the New Frameworks.md">49.精读《Compilers are the New Frameworks》</a>
- <a href="./前沿技术/50.精读《快速上手构建 ARKit 应用》.md">50.精读《快速上手构建 ARKit 应用》</a>
- <a href="./前沿技术/51.精读《Elements of Web Dev》.md">51.精读《Elements of Web Dev》</a>
- <a href="./前沿技术/52.精读《图解 ES 模块》.md">52.精读《图解 ES 模块》</a>
- <a href="./前沿技术/53.精读《插件化思维》.md">53.精读《插件化思维》</a>
- <a href="./前沿技术/54.精读《在浏览器运行 serverRender.md">54.精读《在浏览器运行 serverRender》</a>
- <a href="./前沿技术/55.精读《async await 是把双刃剑》.md">55.精读《async await 是把双刃剑》</a>
- <a href="./前沿技术/56.精读《重新思考 Redux》.md">56.精读《重新思考 Redux》</a>
- <a href="./前沿技术/57.精读《现代 js 框架存在的根本原因》.md">57.精读《现代 js 框架存在的根本原因》</a>
- <a href="./前沿技术/58.精读《Typescript2.0 - 2.9》.md">58.精读《Typescript2.0 - 2.9》</a>
- <a href="./前沿技术/59.精读《如何利用 Nodejs 监听文件夹》.md">59.精读《如何利用 Nodejs 监听文件夹》</a>
- <a href="./前沿技术/60.精读《如何在 nodejs 使用环境变量》.md">60.精读《如何在 nodejs 使用环境变量》</a>
- <a href="./前沿技术/61.精读《React 八种条件渲染》.md">61.精读《React 八种条件渲染》</a>
- <a href="./前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches.md">62.精读《JS 引擎基础之 Shapes and Inline Caches》</a>
- <a href="./前沿技术/63.精读《React 的多态性》.md">63.精读《React 的多态性》</a>
- <a href="./前沿技术/68.精读《衡量用户体验》.md">68.精读《衡量用户体验》</a>
- <a href="./前沿技术/69.精读《SQL vs Flux》.md">69.精读《SQL vs Flux》</a>
- <a href="./前沿技术/72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》.md">72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》</a>
- <a href="./前沿技术/74.精读《12 个评估 JS 库你需要关心的事》.md">74.精读《12 个评估 JS 库你需要关心的事》</a>
- <a href="./前沿技术/76.精读《谈谈 Web Workers》.md">76.精读《谈谈 Web Workers》</a>
- <a href="./前沿技术/77.精读《用 Reduce 实现 Promise 串行执行》.md">77.精读《用 Reduce 实现 Promise 串行执行》</a>
- <a href="./前沿技术/79.精读《React Hooks.md">79.精读《React Hooks》</a>
- <a href="./前沿技术/80.精读《怎么用 React Hooks 造轮子》.md">80.精读《怎么用 React Hooks 造轮子》</a>
- <a href="./前沿技术/81.精读《使用 CSS 属性选择器》.md">81.精读《使用 CSS 属性选择器》</a>
- <a href="./前沿技术/83.精读《React16 新特性》.md">83.精读《React16 新特性》</a>
- <a href="./前沿技术/84.精读《Typescript 3.2 新特性》.md">84.精读《Typescript 3.2 新特性》</a>
- <a href="./前沿技术/86.精读《国际化布局 - Logical Properties.md">86.精读《国际化布局 - Logical Properties》</a>
- <a href="./前沿技术/87.精读《setState 做了什么》.md">87.精读《setState 做了什么》</a>
- <a href="./前沿技术/88.精读《Caches API》.md">88.精读《Caches API》</a>
- <a href="./前沿技术/89.精读《如何编译前端项目与组件》.md">89.精读《如何编译前端项目与组件》</a>
- <a href="./前沿技术/91.精读《正则 ES2018》.md">91.精读《正则 ES2018》</a>
- <a href="./前沿技术/94.精读《Serverless 给前端带来了什么》.md">94.精读《Serverless 给前端带来了什么》</a>
- <a href="./前沿技术/95.精读《Function VS Class 组件》.md">95.精读《Function VS Class 组件》</a>
- <a href="./前沿技术/96.精读《useEffect 完全指南》.md">96.精读《useEffect 完全指南》</a>
- <a href="./前沿技术/97.精读《编写有弹性的组件》.md">97.精读《编写有弹性的组件》</a>
- <a href="./前沿技术/99.精读《Scheduling in React》.md">99.精读《Scheduling in React》</a>
- <a href="./前沿技术/100.精读《V8 引擎 Lazy Parsing.md">100.精读《V8 引擎 Lazy Parsing》</a>
- <a href="./前沿技术/101.精读《持续集成 vs 持续交付 vs 持续部署》.md">101.精读《持续集成 vs 持续交付 vs 持续部署》</a>
- <a href="./前沿技术/102.精读《Monorepo 的优势》.md">102.精读《Monorepo 的优势》</a>
- <a href="./前沿技术/104.精读《Function Component 入门》.md">104.精读《Function Component 入门》</a>
- <a href="./前沿技术/105.精读《What's new in javascript.md">105.精读《What's new in javascript》</a>
- <a href="./前沿技术/107.精读《Optional chaining.md">107.精读《Optional chaining》</a>
- <a href="./前沿技术/109.精读《Vue3.0 Function API》.md">109.精读《Vue3.0 Function API》</a>
- <a href="./前沿技术/111.精读《前端未来展望》.md">111.精读《前端未来展望》</a>
- <a href="./前沿技术/112.精读《源码学习》.md">112.精读《源码学习》</a>
- <a href="./前沿技术/113.精读《Nodejs V12》.md">113.精读《Nodejs V12》</a>
- <a href="./前沿技术/117.精读《Tableau 探索式模型》.md">117.精读《Tableau 探索式模型》</a>
- <a href="./前沿技术/118.精读《使用 css 变量生成颜色主题》.md">118.精读《使用 css 变量生成颜色主题》</a>
- <a href="./前沿技术/119.精读《前端深水区》.md">119.精读《前端深水区》</a>
- <a href="./前沿技术/120.精读《React Hooks 最佳实践》.md">120.精读《React Hooks 最佳实践》</a>
- <a href="./前沿技术/121.精读《前端与 BI》.md">121.精读《前端与 BI》</a>
- <a href="./前沿技术/123.精读《用 Babel 创造自定义 JS 语法》.md">123.精读《用 Babel 创造自定义 JS 语法》</a>
- <a href="./前沿技术/124.精读《用 css grid 重新思考布局》.md">124.精读《用 css grid 重新思考布局》</a>
- <a href="./前沿技术/125.精读《深度学习 - 函数式之美》.md">125.精读《深度学习 - 函数式之美》</a>
- <a href="./前沿技术/126.精读《Nuxtjs》.md">126.精读《Nuxtjs》</a>
- <a href="./前沿技术/127.精读《React Conf 2019 - Day1》.md">127.精读《React Conf 2019 - Day1》</a>
- <a href="./前沿技术/129.精读《React Conf 2019 - Day2》.md">129.精读《React Conf 2019 - Day2》</a>
- <a href="./前沿技术/132.精读《正交的 React 组件》.md">132.精读《正交的 React 组件》</a>
- <a href="./前沿技术/133.精读《寻找框架设计的平衡点》.md">133.精读《寻找框架设计的平衡点》</a>
- <a href="./前沿技术/134.精读《我在阿里数据中台大前端》.md">134.精读《我在阿里数据中台大前端》</a>
- <a href="./前沿技术/138.精读《精通 console.log.md">138.精读《精通 console.log》</a>
- <a href="./前沿技术/139.精读《手写 JSON Parser.md">139.精读《手写 JSON Parser》</a>
- <a href="./前沿技术/140.精读《结合 React 使用原生 Drag Drop API》.md">140.精读《结合 React 使用原生 Drag Drop API》</a>
- <a href="./前沿技术/141.精读《useRef 与 createRef 的区别》.md">141.精读《useRef 与 createRef 的区别》</a>
- <a href="./前沿技术/142.精读《如何做好 CodeReview.md">142.精读《如何做好 CodeReview》</a>
- <a href="./前沿技术/143.精读《Suspense 改变开发方式》.md">143.精读《Suspense 改变开发方式》</a>
- <a href="./前沿技术/144.精读《Webpack5 新特性 - 模块联邦》.md">144.精读《Webpack5 新特性 - 模块联邦》</a>
- <a href="./前沿技术/145.精读《React Router v6》.md">145.精读《React Router v6》</a>
- <a href="./前沿技术/146.精读《React Hooks 数据流》.md">146.精读《React Hooks 数据流》</a>
- <a href="./前沿技术/147. 精读《@types react 值得注意的 TS 技巧》.md">147. 精读《@types react 值得注意的 TS 技巧》</a>
- <a href="./前沿技术/148. 精读《React Error Boundaries.md">148. 精读《React Error Boundaries》</a>
- <a href="./前沿技术/149. 精读《React 性能调试》.md">149. 精读《React 性能调试》</a>
- <a href="./前沿技术/150. 精读《Deno 1.0 你需要了解的》.md">150. 精读《Deno 1.0 你需要了解的》</a>
- <a href="./前沿技术/152. 精读《recoil》.md">152. 精读《recoil》</a>
- <a href="./前沿技术/153. 精读《snowpack》.md">153. 精读《snowpack》</a>
- <a href="./前沿技术/154. 精读《用 React 做按需渲染》.md">154. 精读《用 React 做按需渲染》</a>
- <a href="./前沿技术/157. 精读《如何比较 Object 对象》.md">157. 精读《如何比较 Object 对象》</a>
- <a href="./前沿技术/158. 精读《Typescript 4》.md">158. 精读《Typescript 4》</a>
- <a href="./前沿技术/159. 精读《对低代码搭建的理解》.md">159. 精读《对低代码搭建的理解》</a>
- <a href="./前沿技术/160. 精读《函数缓存》.md">160. 精读《函数缓存》</a>
- <a href="./前沿技术/161.精读《可视化搭建思考 - 富文本搭建》.md">161.精读《可视化搭建思考 - 富文本搭建》</a>
- <a href="./前沿技术/162.精读《Tasks, microtasks, queues and schedules》.md">162.精读《Tasks, microtasks, queues and schedules》</a>
- <a href="./前沿技术/163.精读《Spring 概念》.md">163.精读《Spring 概念》</a>
- <a href="./前沿技术/164.精读《数据搭建引擎 bi-designer API-设计器》.md">164.精读《数据搭建引擎 bi-designer API-设计器》</a>
- <a href="./前沿技术/165.精读《数据搭建引擎 bi-designer API-组件》.md">165.精读《数据搭建引擎 bi-designer API-组件》</a>
- <a href="./前沿技术/166.精读《BI 搭建 - 筛选条件》.md">166.精读《BI 搭建 - 筛选条件》</a>
- <a href="./前沿技术/190.精读《DOM diff 原理详解》.md">190.精读《DOM diff 原理详解》</a>
- <a href="./前沿技术/191.精读《高性能表格》.md">191.精读《高性能表格》</a>
- <a href="./前沿技术/192.精读《DOM diff 最长上升子序列》.md">192.精读《DOM diff 最长上升子序列》</a>
- <a href="./前沿技术/193.精读《React Server Component.md">193.精读《React Server Component》</a>
- <a href="./前沿技术/194.精读《算法基础数据结构》.md">194.精读《算法基础数据结构》</a>
- <a href="./前沿技术/195.精读《新一代前端构建工具对比》.md">195.精读《新一代前端构建工具对比》</a>
- <a href="./前沿技术/196.精读《前端职业规划 - 2021 年》.md">196.精读《前端职业规划 - 2021 年》</a>
- <a href="./前沿技术/197.精读《低代码逻辑编排》.md">197.精读《低代码逻辑编排》</a>
- <a href="./前沿技术/202.精读《React 18》.md">202.精读《React 18》</a>
- <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 dont》</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="./设计模式/167.精读《设计模式 - Abstract Factory 抽象工厂》.md">167.精读《设计模式 - Abstract Factory 抽象工厂》</a>
- <a href="./设计模式/168.精读《设计模式 - Builder 生成器》.md">168.精读《设计模式 - Builder 生成器》</a>
- <a href="./设计模式/169.精读《设计模式 - Factory Method 工厂方法》.md">169.精读《设计模式 - Factory Method 工厂方法》</a>
- <a href="./设计模式/170.精读《设计模式 - Prototype 原型模式》.md">170.精读《设计模式 - Prototype 原型模式》</a>
- <a href="./设计模式/171.精读《设计模式 - Singleton 单例模式》.md">171.精读《设计模式 - Singleton 单例模式》</a>
- <a href="./设计模式/172.精读《设计模式 - Adapter 适配器模式》.md">172.精读《设计模式 - Adapter 适配器模式》</a>
- <a href="./设计模式/173.精读《设计模式 - Bridge 桥接模式》.md">173.精读《设计模式 - Bridge 桥接模式》</a>
- <a href="./设计模式/174.精读《设计模式 - Composite 组合模式》.md">174.精读《设计模式 - Composite 组合模式》</a>
- <a href="./设计模式/175.精读《设计模式 - Decorator 装饰器模式》.md">175.精读《设计模式 - Decorator 装饰器模式》</a>
- <a href="./设计模式/176.精读《设计模式 - Facade 外观模式》.md">176.精读《设计模式 - Facade 外观模式》</a>
- <a href="./设计模式/177.精读《设计模式 - Flyweight 享元模式》.md">177.精读《设计模式 - Flyweight 享元模式》</a>
- <a href="./设计模式/178.精读《设计模式 - Proxy 代理模式》.md">178.精读《设计模式 - Proxy 代理模式》</a>
- <a href="./设计模式/179.精读《设计模式 - Chain of Responsibility 职责链模式》.md">179.精读《设计模式 - Chain of Responsibility 职责链模式》</a>
- <a href="./设计模式/180.精读《设计模式 - Command 命令模式》.md">180.精读《设计模式 - Command 命令模式》</a>
- <a href="./设计模式/181.精读《设计模式 - Interpreter 解释器模式》.md">181.精读《设计模式 - Interpreter 解释器模式》</a>
- <a href="./设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md">182.精读《设计模式 - Iterator 迭代器模式》</a>
- <a href="./设计模式/183.精读《设计模式 - Mediator 中介者模式》.md">183.精读《设计模式 - Mediator 中介者模式》</a>
- <a href="./设计模式/184.精读《设计模式 - Memoto 备忘录模式》.md">184.精读《设计模式 - Memoto 备忘录模式》</a>
- <a href="./设计模式/185.精读《设计模式 - Observer 观察者模式》.md">185.精读《设计模式 - Observer 观察者模式》</a>
- <a href="./设计模式/186.精读《设计模式 - State 状态模式》.md">186.精读《设计模式 - State 状态模式》</a>
- <a href="./设计模式/187.精读《设计模式 - Strategy 策略模式》.md">187.精读《设计模式 - Strategy 策略模式》</a>
- <a href="./设计模式/188.精读《设计模式 - Template Method 模版模式》.md">188.精读《设计模式 - Template Method 模版模式》</a>
- <a href="./设计模式/189.精读《设计模式 - Visitor 访问者模式》.md">189.精读《设计模式 - Visitor 访问者模式》</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.精读《手写 SQL 编译器 - 词法分析》.md">64.精读《手写 SQL 编译器 - 词法分析》</a>
- <a href="./编译原理/65.精读《手写 SQL 编译器 - 文法介绍》.md">65.精读《手写 SQL 编译器 - 文法介绍》</a>
- <a href="./编译原理/66.精读《手写 SQL 编译器 - 语法分析》.md">66.精读《手写 SQL 编译器 - 语法分析》</a>
- <a href="./编译原理/67.精读《手写 SQL 编译器 - 回溯》.md">67.精读《手写 SQL 编译器 - 回溯》</a>
- <a href="./编译原理/70.精读《手写 SQL 编译器 - 语法树》.md">70.精读《手写 SQL 编译器 - 语法树》</a>
- <a href="./编译原理/71.精读《手写 SQL 编译器 - 错误提示》.md">71.精读《手写 SQL 编译器 - 错误提示》</a>
- <a href="./编译原理/78.精读《手写 SQL 编译器 - 性能优化之缓存》.md">78.精读《手写 SQL 编译器 - 性能优化之缓存》</a>
- <a href="./编译原理/85.精读《手写 SQL 编译器 - 智能提示》.md">85.精读《手写 SQL 编译器 - 智能提示》</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.精读《Immer.js》源码.md">48.精读《Immer.js》源码</a>
- <a href="./源码解读/73.精读《sqorn 源码》.md">73.精读《sqorn 源码》</a>
- <a href="./源码解读/75.精读《Epitath 源码 - renderProps 新用法》.md">75.精读《Epitath 源码 - renderProps 新用法》</a>
- <a href="./源码解读/82.精读《Htm - Hyperscript 源码》.md">82.精读《Htm - Hyperscript 源码》</a>
- <a href="./源码解读/92.精读《React PowerPlug 源码》.md">92.精读《React PowerPlug 源码》</a>
- <a href="./源码解读/93.精读《syntax-parser 源码》.md">93.精读《syntax-parser 源码》</a>
- <a href="./源码解读/98.精读《react-easy-state 源码》.md">98.精读《react-easy-state 源码》</a>
- <a href="./源码解读/110.精读《Inject Instance 源码》.md">110.精读《Inject Instance 源码》</a>
- <a href="./源码解读/122.精读《robot 源码 - 有限状态机》.md">122.精读《robot 源码 - 有限状态机》</a>
- <a href="./源码解读/128.精读《Hooks 取数 - swr 源码》.md">128.精读《Hooks 取数 - swr 源码》</a>
- <a href="./源码解读/130.精读《unstated 与 unstated-next 源码》.md">130.精读《unstated 与 unstated-next 源码》</a>
- <a href="./源码解读/151. 精读《@umijs use-request》源码.md">151. 精读《@umijs use-request》源码</a>
- <a href="./源码解读/155. 精读《use-what-changed 源码》.md">155. 精读《use-what-changed 源码》</a>
- <a href="./源码解读/156. 精读《react-intersection-observer 源码》.md">156. 精读《react-intersection-observer 源码》</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="./商业思考/90.精读《极客公园 2019》.md">90.精读《极客公园 2019》</a>
- <a href="./商业思考/103.精读《为什么专家不再关心技术细节》.md">103.精读《为什么专家不再关心技术细节》</a>
- <a href="./商业思考/106.精读《数据之上·智慧之光 - 2018》.md">106.精读《数据之上·智慧之光 - 2018》</a>
- <a href="./商业思考/108.精读《智能商业》.md">108.精读《智能商业》</a>
- <a href="./商业思考/114.精读《谁在世界中心》.md">114.精读《谁在世界中心》</a>
- <a href="./商业思考/115.精读《Tableau 入门》.md">115.精读《Tableau 入门》</a>
- <a href="./商业思考/116.精读《刷新》.md">116.精读《刷新》</a>
- <a href="./商业思考/131.精读《从 0 到 1》.md">131.精读《从 0 到 1》</a>
- <a href="./商业思考/135.精读《极客公园 IFX - 上》.md">135.精读《极客公园 IFX - 上》</a>
- <a href="./商业思考/136.精读《极客公园 IFX - 下》.md">136.精读《极客公园 IFX - 下》</a>
- <a href="./商业思考/137.精读《当我在分享的时候,我在做什么?》.md">137.精读《当我在分享的时候,我在做什么?》</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.精读《算法 - 动态规划》.md">198.精读《算法 - 动态规划》</a>
- <a href="./算法/199.精读《算法 - 滑动窗口》.md">199.精读《算法 - 滑动窗口》</a>
- <a href="./算法/200.精读《算法 - 回溯》.md">200.精读《算法 - 回溯》</a>
- <a href="./算法/201.精读《算法 - 二叉树》.md">201.精读《算法 - 二叉树》</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>
## 关注前端精读微信公众号
@@ -27,20 +27,12 @@ BI 平台是阿里数据中台团队非常重要的平台级产品,要保证
3. 如果切换到 active 后 props 没有变化,也不应该触发重渲染。
4. 从 active 切换到 inActive 后不应触发渲染,且立即阻塞后续重渲染。
目前 Function Component 做不到这一点,我们仍需借助 Class Component 的 `shouldComponentUpdate` 做到这一点,因为 Class Component 阻塞渲染时,会将最新 props 存储下来,而 Function Component 完全没有内部状态,目前还无法胜任这项工作。
我们可以写一个 `RenderWhenActive` 组件轻松实现此功能:
```jsx
class RenderWhenActive extends React.Component {
public shouldComponentUpdate(nextProps) {
return nextProps.active;
}
public render() {
return this.props.children
}
}
const RenderWhenActive = React.memo(({ children }) => children, (prevProps, nextProps) => (
!nextProps.active
))
```
### 获取组件 active 状态
@@ -11,7 +11,7 @@
# 2 内容概要
来自 Wikipedia 的定义:模态框是一个定位于应用视窗层的元素。它创造了一种模式让自身保持在一个最外层的子视察下显示,并让主视窗失效。用户必须在回到主视窗前在它上面做交互动作。
来自 Wikipedia 的定义:模态框是一个定位于应用视窗层的元素。它创造了一种模式让自身保持在一个最外层的子视察下显示,并让主视窗失效。用户必须在回到主视窗前在它上面做交互动作。
**模态框用处**
@@ -103,7 +103,7 @@ const TdElement = data.map(item => {
});
```
上面代码初始化执行了 N 个模态框初始化代码,显然不合适。对于 table 操作列中触发的模态框,所有行都对应一个模态框,通过父级中一个状态变量来控制展示的内容:
上面代码初始化执行了 N 个模态框初始化代码,显然不合适。对于 table 操作列中触发的模态框,所有行都复用同一个模态框,通过父级中一个状态变量来控制展示的内容:
```js
class Table extends Component {
+1 -1
View File
@@ -111,7 +111,7 @@ const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
- startTransition。
- useDeferredValue。
- <SuspenseList>
- &lt;SuspenseList&gt;
后两个文档还未放出,所以本文只介绍第一个 API:startTransition。首先看一下用法:
@@ -0,0 +1,223 @@
从代码可维护性角度出发,命名导出比默认导出更好,因为它减少了因引用产生重命名情况的发生。
但命名导出与默认导出的区别不止如此,在逻辑上也有很大差异,为了减少开发时在这方面栽跟头,有必要提前了解它们的区别。
本周找来了这方面很好的的文章:[export-default-thing-vs-thing-as-default](https://jakearchibald.com/2021/export-default-thing-vs-thing-as-default/),先描述梗概,再谈谈我的理解。
## 概述
一般我们认为,import 导入的是引用而不是值,也就是说,当导入对象在模块内值发生变化后,import 导入的对象值也应当同步变化。
```javascript
// module.js
export let thing = 'initial';
setTimeout(() => {
thing = 'changed';
}, 500);
```
上面的例子,500ms 后修改导出对象的值。
```javascript
// main.js
import { thing as importedThing } from './module.js';
const module = await import('./module.js');
let { thing } = await import('./module.js');
setTimeout(() => {
console.log(importedThing); // "changed"
console.log(module.thing); // "changed"
console.log(thing); // "initial"
}, 1000);
```
1s 后输出发现,前两种输出结果变了,第三种没有变。也就是对命名导出来说,前两种是引用,第三种是值。
但默认导出又不一样:
```javascript
// module.js
let thing = 'initial';
export { thing };
export default thing;
setTimeout(() => {
thing = 'changed';
}, 500);
```
```javascript
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // "changed"
console.log(defaultThing); // "initial"
console.log(anotherDefaultThing); // "initial"
}, 1000);
```
为什么对默认导出的导入结果是值而不是引用?
原因是默认导出可以看作一种对 “default 赋值” 的特例,就像 `export default = thing` 这种旧语法表达的一样,本质上是一种赋值,所以拿到的是值而不是引用。
那么默认导出的另一种写法 `export { thing as default }` 也是如此吗?并不是:
```javascript
// module.js
let thing = 'initial';
export { thing, thing as default };
setTimeout(() => {
thing = 'changed';
}, 500);
```
```javascript
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // "changed"
console.log(defaultThing); // "changed"
console.log(anotherDefaultThing); // "changed"
}, 1000);
```
可见,这种默认导出,导出的都是引用。所以导出是否是引用,不取决于是否是命名导出,**而是取决于写法**。不同的写法效果不同,哪怕相同含义的不同写法,效果也不同。
难道是写法的问题吗?是的,只要是 `export default` 导出的都是值而不是引用。但不幸的是,存在一个特例:
```javascript
// module.js
export default function thing() {}
setTimeout(() => {
thing = 'changed';
}, 500);
```
```javascript
// main.js
import thing from './module.js';
setTimeout(() => {
console.log(thing); // "changed"
}, 1000);
```
为什么 `export default function` 是引用呢?原因是 `export default function` 是一种特例,这种写法就会导致导出的是引用而不是值。如果我们用正常方式导出 Function,那依然遵循前面的规则:
```javascript
// module.js
function thing() {}
export default thing;
setTimeout(() => {
thing = 'changed';
}, 500);
```
只要没有写成 `export default function` 语法,哪怕导出的对象是个 Function,引用也不会变化。所以取决效果的是写法,而与导出对象类型无关。
对于循环引用也有时而生效,时而不生效的问题,其实也取决于写法。下面的循环引用是可以正常工作的:
```javascript
// main.js
import { foo } from './module.js';
foo();
export function hello() {
console.log('hello');
}
```
```javascript
// module.js
import { hello } from './main.js';
hello();
export function foo() {
console.log('foo');
}
```
为什么呢?因为 `export function` 是一种特例,JS 引擎对其做了全局引用提升,所以两个模块都能各自访问到。下面方式就不行了,原因是不会做全局提升:
```javascript
// main.js
import { foo } from './module.js';
foo();
export const hello = () => console.log('hello');
```
```javascript
// module.js
import { hello } from './main.js';
hello();
export const foo = () => console.log('foo');
```
所以是否生效取决于是否提升,而是否提升取决于写法。当然下面的写法也会循环引用失败,因为这种写法会被解析为导出值:
```javascript
// main.js
import foo from './module.js';
foo();
function hello() {
console.log('hello');
}
export default hello;
```
作者的探索到这里就结束了,我们来整理一下思路,尝试理解其中的规律。
## 精读
可以这么理解:
1. 导出与导入均为引用时,最终才是引用。
2. 导入时,除 `{} = await import()` 外均为引用。
3. 导出时,除 `export default thing``export default 123` 外均为引用。
对导入来说,`{} = await import()` 相当于重新赋值,所以具体对象的引用会丢失,也就是说异步的导入会重新赋值,而 `const module = await import()` 引用不变的原因是 `module` 本身是一个对象,`module.thing` 的引用还是不变的,即便 `module` 是被重新赋值的。
对导出来说,默认导出可以理解为 `export default = thing` 的语法糖,所以 `default` 本身就是一个新的变量被赋值,所以基础类型的引用无法被导出也很合理。甚至 `export default '123'` 是合法的,而 `export { '123' as thing }` 是非法的也证明了这一点,因为命名导出本质是赋值到 `default` 变量,你可以用已有变量赋值,也可以直接用一个值,但命名导出不存在赋值,所以你不能用一个字面量作命名导出。
而导出存在一个特例,`export default function`,这个我们尽量少写就行了,写了也无所谓,因为函数保持引用不变一般不会引发什么问题。
为了保证导入的总是引用,一方面尽量用命名导入,另一方面要注意命名导出。如果这两点都做不到,可以尽量把需要维持引用的变量使用 `Object` 封装,而不要使用简单变量。
最后对循环依赖而言,只有 `export default function` 存在申明提升的 Magic,可以保证循环依赖正常 Work,但其他情况都不支持。要避免这种问题,最好的办法是不要写出循环依赖,遇到循环依赖时使用第三个模块作中间人。
## 总结
一般我们都希望 import 到的是引用而不是瞬时值,但因为语义与特殊语法糖的原因,导致并不是所有写法效果都是一致的。
我也认为不需要背下来这些导入导出细枝末节的差异,只要写模块时都用规范的命名导入导出,少用默认导出,就可以在语义与实际表现上规避掉这些问题啦。
> 讨论地址是:[精读《export 默认/命名导出的区别》· Issue #342 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/342)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,94 @@
with 是一个不推荐使用的语法,因为它的作用是改变上下文,而上下文环境对开发者影响很大。
本周通过 [JavaScript's Forgotten Keyword (with)](https://dev.to/mistval/javascript-s-forgotten-keyword-with-48id) 这篇文章介绍一下 with 的功能。
## 概述
下面是一种使用 with 的例子:
```javascript
with (console) {
log('I dont need the "console." part anymore!');
}
```
我们往上下文注入了 `console` 对象,而 `console.log` 这个属性就被注册到了这个 Scope 里。
再比如:
```javascript
with (console) {
with (['a', 'b', 'c']) {
log(join('')); // writes "abc" to the console.
}
}
```
通过嵌套,我们可以追加注入上下文。其中 `with (['a', 'b', 'c'])` 其实是把 `['a', 'b', 'c']` 的返回值对象注入到了上下文,而数组对象具有 `.join` 成员函数,所以可以直接调用 `join('')` 输出 `"abc"`
为了不让结果这么 Magic,建议以枚举方式申明要注入的 key:
```javascript
with ({ myProperty: 'Hello world!' }) {
console.log(myProperty); // Logs "Hello world!"
}
```
那为什么不推荐使用 with 呢?比如下面的情况:
```javascript
function getAverage(min, max) {
with (Math) {
return round((min + max) / 2);
}
}
getAverage(1, 5);
```
注入的上下文可能与已有上下文产生冲突,导致输出结果为 `NaN`
所以业务代码中不推荐使用 with,而且实际上在 **严格模式** 下 with 也是被禁用的。
## 精读
由于 with 定义的上下文会优先查找,因此在前端沙盒领域是一种解决方案,具体做法是:
```javascript
const sandboxCode = `with(scope) { ${code} }`
new Function('scope', sandboxCode)
```
这样就把所有 scope 定义的对象限定住了。但如果访问 scope 外的对象还是会向上冒泡查找,我们可以结合 Proxy 来限制查找范围,这样就能完成一个可用性尚可的沙盒。
第二种 with 的用法是前端模版引擎。
我们经常看到模版引擎里会有一些 `forEach``map` 等特殊用法,这些语法完全可以通过 with 注入。当然并不是所有模版引擎都是这么实现的,还有另一种方案是,现将模版引擎解析为 AST,再根据 AST 构造并执行,如果把这个过程放到编译时,那么 JSX 就是一个例子。
最后关于 with 注入上下文,还有一个误区,那就是认为下面的代码仅仅注入了 `run` 属性:
```javascript
with ({ run: () => {} }) {
run()
}
```
其实不然,因为 with 会在整个原型链上查找,而 `{}` 的原型链是 `Object.prototype`,这就导致挂在了许多非预期的属性。
如果想要挂载一个纯净的对象,可以使用 `Object.create()` 创建对象挂载到 with 上。
## 总结
with 的使用场景很少,一般情况下不推荐使用。
如果你还有其他正经的 with 使用场景,可以告知我,或者给出评论。
> 讨论地址是:[精读《JS with 语法》· Issue #343 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/343)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,248 @@
维护大型项目 OR UI 组件模块时,一定会遇到全局数据传递问题。
维护项目时,像全局用户信息、全局项目配置、全局功能配置等等,都是跨模块复用的全局数据。
维护 UI 组件时,调用组件的入口只有一个,但组件内部会继续拆模块,分文件,对于这些组件内模块而言,入口文件的参数也就是全局数据。
这时一般有三种方案:
1. props 透传。
2. 上下文。
3. 全局数据流。
props 透传方案,因为任何一个节点掉链子都会导致参数传递失败,因此带来的维护成本与心智负担都特别大。
上下文即 `useContext` 利用上下文共享全局数据,带来的问题是更新粒度太粗,同上下文中任何值的改变都会导致重渲染。有一种较为 Hack 的解决方案 [use-context-selector](https://github.com/dai-shi/use-context-selector),不过这个和下面说到的全局数据流很像。
全局数据流即利用 `react-redux` 等工具,绕过 React 更新机制进行全局数据传递的方案,这种方案较好解决了项目问题,但很少有组件会使用。以前也有过不少利用 Redux 做局部数据流的方案,但本质上还是全局数据流。现在 `react-redux` 支持了局部作用域方案:
```javascript
import { shallowEqual, createSelectorHook, createStoreHook } from 'react-redux'
const context = React.createContext(null)
const useStore = createStoreHook(context)
const useSelector = createSelectorHook(context)
const useDispatch = createDispatchHook(context)
```
因此是机会好好梳理一下数据流管理方案,做一个项目、组件通用的数据流管理方案。
## 精读
对项目、组件来说,数据流包含两种数据:
1. 可变数据。
2. 不可变数据。
对项目来说,可变数据的来源有:
1. 全局外部参数。
2. 全局项目自定义变量。
不可变数据来源有:
1. 操作数据或行为的函数方法。
> 全局外部参数指不受项目代码控制的,比如登陆用户信息数据。全局项目自定义变量是由项目代码控制的,比如定义了一些模型数据、状态数据。
对组件来说,可变数据的来源有:
1. 组件被调用时的传参。
2. 全局组件自定义变量。
不可变数据来源有:
1. 组件被调用时的传参。
2. 操作数据或行为的函数方法。
对组件来说,被调用时的传参既可能是可变数据,也可能是不可变数据。比如传入的 `props.color` 可能就是可变数据,而 `props.defaultValue``props.onChange` 就是不可变数据。
当梳理清楚项目与组件到底有哪些全局数据后,我们就可以按照注册与调用这两步来设计数据流管理规范了。
### 数据流调用
首先来看调用。为了同时保证使用的便捷与应用程序的性能,我们希望使用一个统一的 API `useXXX` 来访问所有全局数据与方法,并满足:
1. `{} = useXXX()` 只能引用到不可变数据,包括变量与方法。
2. `{ value } = useXXX(state => ({ value: state.value }))` 可以引用到可变数据,但必须通过选择器来调用。
比如一个应用叫 `gaea`,那么 `useGaea` 就是对这个应用全局数据的唯一调用入口,我可以在组件里这么调用数据与方法:
```typescript
const Panel = () => {
// appId 是应用不可变数据,所以即使是变量也可以直接获取,因为它不会变化,也不会导致重渲染
// fetchData 是取数函数,内置发送了 appId,所以绑定了一定上下文,也属于不可变数据
const { appId, fetchData } = useGaea()
// 主题色可能在运行时修改,只能通过选择器获取
// 此时这个组件会额外在 color 变化时重渲染
const { color } = useGaea(state => ({
color: state.theme?.color
}))
}
```
比如一个组件叫 `Menu`,那么 `useMenu` 就是这个组件的全局数据调用入口,可以这么使用:
```typescript
// SubMenu 是 Menu 组件的子组件,可以直接使用 useMenu
const SubMenu = () => {
// defaultValue 是一次性值,所以处理时做了不可变处理,这里已经是不可变数据了
// onMenuClick 是回调函数,不管传参引用如何变化,这里都处理成不可变的引用
const { defaultValue, onMenuClick } = useMenu()
// disabled 是 menu 的参数,需要在变化时立即响应,所以是可变数据
const { disabled } = useMenu(state => ({
disabled: state.disabled
}))
// selectedMenu 是 Menu 组件的内部状态,也作为可变数据调用
const { selectedMenu } = useMenu(state => ({
selectedMenu: state.selectedMenu
}))
}
```
可以发现,在整个应用或者组件的使用 Scope 中,已经做了一层抽象,即不关心数据是怎么来的,只关心数据是否可变。这样对于组件或应用,随时可以将内部状态开放到 API 层,而内部代码完全不用修改。
### 数据流注册
数据流注册的时候,我们只要定义三种参数:
1. `dynamicValue`: 动态参数,通过 `useInput(state => state.xxx)` 才能访问到。
2. `staticValue`: 静态参数,引用永远不会改变,可以直接通过 `useInput().xxx` 访问到。
3. 自定义 hooks,入参是 `staticValue` `getState` `setState`,这里可以封装自定义方法,并且定义的方法都必须是静态的,可以直接通过 `useInput().xxx` 访问到。
```typescript
const { useState: useInput, Provider } = createHookStore<{
dynamicValue: {
fontSize: number
}
staticValue: {
onChange: (value: number) => void
}
}>(({ staticValue }) => {
const onCustomChange = React.useCallback((value: number) => {
staticValue.onChange(value + 1)
}, [staticValue])
return React.useMemo(() => ({
onCustomChange
}), [onCustomChange])
})
```
上面的方法暴露了 `Provider``useInput` 两个对象,我们首先需要在组件里给它传输数据。比如我写的是组件 `Input`,就可以这么调用:
```jsx
function Input({ onChange, fontSize }) {
return (
<Provider dynamicValue={{fontSize}} staticValue={{onChange}}>
<InputComponent />
</Provider>
)
}
```
如果对于某些动态数据,我们只想赋初值,可以使用 `defaultDynamicValue`
```jsx
function Input({ onChange, fontSize }) {
return (
<Provider dynamicValue={{fontSize}} defaultDynamicValue={{count: 1}}>
<InputComponent />
</Provider>
)
}
```
这样 `count` 就是一个动态值,必须通过 `useInput(state => ({ count: state.count }))` 才能取到,但又不会因为外层组件 Rerender 而被重新赋值为 `1`。所有动态值都可以通过 `setState` 来修改,这个后面再说。
这样所有 Input 下的子组件就可以通过 `useInput` 访问到全局数据流的数据啦,我们有三种访问数据的场景。
一:访问传给 `Input` 组件的 `onChange`
因为 `onChange` 是不可变对象,因此可以通过如下方式访问:
```typescript
function InputComponent() {
const { onChange } = useInput()
}
```
二:访问我们自定义的全局 Hooks 函数 `onCustomChange`
```typescript
function InputComponent() {
const { onCustomChange } = useInput()
}
```
三:访问可能变化的数据 `fontSize`。由于我们需要在 `fontSize` 变化时让组件重渲染,又不想让上面两种调用方式受到 `fontSize` 的影响,需要通过如下方式访问:
```typescript
function InputComponent() {
const { fontSize } = useInput(state => ({
fontSize: state.fontSize
}))
}
```
最后在自定义方法中,如果我们想修改可变数据,都要通过 `updateStore` 封装好并暴露给外部,而不能直接调用。具体方式是这样的,举个例子,假设我们需要定义一个应用状态 `status`,其可选值为 `edit``preview`,那么可以这么去定义:
```jsx
const { useState: useInput, Provider } = createHookStore<{
dynamicValue: {
isAdmin: boolean
status: 'edit' | 'preview'
}
}>(({ getState, setState }) => {
const toggleStatus = React.useCallback(() => {
// 管理员才能切换应用状态
if (!getState().isAdmin) {
return
}
setState(state => ({
...state,
status: state.status === 'edit' ? 'preview' : 'edit'
}))
}, [getState, setState])
return React.useMemo(() => ({
toggleStatus
}), [toggleStatus])
})
```
下面是调用:
```jsx
function InputComponent() {
const { toggleStatus } = useInput()
return (
<button onClick={toggleStatus} />
)
}
```
而且整个链路的类型定义也是完全自动推导的,这套数据流管理方案到这里就讲完了。
## 总结
对全局数据的使用,最方便的就是收拢到一个 `useXXX` API,并且还能区分静态、动态值,并在访问静态值时完全不会导致重渲染。
而之所以动态值 `dynamicValue` 需要在 `Provider` 里定义,是因为当动态值变化时,会自动更新数据流中的数据,使整个应用数据与外部动态数据同步。而这个更新步骤就是通过 Redux Store 来完成的。
本文特意没有给出实现源码,感兴趣的同学可以自己实现一个试一试。
> 讨论地址是:[精读《一种 Hooks 数据流管理方案》· Issue #345 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/345)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,115 @@
Infer 关键字用于条件中的类型推导。
Typescript 官网也拿 `ReturnType` 这一经典例子说明它的作用:
```typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
```
理解为:如果 `T` 继承了 `(...args: any[]) => any` 类型,则返回类型 `R`,否则返回 `any`。其中 `R` 是什么呢?`R` 被定义在 `extends (...args: any[]) => infer R` 中,即 R 是从传入参数类型中推导出来的。
## 精读
我们可以从两个视角来理解 `infer`,分别是需求角度与设计角度。
### 需求角度理解 infer
实现 `infer` 这个关键字一定是背后存在需求,这个需求是普通 Typescript 能力无法满足的。
设想这样一个场景:实现一个函数,接收一个数组,返回第一项。
我们无法用泛型来描述这种类型推导,因为泛型类型是一个整体,而我们想要返回的是入参其中某一项,我们并不能通过类似 `T[0]` 的写法拿到第一项类型:
```typescript
function xxx<T>(...args: T[]): T[0]
```
而实际上不支持这种写法也是合理的,因为这次是获取第一项类型,如果 `T` 是一个对象,我们想返回其中 `onChange` 这个 Key 的返回值类型,就不知道如何书写了。所以此时必须用一种新的语法实现,就是 `infer`
### 设计角度理解 infer
从类型推导功能来看,泛型功能非常强大,我们可以用泛型描述调用时才传入的类型,并提前将它描述在类型表达式中:
```typescript
function xxx<T>(value: T): { result: T }
```
但我们发现 `T` 这个泛型太整体化了,我们还不具备从中 Pick 子类型的能力。也就是对于 `xxx<{label: string}>` 这个场景,`T = {label: string}`,但我们无法将 `R` 定义为 `{label: R}` 这个位置,因为泛型是一个不可拆分的整体。
而且实际上为了类型安全,我们也不能允许用户描述任意的类型位置,**万一传入的类型结构不是 `{label: xxx}` 而是一个回调 `() => void`,那子类型推导岂不是建立在了错误的环境中。** 所以考虑到想要拿到 `{label: infer R}`,首先参数必须具备 `{label: xxx}` 的结构,所以正好可以将 `infer` 与条件判断 `T extends xxx ? A : B` 结合起来用,即:
```typescript
type GetLabelTypeFromObject<T> = T extends { label: infer R } ? R : never
type Result = GetLabelTypeFromObject<{ label: string }>;
// type Result = string
```
即如果 `T` 遵循 `{ label: any }` 这样一个结构,那么我可以将这个结构中任何变量位置替换为 `infer xxx`,如果传入类型满足这个结构(TS 静态解析环节判断),则可以基于这个结构体继续推导,所以在推导过程中我们就可以使用 `infer xxx` 推断的变量类型。
回过头来看第一个需求,拿到第一个参数类型就可以用 `infer` 实现了:
```typescript
type GetFirstParamType<T> = T extends (...args: infer R) => any ? R[0] : never
```
可以理解为,如果此时 `T` 满足 `(...args: any) => any` 这个结构,同时我们用 `infer R` 表示 `R` 这个临时变量指代第一个 `any` 运行时类型,那么整个函数返回的类型就是 `R`。如果 `T` 都不满足 `(...args: any) => any` 这个结构,比如 `GetFirstParamType<number>`,那这种推导根本无从谈起,直接返回 `never` 类型兜底,当然也可以自定义比如 `any` 之类的任何类型。
## 概述
我们理解了 `infer` 含义后,再结合 [conditional infer](https://learntypescript.dev/09/l2-conditional-infer) 这篇文章理解里面的例子,有助于加深记忆。
```typescript
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
// type of item1 is `number`
type item1 = ArrayElementType<number[]>;
// type of item1 is `{name: string}`
type item2 = ArrayElementType<{ name: string }>;
```
可以看到,`ArrayElementType` 利用了条件推断与 `infer`,表示了这样一个逻辑:如果 `T` 类型是一个数组,且我们将数组的每一项定义为 `E` 类型,那么返回类型就为 `E`,否则为 `T` 整体类型本身。
所以对于 `item1` 是满足结构的,所以返回 `number`,而 `item2` 不满足结构,所以返回其类型本身。
特别补充一点,对于下面的例子返回什么呢?
```typescript
type item3 = ArrayElementType<[number, string]>;
```
答案是 `number | string`,原因是我们用多个 `infer E``(infer E)[]` 相当于 `[infer E, infer E]...` 不就是多个变量指向同一个类型代词 `E` 嘛)同时接收到了 `number``string`,所以可以理解为 `E` 时而为 `number` 时而为 `string`,所以是或关系,这就是协变。
那如果是函数参数呢?
```typescript
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
? U : never
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
```
发现结果是 `string & number`,也就是逆变。但这个例子也是同一个 `U` 时而为 `string` 时而为 `number` 呀,为什么是且的关系,而不是或呢?
其实协变或逆变与 `infer` 参数位置有关。在 TypeScript 中,对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系,所以 `infer` 位置如果在函数参数上,就会遵循逆变原则。
> 逆变与协变:
>
> - 协变(co-variant):类型收敛。
> - 逆变(contra-variant):类型发散。
关于逆变与协变更深入的话题可以再开一篇文章了,这里就不细讲了,对于 `infer` 理解到这里就够啦。
## 总结
`infer` 关键字让我们拥有深入展开泛型的结构,并 Pick 出其中任何位置的类型,并作为临时变量用于最终返回类型的能力。
对于 Typescript 类型编程,最大的问题莫过于希望实现一个效果却不知道用什么语法,`infer` 作为一个强大的类型推导关键字,势必会在大部分复杂类型推导场景下派上用场,所以在遇到困难时,可以想想是不是能用 `infer` 解决问题。
> 讨论地址是:[精读《Typescript infer 关键字》· Issue #346 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/346)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,275 @@
Typescript 4.4 正式发布了!距离 Typescript 4.5 发布还有三个月的时间,抓紧上车学习吧!
本周精读的文章:[announcing-typescript-4-4](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/)
## 概述
### 更智能的自动类型收窄
类型收窄功能非常方便,它可以让 Typescript 尽可能的像 Js 一样自动智能判定类型,从而避免类型定义的工作,让你的 Typescript 写得更像 Js。
其实这个功能早就有了,在我们 [精读《Typescript2.0 - 2.9》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md#%E8%87%AA%E5%8A%A8%E7%B1%BB%E5%9E%8B%E6%8E%A8%E5%AF%BC) 就已经介绍过,当时用的名词是自动类型推导,这次用了更精确的自动类型收窄一词,因为只有类型收窄是安全的,比如:
```typescript
function foo(arg: unknown) {
if (typeof arg === "string") {
// We know 'arg' is a string now.
console.log(arg.toUpperCase());
}
}
```
而在 Typescript 4.4 之前的版本,如果我们将这个判定赋值给一个变量,再用到 `if` 分支里,就无法正常收窄类型了:
```typescript
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
}
}
```
这个问题在 Typescript 4.4 得到了解决,实际上是把这种类型收窄判断逻辑加深了,即无论这个判断写在哪都可以生效。所以下面这种解构的用法判断也可以推断出类型收窄:
```typescript
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number };
function area(shape: Shape): number {
// Extract out the 'kind' field first.
const { kind } = shape;
if (kind === "circle") {
// We know we have a circle here!
return Math.PI * shape.radius ** 2;
}
else {
// We know we're left with a square here!
return shape.sideLength ** 2;
}
}
```
不仅是单一的判断,Typescript 4.4 还支持复合类型推导:
```typescript
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean,
) {
const mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// We can access 'string' properties on both 'inputA' and 'inputB'!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
```
`mustDoWork``true` 的分支就意味着 `inputA``inputB` 均收窄为 `string` 类型。
这种深层的判定还体现在,一个具备类型判断的变量进行再计算,生成的变量还具有类型判断功能:
```typescript
function f(x: string | number | boolean) {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x; // Type of 'x' is 'string | number'.
}
else {
x; // Type of 'x' is 'boolean'.
}
}
```
可以看到,我们几乎可以像写 Js 一样写 Typescript,4.4 支持了大部分符合直觉的推导非常方便。但要注意的是,Typescript
毕竟不是运行时,无法做到更彻底的自动推断,但足以支持绝大部分场景。
### 下标支持 Symbol 与模版字符串类型判定
原本我们定义一个用下标访问的对象是这样的:
```typescript
interface Values {
[key: string]: number
}
```
现在也支持 Symbol 拉:
```typescript
interface Colors {
[sym: symbol]: number;
}
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
let colors: Colors = {};
colors[red] = 255; // Assignment of a number is allowed
let redVal = colors[red]; // 'redVal' has the type 'number'
colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.
```
而且对于特定的字符串模版也支持类型匹配,比如希望以 `data-` 开头的下标是一种独立类型,可以这么定义:
```typescript
interface Options {
width?: number;
height?: number;
}
let a: Options = {
width: 100,
height: 100,
"data-blah": true, // Error! 'data-blah' wasn't declared in 'Options'.
};
interface OptionsWithDataProps extends Options {
// Permit any property starting with 'data-'.
[optName: `data-${string}`]: unknown;
}
let b: OptionsWithDataProps = {
width: 100,
height: 100,
"data-blah": true, // Works!
"unknown-property": true, // Error! 'unknown-property' wasn't declared in 'OptionsWithDataProps'.
};
```
这个对于 HTML 的 `data-` 属性非常有帮助。
同时还支持联合类型定义,下面两种类型定义方式是等价的:
```typescript
interface Data {
[optName: string | symbol]: any;
}
// Equivalent to
interface Data {
[optName: string]: any;
[optName: symbol]: any;
}
```
### 更严格的错误捕获类型
`unknown` 类型出来之前,Typescript 以 `any` 作为抛出错误的默认类型,毕竟谁也不知道抛出错误的类型是什么:
```typescript
try {
// Who knows what this might throw...
executeSomeThirdPartyCode();
}
catch (err) { // err: any
console.error(err.message); // Allowed, because 'any'
err.thisWillProbablyFail(); // Allowed, because 'any' :(
}
```
Who knows what this might throw... 这句话很有意思,一个函数任何地方都可能出现运行时错误,这根本不是静态分析可以解决的,所以不可能自动推断错误类型,所以只能用 `any`
在 Typescript 4.4 的 `--useUnknownInCatchVariables``--strict` 模式下都将以 `unknown` 作为捕获到错误的默认类型。
相比不存在的类型 `never``unknown` 仅仅是不知道是什么类型而已,所以不能像 `any` 一样当作任何类型使用,但我们可以将其随意推断为任意类型:
```typescript
try {
executeSomeThirdPartyCode();
}
catch (err) { // err: unknown
// Error! Property 'message' does not exist on type 'unknown'.
console.error(err.message);
// Works! We can narrow 'err' from 'unknown' to 'Error'.
if (err instanceof Error) {
console.error(err.message);
}
}
```
如果觉得这样做麻烦,也可以重新申明类型为 `any`
```typescript
try {
executeSomeThirdPartyCode();
}
catch (err: any) {
console.error(err.message); // Works again!
}
```
但这样做其实并不合适,因为即便是考虑了运行时因素,理论上还是可能发生意外错误,所以对错误过于自信的类型推断是不太合适的,最好保持其 `unknown` 类型,对所有可能的边界情况做处理。
### 明确的可选属性
对象的可选属性在类型描述时有个含糊不清的地方,比如:
```typescript
interface Person {
name: string,
age?: number;
}
```
其实 Typescript 对其的类型定义的是:
```typescript
interface Person {
name: string,
age?: number | undefined;
}
```
为什么要这么定义呢?因为很多情况下,没有这个 key,与这个 key 的值为 `undefined` 的表现是等价的。但比如 `Object.keys` 场景下这两种表现却又不等价,所以理论上对于 `age?: number` 的确切表述是:要么没有 `age`,要么有 `age` 且类型为 `number`,也就是说下面的写法应该是错误的:
```typescript
// With 'exactOptionalPropertyTypes' on:
const p: Person = {
name: "Daniel",
age: undefined, // Error! undefined isn't a number
};
```
在 Typescript 4.4 中同时开启 `--exactOptionalPropertyTypes``--strictNullChecks` 即可生效。
仔细想想这是合理的,既然定义的类型不是 `undefined`,就算对象是可选类型,也不能认为赋值 `undefined` 是合理的,因为 `age?: number` 的心理预期是,要么没有这个 key,要么有但是类型为 `number`,所以当 `Object.keys` 发现 `age` 这个 key 时,值就应该是 `number`
### 支持 Static Block
Typescript 4.4 支持了 [class static blocks](https://github.com/tc39/proposal-class-static-block#ecmascript-class-static-initialization-blocks),并且在代码块作用域内可以访问私有变量。
还有一些性能提升与体验优化杂项就不一一列举了,感兴趣可以直接看原文档:[perf-improvements](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#perf-improvements)。
## 总结
从 Typescript 4.4 特性可以看出,Typescript 正在往 “更具备原生 JS 亲和性” 方向作出努力,这无疑会使 Typescript 变得越来越好用。
对更多新特性感兴趣,可以 [查看 Typescript 4.5 版本发布计划](https://github.com/microsoft/TypeScript/issues/45418)。
> 讨论地址是:[精读《Typescript 4.4》· Issue #348 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/348)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,255 @@
成熟的产品都有较高的稳定性要求,仅前端就要做大量监控、错误上报,后端更是如此,一个未考虑的异常可能导致数据错误、服务雪崩、内存溢出等等问题,轻则每天焦头烂额的处理异常,重则引发线上故障。
假设代码逻辑没有错误,那么剩下的就是异常错误了。
由于任何服务、代码都可能存在外部调用,只要外部调用存在不确定性,代码就可能出现异常,所以捕获异常是一个非常重要的基本功。
所以本周就精读 [How to avoid uncaught async errors in Javascript](https://advancedweb.hu/how-to-avoid-uncaught-async-errors-in-javascript/) 这篇文章,看看 JS 如何捕获异步异常错误。
## 概述
之所以要关注异步异常,是因为捕获同步异常非常简单:
```typescript
try {
;(() => {
throw new Error('err')
})()
} catch (e) {
console.log(e) // caught
}
```
但异步错误却无法被直接捕获,这不太直观:
```typescript
try {
;(async () => {
throw new Error('err') // uncaught
})()
} catch (e) {
console.log(e)
}
```
原因是异步代码并不在 `try catch` 上下文中执行,唯一的同步逻辑只有创建一个异步函数,所以异步函数内的错误无法被捕获。
要捕获 `async` 函数内的异常,可以调用 `.catch`,因为 `async` 函数返回一个 Promise
```typescript
;(async () => {
throw new Error('err')
})().catch((e) => {
console.log(e) // caught
})
```
当然也可以在函数体内直接用 `try catch`
```typescript
;(async () => {
try {
throw new Error('err')
} catch (e) {
console.log(e) // caught
}
})()
```
类似的,如果在循环体里捕获异常,则要使用 `Promise.all`
```typescript
try {
await Promise.all(
[1, 2, 3].map(async () => {
throw new Error('err')
})
)
} catch (e) {
console.log(e) // caught
}
```
也就是说 `await` 修饰的 Promise 内抛出的异常,可以被 `try catch` 捕获。
但不是说写了 `await` 就一定能捕获到异常,一种情况是 Promise 内再包含一个异步:
```typescript
new Promise(() => {
setTimeout(() => {
throw new Error('err') // uncaught
}, 0)
}).catch((e) => {
console.log(e)
})
```
这个情况要用 `reject` 方式抛出异常才能被捕获:
```typescript
new Promise((res, rej) => {
setTimeout(() => {
rej('err') // caught
}, 0)
}).catch((e) => {
console.log(e)
})
```
另一种情况是,这个 `await` 没有被执行到:
```typescript
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
;(async () => {
try {
const p1 = wait(3000).then(() => {
throw new Error('err')
}) // uncaught
await wait(2000).then(() => {
throw new Error('err2')
}) // caught
await p1
} catch (e) {
console.log(e)
}
})()
```
`p1` 等待 3s 后抛出异常,但因为 2s 后抛出了 `err2` 异常,中断了代码执行,所以 `await p1` 不会被执行到,导致这个异常不会被 catch 住。
而且有意思的是,如果换一个场景,提前执行了 `p1`,等 1s 后再 `await p1`,那异常就从无法捕获变成可以捕获了,这样浏览器会怎么处理?
```typescript
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
;(async () => {
try {
const p1 = wait(1000).then(() => {
throw new Error('err')
})
await wait(2000)
await p1
} catch (e) {
console.log(e)
}
})()
```
结论是浏览器 1s 后会抛出一个未捕获异常,但再过 1s 这个未捕获异常就消失了,变成了捕获的异常。
这个行为很奇怪,当程序复杂时很难排查,因为并行的 Promise 建议用 Promise.all 处理:
```typescript
await Promise.all([
wait(1000).then(() => {
throw new Error('err')
}), // p1
wait(2000),
])
```
另外 Promise 的错误会随着 Promise 链传递,因此建议把 Promise 内多次异步行为改写为多条链的模式,在最后 `catch` 住错误。
还是之前的例子,Promise 无法捕获内部的异步错误:
```typescript
new Promise((res, rej) => {
setTimeout(() => {
throw Error('err')
}, 1000) // 1
}).catch((error) => {
console.log(error)
})
```
但如果写成 Promise Chain,就可以捕获了:
```typescript
new Promise((res, rej) => {
setTimeout(res, 1000) // 1
})
.then((res, rej) => {
throw Error('err')
})
.catch((error) => {
console.log(error)
})
```
原因是,用 Promise Chain 代替了内部多次异步嵌套,这样多个异步行为会被拆解为对应 Promise Chain 的同步行为,Promise 就可以捕获啦。
最后,DOM 事件监听内抛出的错误都无法被捕获:
```typescript
document.querySelector('button').addEventListener('click', async () => {
throw new Error('err') // uncaught
})
```
同步也一样:
```typescript
document.querySelector('button').addEventListener('click', () => {
throw new Error('err') // uncaught
})
```
只能通过函数体内 `try catch` 来捕获。
## 精读
我们开篇提到了要监控所有异常,仅通过 `try catch``then` 捕获同步、异步错误还是不够的,因为这些是局部错误捕获手段,当我们无法保证所有代码都处理了异常时,需要进行全局异常监控,一般有两种方法:
- `window.addEventListener('error')`
- `window.addEventListener('unhandledrejection')`
`error` 可以监听所有同步、异步的运行时错误,但无法监听语法、接口、资源加载错误。而 `unhandledrejection` 可以监听到 Promise 中抛出的,未被 `.catch` 捕获的错误。
在具体的前端框架中,也可以通过框架提供的错误监听方案解决部分问题,比如 React 的 [Error Boundaries](https://reactjs.org/docs/error-boundaries.html)、Vue 的 [error handler](https://v3.vuejs.org/api/application-config.html#errorhandler),一个是 UI 组件级别的,一个是全局的。
回过头来看,本身 js 提供的 `try catch` 错误捕获是非常有效的,之所以会遇到无法捕获错误的经常,大多是因为异步导致的。
然而大部分异步错误,都可以通过 `await` 的方式解决,我们唯一要注意的是,`await` 仅支持一层,或者说一条链的错误监听,比如这个例子是可以监听到错误的:
```typescript
try {
await func1()
} catch (err) {
// caught
}
async function func1() {
await func2()
}
async function func2() {
throw Error('error')
}
```
也就是说,只要这一条链内都被 `await` 住了,那么最外层的 `try catch` 就能捕获异步错误。但如果有一层异步又脱离了 `await`,那么就无法捕获了:
```typescript
async function func2() {
setTimeout(() => {
throw Error('error') // uncaught
})
}
```
针对这个问题,原文也提供了例如 `Promise.all`、链式 Promise、`.catch` 等方法解决,因此只要编写代码时注意对异步的处理,就可以用 `try catch` 捕获这些异步错误。
## 总结
关于异步错误的处理,如果还有其它未考虑到的情况,欢迎留言补充。
> 讨论地址是:[精读《捕获所有异步 error》· Issue #350 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/350)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,142 @@
[class-static-block](https://github.com/tc39/proposal-class-static-block) 提案于 [2021.9.1](https://github.com/tc39/proposal-class-static-block/commit/c0cabee0aa2d036a8d902fea7bc1d179e3de2477) 进入 stage4,是一个基于 Class 增强的提案。
本周我们结合 [ES2022 feature: class static initialization blocks](https://2ality.com/2021/09/class-static-block.html) 这篇文章一起讨论一下这个特性。
## 概述
为什么我们需要 class static block 这个语法呢?其中一个原因是对 Class 静态变量的灵活赋值需求。以下面为例,我们想在 Class 内部对静态变量做批量初始化,就不得不写一个无用的 `_` 变量用来做初始化的逻辑:
```typescript
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static _ = initializeTranslator( // (A)
this.translations, this.englishWords, this.germanWords);
}
function initializeTranslator(translations, englishWords, germanWords) {
for (const [english, german] of Object.entries(translations)) {
englishWords.push(english);
germanWords.push(german);
}
}
```
而且我们为什么把 `initializeTranslator` 写在外面呢?就因为在 Class 内部不能写代码块,但这造成一个严重的问题,是外部函数无法访问 Class 内部属性,所以需要做一堆枯燥的传值。
从这个例子看出,我们为了自定义一段静态变量初始化逻辑,需要做出两个妥协:
1. 在外部定义一个函数,并接受大量 Class 成员变量传参。
2. 在 Class 内部定义一个无意义的变量 `_` 用来启动这个函数逻辑。
这实在太没有代码追求了,我们在 Class 内部做掉这些逻辑不就简洁了吗?这就是 class static block 特性:
```typescript
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static { // (A)
for (const [english, german] of Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
}
}
```
可以看到,`static` 关键字后面不跟变量,而是直接跟一个代码块,就是 class static block 语法的特征,在这个代码块内部,可以通过 `this` 访问 Class 所有成员变量,包括 `#` 私有变量。
原文对这个特性使用介绍就结束了,最后还提到一个细节,就是执行顺序。即所有 `static` 变量或区块都按顺序执行,父类优先执行:
```typescript
class SuperClass {
static superField1 = console.log('superField1');
static {
assert.equal(this, SuperClass);
console.log('static block 1 SuperClass');
}
static superField2 = console.log('superField2');
static {
console.log('static block 2 SuperClass');
}
}
class SubClass extends SuperClass {
static subField1 = console.log('subField1');
static {
assert.equal(this, SubClass);
console.log('static block 1 SubClass');
}
static subField2 = console.log('subField2');
static {
console.log('static block 2 SubClass');
}
}
// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'
```
所以 Class 内允许有多个 class static block,父类和子类也可以有,不同执行顺序结果肯定不同,这个选择权交给了使用者,因为执行顺序和书写顺序一致。
## 精读
结合提案来看,class static block 还有一个动机,就是给了一个访问私有变量的机制:
```typescript
let getX;
export class C {
#x
constructor(x) {
this.#x = { data: x };
}
static {
// getX has privileged access to #x
getX = (obj) => obj.#x;
}
}
export function readXData(obj) {
return getX(obj).data;
}
```
理论上外部无论如何都无法访问 Class 私有变量,但上面例子的 `readXData` 就可以,而且不会运行时报错,原因就是其整个流程都是合法的,最重要的原因是,class static block 可以同时访问私有变量与全局变量,所以可以利用其做一个 “里应外合”。
不过我并不觉得这是一个好点子,反而像一个 "BUG",因为任何对规定的突破都会为可维护性埋下隐患,除非这个特性用在稳定的工具、框架层,用来做一些便利性工作,最终提升了应用编码的体验,这种用法是可以接受的。
最后要意识到,class static block 本质上并没有增加新功能,我们完全可以用普通静态变量代替,只是写起来很不自然,所以这个特性可以理解为对缺陷的补充,或者是语法完善。
## 总结
总的来说,class static block 在 Class 内创建了一个块状作用域,这个作用域内拥有访问 Class 内部私有变量的特权,且这个块状作用域仅在引擎调用时初始化执行一次,是一个比较方便的语法。
原文下方有一些反对声音,说这是对 JS 的复杂化,也有诸如 JS 越来越像 Java 的声音,不过我更赞同作者的观点,也就是 Js 中 Class 并不是全部,现在越来越多代码使用函数式语法,即便使用了 Class 的场景也会存在大量函数申明,所以 class static block 这个提案对开发者的感知实际上并不大。
> 讨论地址是:[精读《class static block》· Issue #351 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/351)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,122 @@
Power Fx 是一门语言,虽然它被推荐的场景是低代码,但我们必须以一门语言角度看待它,才能更好的理解。
Power Fx 的创建是为了更好的辅助非专业开发人员,因此这门语言被设计的足够简单,希望这门语言可以同时服务于专业与非专业开发者,这是个非常崇高的理想。
本周我们就随着 [Microsoft Power Fx 概述](https://docs.microsoft.com/zh-cn/power-platform/power-fx/overview) 这篇文章,详细了解一下这门语言是怎么做的。
## 概述
```javascript
Notify("this is a problem", Error)
```
这就是 Power Fx 语言的一个例子,乍一看没什么特别的。
Power Fx 描述的是画布应用公式语言,也就是说,这个编程语言是专门为画布引用设计的。
那什么是画布应用呢?低代码、网站搭建、BI、Web Excel 这些统统都是画布应用,所以 Power Fx 其实是一门适应画布场景的语言,直接面向用户。
那这种画布语言应该具备什么特性呢?Power Fx 团队已经有了一些思考:
- 简单:该语言设计本着简介简单的原则,这样才方便非开发人员上手。
- Excel 一致性:可以帮助 Excel 开发者做知识迁移,一部分是和微软 Excel 太成功了有关,另一方面 Excel 表达式在画布语言领域探索确实深入,有可取性。对不能满足的尝试借鉴 SQL 这种声明性语言。
- 声明性:这个最重要,即描述做什么,而不是如何或何时做。这个有点像 Jquery 转到 React 模式时,过程式代码与数据驱动代码的区别。
- 函数式:函数式在灵活性和易用性上有天然优势,且无副作用的特性也利于理解逻辑与编译优化。
- 组合:即利用函数式这个特性,推荐利用已有函数组合成新功能,而不是将比如 Sort、Filter 等功能在每个组件上重复实现或者重复配置一遍。
- 强类型:类型对可维护性至关重要,再强大的低代码语言,如果没有类型支持,都不能称为易上手。
- 类型推理:可以自动推断类型。这个和强类型一样,有点 TS 的感觉,主要方便书写简洁代码。
- 不推荐面向对象:既然推荐了函数式,当然不推荐面向对象了。
- 可推展:开发者要拥有拓展函数与组件的能力,还要支持通过 Javascript 来拓展。
- 对开发人员友好:这门语言还要在与前面原则不冲突的情况下,尽量对开发人员友好。
- 语言的迭代:即当语法变更时,要帮助用户平滑迁移,毕竟这门语言直接面向普通用户而非专业开发者。Power Fx 提供了这个能力,对每个文档进行版本标记,并在升级后,通过 “兼容转换器” 自动将老语法升级为新语法。
- 无 undefined 值:为了简化语言带来的理解成本,移除了 undefined 值这个特定。
所以,基于这些考虑的 Power Fx 设计出来是这样的:
1. 实时性
即无论任何 UI 或语法错误,都不会阻塞其它正常节点的工作,同时代码效果与错误信息实时反馈。这保证了在画布应用编写逻辑的良好体验,因为本身画布应用就是实时的,低代码能力本身也要与画布实时性浑然一体。
<img width=500 src="https://z3.ax1x.com/2021/09/25/4sqx1g.gif">
2. 低代码特征
即任何 UI 组件都不需要描述类似 `onChange` 之类的回调,它们只要申明使用的变量,当这些变量变化时,程序会自动、异步、按需的更新使用到的组件。
3. 与无代码结合
所谓无代码,就是通过 UI 表单可视化的对画布应用进行配置。
与无代码的结合方式是,任意属性都可以用低代码,即表达式编写,但也提供了 UI 表单供编辑,其中 UI 表单编辑后,可以用低代码二次加工,而用低代码编辑的属性,表单就无法编辑了,此时点击表单编辑会跳转到低代码编辑框。
## 精读
创建一门不用学习就能上手的编程语言,需要足够简单,即从用户角度来理解事物:比如用户不知道回调函数等概念,那就屏蔽所谓的回调函数概念,让一切都是表达式。
这些表达式看起来很简单,也符合直觉,并且会自动驱动 UI 重绘,即声明式编程。
下面我们来讨论几个有意思的点:
### 为什么不用 Js
大部分画布应用都是指 Web 应用了,即便是 Excel,现在也早已转型到 Web Excel,就微软来说,早早转型到 Office Online 就能看出来。
然而 Js 是浏览器内置支持的脚本语言,且上手成本也比较低,其实很多低代码平台内置的编程语言就是 Js,其好处是实现成本低(沙箱甚至 `new function`),而 Power Fx 在浏览器平台最终也要转换为 Js 执行,费这么大劲创造一门新语言,无非是觉得 Js 不够 “零门槛”。
首先第一点是不符合 Excel 表达式规范,我们不要忘了 Power Fx 也是有小心机的,它想利用 Excel 生态扩大用户群,所以第一目的是兼容 Excel 语法。比如 Excel 使用 & 链接字符串,而 Js 使用 + 连接,虽然我觉得显然 + 号更自然,但微软觉得还是要符合 Excel 用户习惯。说实话在这一点上,撇开 Excel 的语法,我很难看出为什么 & 连接字符串就 “更易上手”,而 + 连接字符串 “更适合程序员使用”。
但有些是认可的,比如移除了 undefined 值,确实让语言更好理解。
也许未来 Power Fx 会更进一步,引入类 SQL 描述性的语法,像写自然语言一样编程,在这种程度上,配合强类型提示,在特定场景会比 Js 更好用。
### 提供内置函数
Js 提供了大量内置函数,这似乎不是 Power Fx 的专利,但 Power Fx 提供了许多 UI 级别的函数,这可比 Js 点到为止的 `alert` 强多了。
Power Fx 提供了 Confirm、Notify 用于弹出提示窗供用户输入,并且就算要形成逻辑,也只需要几乎一行代码:
```text
If( Confirm( "Are you sure?", {Title: "Delete Confirmation"} ), Remove( ThisItem ) )
```
可以看到,这里充斥着异步操作:
- 等待用户输入。
- 删除元素。
但这些内置函数间的组合将异步效果转换为同步写法,这大大降低开发成本。
另一类内置函数则封装了业务属性,比如 `User` 可以获取当前用户信息。本来获取用户信息就需要代码开发,但低代码平台本身就实现了全套账号体系,因此低代码平台可以直接提供如 `User().Email` 函数访问当前用户的邮箱地址。
还有诸如 `Reset` 函数,可以重制控件为默认值,比如 `Reset( TextInput1 )`,这其实是把平台提供的所有上层能力抽象成低代码函数供用户调用,这样用户只要付出一点点学习成本,就可以获得比简单 UI 强大的多的应用编辑能力,这非常值得我们学习。
更多公式函数可以参考 [文档](https://docs.microsoft.com/zh-cn/powerapps/maker/canvas-apps/formula-reference)。
### 提供对表的操作
[对表的操作](https://docs.microsoft.com/zh-cn/power-platform/power-fx/tables) 让应用数据管理可以和 Excel 同一概念来看待了,这个统一方式就是,把数据抽象成表。Power Fx 提供了系列函数用于表处理:
```text
AddColumns(
Filter( Products, 'Quantity Requested' > 'Quantity Available' ),
"Quantity To Order", 'Quantity Requested' - 'Quantity Available'
)
```
这些函数可以跨语言操作 Excel、Sql Server 等数据源的数据,学习成本与 SQL 类似,其实到这一步,对低代码用户的要求也不低,至少和熟练使用计算公式的 Excel 使用者相当。
## 总结
UI 编辑能力局限但易上手,代码能力最强但难上手,Power Fx 给我们提供了一种折中方案,即提供一种 “高度封装的简化代码” 供用户使用。
纵观其它低代码平台,也有一类采用了另一种折中方案,即超强的复杂编辑 UI,登峰造极的产物便是逻辑编排,这个方向在特定领域也是不错的选择,参考: [精读《低代码逻辑编排》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/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)。
> 讨论地址是:[精读《Microsoft Power Fx》· Issue #355 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/355)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,101 @@
> PS: 所有没给原文链接的精读都是原创,本篇也是原创。
前端精读之前写了 23 篇设计模式总结文,再加上 6 种设计原则,开闭、单一职责、依赖倒置、接口分离、迪米特法则、里氏替换原则,基本上对代码的可维护性有了全面深刻的理解。
但你我在工作中都会不断遇到烂代码,快要无法维护的大型项目,想一想,仅凭设计模式就能解决这些问题吗?为什么不断膨胀的大型项目总是变得越来越难以维护,而复杂度更高的真实世界,但没有人觉得快要崩塌了呢?
设计模式考虑的是代码之间的关系,设计原则考虑的是模块以及项目间的关系,那是否存在更上层的思考,解决大型项目越来越难维护的问题?
## 精读
先考虑一下,为什么真实世界没有可维护性问题?
### 真实世界为什么没有可维护问题
这个问题看起来有点傻,因为从来没有人会发出这样的抱怨 “我们的产品、科技、概念太多了,多到我觉得无法在这个世界活下去了”。但是在代码世界,程序员经常会抱怨,项目的概念太多、设计过于复杂,以至于他无法继续再维护下去了,是时候寻找下一份工作了。
一种显而易见的解释是,生活中,我们都是小角色,活在自己的天空下并不需要触及那么多概念,而程序员在项目中基本扮演了上帝的角色,必须为每一个细节操心。
但这并不完全解释得通。我们以为自己接触的东西不多,但实际上日常生活的知识太多了,就拿家电来说,每个人都会同时接触几十种家电,大到空调冰箱洗衣机,小到手机牙刷充电器,即便这些产品被大量标准化,但每个产品用起来都有大量细节的区别,但没有一个人觉得学习使用一个新剃须刀是一种负担,也并不觉得一款设计得不好的牙刷,会对整个牙刷行业造成怎样负面的冲击。
这背后的原因是:拷贝。正因为我们用的每一件东西都是拷贝,所以即使用坏了也不会对其它相同物品产生任何影响。但代码世界则不同,因为代码调用关系的存在,复用的越优雅,破坏力也就越大。一栋大楼断了几块钢筋尚可支撑,但换在代码世界,只要断了一块钢筋,就意味着这栋大楼所有钢筋都断了。这就是程序员最痛恨的问题之一,就是为什么改了一处看似人畜无害的代码,却导致一场故障。
从这个角度来说,代码世界是无法吸取真实世界经验的。而且代码世界的这种副作用,在商业上是有巨大正向价值的,即软件的边际成本几乎为零,这是实体产品做不到的,因此软件需要付出可维护性代价,似乎是这种极低边际成本的代价。
虽然通过借鉴真实世界的经验,使自己维护成本变成零是不可能的,但真实世界对软件世界确实有可借鉴之处,下面我们就来探讨几个有意思的点。
### 真实世界不断屏蔽复杂度
不知道你会不会有过这样的思考:面试官总是问原理,就是担心我只会用框架,而缺乏基础。但基础是什么呢?懂得 js,java 算是基础吗?也可以说不算,因为这些语言背后的编译原理好像才是基础,编译原理背后还有操作系统,操作系统运行在硬件上,而硬件的原理呢?从 CPU 设计到背后的硅是如何制作的,等等,这样下去,似乎永远也无法掌握原理。
但当我们从软件推导到硬件时,可以很自然的发现,没有人觉得掌握硅胶的制作过程是一件必须的事,我们可以一直使用硅胶制作的产品,但却可以不用了解硅胶制作的原理。
真实世界总是不断屏蔽复杂度,作为消费者时,我们面对的商品总是经过精心包装,简单易用的,只有我们工作时,才需要对某个专业领域的原理有所了解。
这个道理可以迁移到代码世界,即对于一个庞大而复杂的项目,不能指望每位开发者都了解全部原理后才能工作,我们需要在大多数时候把开发者当作消费者来看待,提供精美而稳定的接口。要做到这一点,需要一个类似下图的架构设计:
<img width=400 src="https://z3.ax1x.com/2021/10/08/5PTFBR.png">
从图中可以看出,即便是业务层代码,我也不需要关心过于底层的实现,底层的代码就像脚下被压实了的土地,只需要在上面走就行了。
然而最让人崩溃的是下面的设计:
<img width=400 src="https://z3.ax1x.com/2021/10/08/5P7Fxg.png">
为了解决一个问题,需要面对无穷无尽的上下文,这就是维护成本高的最主要原因。
### 为什么觉得维护成本高
作为开发者,已经习惯了评价代码维护成本高还是低,今天我们换个视角,想一想为什么你会觉得维护成本高?
对维护成本的感受不完全是客观的,我画了一个四象限图:
<img width=300 src="https://z3.ax1x.com/2021/10/09/5iGjZ6.png">
左边是和人相关部分,包括你对代码的理解能力,以及对项目的熟悉度。
理解能力越强,越不容易觉得维护成本高;对项目越熟悉,哪怕是屎山代码,也会觉得重构后可维护性并不会提高,因为自己对项目会变得不熟悉。
右边是和项目相关部分,包括业务本身的复杂度,以及这背后的技术抽象实现的质量。
业务本身越复杂,维护成本就会越高,因为信息量不可避免的增大了,我们永远不能只盯着 Hello World 的 Demo 研究框架;代码质量体现了技术对业务的抽象,抽象的好,复杂度曲线就会比较贴合业务真实复杂度,抽象的不好,Hello World Demo 也能够新人进来喝一壶。
在这四个关键词中,业务复杂度是几乎无法改变的,对项目熟悉也需要一个过程,所以重点应该放在理解能力与代码质量两部分。
无论是个人理解能力,还是代码质量,目标都是帮助我们快速理解项目,也就是说,只要能快速理解技术项目在做什么,我如何快速融入,就会觉得可维护性高,反之则觉得不好维护。
所以一个简单的项目,或者一个分层合理,文档清晰的大型项目都会让人觉得可维护性好。在这一点上,需要向真实世界学习的经验就是,即便在软件世界,也并不是了解所有原理,所有犄角旮旯的逻辑才表明技高一筹,带着这种思想工作只会让大家陷入无尽的内卷和理解焦虑。我们要给大家思想减负,不需要理解的模块、代码设计,就不要轻易展示出来,将每个模块开发所需了解的最小知识设定好,最大程度减少开发者的理解负担。
当然要补充一句,这并不意味着局限开发者的成长和学习空间,其它知识随时敞开大门,只是理解它们并不是日常开发所必要的,这些知识形成文档可以用完即弃,不用成为长期记忆。说到这,就引出了真实世界第二个有趣的地方,就是说明书。
### 真实世界的说明书
我回头想想也挺不可思议的,无论快递买来任何需要组装的东西,按照说明书的指引最终都可以组装好,而且装好之后就可以把说明书扔了,完全没有认知负担。
与其说快递包裹的说明书太完善了,不如说说明不完善,不好用的商品根本卖不出去。我们早已习惯极度易用的商品,及其详尽的说明书了,这是商业社会持续发展,长期博弈后的结果,而且会稳定持续下去。试想一下,如果我们参与维护的项目也有精巧的设计,完善的文档,那维护就不是什么问题,按照文档说的一步步来就行了。
那为什么大部分情况,我们接手的项目就像一个没有说明书的乐高呢?这应该是商品与代码的本质区别了,即商品质量好不好,是由买家用钞票投票的,做得好用,说明书完善的商品才能存活下来,但这背后的技术实现是看不到的,也没有人可以投票,即便技术人员吐槽代码无法维护,但如果项目取得了商业上的成功,也只会越做越大,技术债越滚越多。
技术项目的买家是程序员,但程序员没有拒签的办法,导致无论项目质量如何都要接受,没有市场机制的作用,就导致了烂代码随处可见。
要解决这个问题,首先要意识到这个问题,即技术项目质量本质上是无人长期、持续关心的,你可能会说,技术 Leader 会关心呀?但这和业务驱动相比实在是太弱了。产品有用户侧钞票的投票,无论管理者换多少人,还是会从源头持续提供动力,但项目质量总是要反复强调,间歇性整治,并且不同的 Leader 关心程度也不同,因为这背后没有源动力,除非项目质量影响到用户那头的现金供给了,但这种情况发生时,说明项目早已烂透了。
正是因为技术质量缺乏源动力,或者说源动力传导链路太长,我们才要人为的不断加强重视,重视文档、重视使用体验、重视是否符合设计模式。只有长期主义者才能坚持做代码质量治理,因为坚信总有一天,代码质量会影响到业务发展。
## 总结
这次从真实世界借鉴了一些经验到软件世界,我们从借鉴真实世界的屏蔽复杂度,谈到了为什么真实世界的说明书这么好用,但技术项目文档却总是缺胳膊少腿的问题。
我们总结出的经验是,设计原则与设计模式固然可以提升可维护性,但归根结底还是动力的问题,提升代码质量本身就是一件缺乏动力去做的事,或者长期被认为是重要不紧急的事,往往很难找出理由现在就去做,但没有人觉得不应该做。
所以想要提升可维护性,找到为什么现在,立刻,马上就要做技术优化的原因,并立即开始优化才是最重要的。
> 讨论地址是:[精读《可维护性思考》· Issue #359 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/359)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,569 @@
ORM(Object relational mappers) 的含义是,将数据模型与 Object 建立强力的映射关系,这样我们对数据的增删改查可以转换为操作 Object(对象)。
Prisma 是一个现代 Nodejs ORM 库,根据 [Prisma 官方文档](https://www.prisma.io/docs/concepts/overview/what-is-prisma) 可以了解这个库是如何设计与使用的。
## 概述
Prisma 提供了大量工具,包括 Prisma Schema、Prisma Client、Prisma Migrate、Prisma CLI、Prisma Studio 等,其中最核心的两个是 Prisma Schema 与 Prisma Client,分别是描述应用数据模型与 Node 操作 API。
与一般 ORM 完全由 Class 描述数据模型不同,Primsa 采用了一个全新语法 Primsa Schema 描述数据模型,再执行 `prisma generate` 产生一个配置文件存储在 `node_modules/.prisma/client` 中,Node 代码里就可以使用 Prisma Client 对数据增删改查了。
### Prisma Schema
Primsa Schema 是在最大程度贴近数据库结构描述的基础上,对关联关系进行了进一步抽象,并且背后维护了与数据模型的对应关系,下图很好的说明了这一点:
<img width=400 src="https://z3.ax1x.com/2021/10/17/5YwZoF.png">
可以看到,几乎与数据库的定义一模一样,唯一多出来的 `posts``author` 其实是弥补了数据库表关联外键中不直观的部分,将这些外键转化为实体对象,让操作时感受不到外键或者多表的存在,在具体操作时再转化为 join 操作。下面是对应的 Prisma Schema:
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
```
`datasource db` 申明了链接数据库信息;`generator client` 申明了使用 Prisma Client 进行客户端操作,也就是说 Prisma Client 其实是可以替换实现的;`model` 是最核心的模型定义。
在模型定义中,可以通过 `@map` 修改字段名映射、`@@map` 修改表名映射,默认情况下,字段名与 key 名相同:
```prisma
model Comment {
title @map("comment_title")
@@map("comments")
}
```
字段由下面四种描述组成:
- 字段名。
- 字段类型。
- 可选的类型修饰。
- 可选的属性描述。
```prisma
model Tag {
name String? @id
}
```
在这个描述里,包含字段名 `name`、字段类型 `String`、类型修饰 `?`、属性描述 `@id`
#### 字段类型
字段类型可以是 model,比如关联类型字段场景:
```prisma
model Post {
id Int @id @default(autoincrement())
// Other fields
comments Comment[] // A post can have many comments
}
model Comment {
id Int
// Other fields
Post Post? @relation(fields: [postId], references: [id]) // A comment can have one post
postId Int?
}
```
关联场景有 1v1, nv1, 1vn, nvn 四种情况,字段类型可以为定义的 model 名称,并使用属性描述 `@relation` 定义关联关系,比如上面的例子,描述了 `Commenct``Post` 存在 nv1 关系,并且 `Comment.postId``Post.id` 关联。
字段类型还可以是底层数据类型,通过 `@db.` 描述,比如:
```prisma
model Post {
id @db.TinyInt(1)
}
```
对于 Prisma 不支持的类型,还可以使用 `Unsupported` 修饰:
```prisma
model Post {
someField Unsupported("polygon")?
}
```
这种类型的字段无法通过 ORM API 查询,但可以通过 `queryRaw` 方式查询。`queryRaw` 是一种 ORM 对原始 SQL 模式的支持,在 Prisma Client 会提到。
#### 类型修饰
类型修饰有 `?` `[]` 两种语法,比如:
```prisma
model User {
name String?
posts Post[]
}
```
分别表示可选与数组。
#### 属性描述
属性描述有如下几种语法:
```prisma
model User {
id Int @id @default(autoincrement())
isAdmin Boolean @default(false)
email String @unique
@@unique([firstName, lastName])
}
```
`@id` 对应数据库的 PRIMARY KEY。
`@default` 设置字段默认值,可以联合函数使用,比如 `@default(autoincrement())`,可用函数包括 `autoincrement()``dbgenerated()``cuid()``uuid()``now()`,还可以通过 `dbgenerated` 直接调用数据库底层的函数,比如 `dbgenerated("gen_random_uuid()")`
`@unique` 设置字段值唯一。
`@relation` 设置关联,上面已经提到过了。
`@map` 设置映射,上面也提到过了。
`@updatedAt` 修饰字段用来存储上次更新时间,一般是数据库自带的能力。
`@ignore` 对 Prisma 标记无效的字段。
所有属性描述都可以组合使用,并且还存在需对 model 级别的描述,一般用两个 `@` 描述,包括 `@@id``@@unique``@@index``@@map``@@ignore`
#### ManyToMany
Prisma 在多对多关联关系的描述上也下了功夫,支持隐式关联描述:
```prisma
model Post {
id Int @id @default(autoincrement())
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
```
看上去很自然,但其实背后隐藏了不少实现。数据库多对多关系一般通过第三张表实现,第三张表会存储两张表之间外键对应关系,所以如果要显式定义其实是这样的:
```prisma
model Post {
id Int @id @default(autoincrement())
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
posts CategoriesOnPosts[]
}
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int // relation scalar field (used in the `@relation` attribute above)
category Category @relation(fields: [categoryId], references: [id])
categoryId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
```
背后生成如下 SQL
```sql
CREATE TABLE "Category" (
id SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
id SERIAL PRIMARY KEY
);
-- Relation table + indexes -------------------------------------------------------
CREATE TABLE "CategoryToPost" (
"categoryId" integer NOT NULL,
"postId" integer NOT NULL,
"assignedBy" text NOT NULL
"assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ("categoryId") REFERENCES "Category"(id),
FOREIGN KEY ("postId") REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);
```
### Prisma Client
描述好 Prisma Model 后,执行 `prisma generate`,再利用 `npm install @prisma/client` 安装好 Node 包后,就可以在代码里操作 ORM 了:
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```
#### CRUD
使用 `create` 创建一条记录:
```typescript
const user = await prisma.user.create({
data: {
email: 'elsa@prisma.io',
name: 'Elsa Prisma',
},
})
```
使用 `createMany` 创建多条记录:
```typescript
const createMany = await prisma.user.createMany({
data: [
{ name: 'Bob', email: 'bob@prisma.io' },
{ name: 'Bobo', email: 'bob@prisma.io' }, // Duplicate unique key!
{ name: 'Yewande', email: 'yewande@prisma.io' },
{ name: 'Angelique', email: 'angelique@prisma.io' },
],
skipDuplicates: true, // Skip 'Bobo'
})
```
使用 `findUnique` 查找单条记录:
```typescript
const user = await prisma.user.findUnique({
where: {
email: 'elsa@prisma.io',
},
})
```
对于联合索引的情况:
```prisma
model TimePeriod {
year Int
quarter Int
total Decimal
@@id([year, quarter])
}
```
需要再嵌套一层由 `_` 拼接的 key
```typescript
const timePeriod = await prisma.timePeriod.findUnique({
where: {
year_quarter: {
quarter: 4,
year: 2020,
},
},
})
```
使用 `findMany` 查询多条记录:
```typescript
const users = await prisma.user.findMany()
```
可以使用 SQL 中各种条件语句,语法如下:
```typescript
const users = await prisma.user.findMany({
where: {
role: 'ADMIN',
},
include: {
posts: true,
},
})
```
使用 `update` 更新记录:
```typescript
const updateUser = await prisma.user.update({
where: {
email: 'viola@prisma.io',
},
data: {
name: 'Viola the Magnificent',
},
})
```
使用 `updateMany` 更新多条记录:
```typescript
const updateUsers = await prisma.user.updateMany({
where: {
email: {
contains: 'prisma.io',
},
},
data: {
role: 'ADMIN',
},
})
```
使用 `delete` 删除记录:
```typescript
const deleteUser = await prisma.user.delete({
where: {
email: 'bert@prisma.io',
},
})
```
使用 `deleteMany` 删除多条记录:
```typescript
const deleteUsers = await prisma.user.deleteMany({
where: {
email: {
contains: 'prisma.io',
},
},
})
```
使用 `include` 表示关联查询是否生效,比如:
```typescript
const getUser = await prisma.user.findUnique({
where: {
id: 19,
},
include: {
posts: true,
},
})
```
这样就会在查询 `user` 表时,顺带查询所有关联的 `post` 表。关联查询也支持嵌套:
```typescript
const user = await prisma.user.findMany({
include: {
posts: {
include: {
categories: true,
},
},
},
})
```
筛选条件支持 `equals``not``in``notIn``lt``lte``gt``gte``contains``search``mode``startsWith``endsWith``AND``OR``NOT`,一般用法如下:
```typescript
const result = await prisma.user.findMany({
where: {
name: {
equals: 'Eleanor',
},
},
})
```
这个语句代替 sql 的 `where name="Eleanor"`,即通过对象嵌套的方式表达语义。
Prisma 也可以直接写原生 SQL
```typescript
const email = 'emelie@prisma.io'
const result = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM User WHERE email = ${email}`
)
```
### 中间件
Prisma 支持中间件的方式在执行过程中进行拓展,看下面的例子:
```typescript
const prisma = new PrismaClient()
// Middleware 1
prisma.$use(async (params, next) => {
console.log(params.args.data.title)
console.log('1')
const result = await next(params)
console.log('6')
return result
})
// Middleware 2
prisma.$use(async (params, next) => {
console.log('2')
const result = await next(params)
console.log('5')
return result
})
// Middleware 3
prisma.$use(async (params, next) => {
console.log('3')
const result = await next(params)
console.log('4')
return result
})
const create = await prisma.post.create({
data: {
title: 'Welcome to Prisma Day 2020',
},
})
const create2 = await prisma.post.create({
data: {
title: 'How to Prisma!',
},
})
```
输出如下:
```text
Welcome to Prisma Day 2020
1
2
3
4
5
6
How to Prisma!
1
2
3
4
5
6
```
可以看到,中间件执行顺序是洋葱模型,并且每个操作都会触发。我们可以利用中间件拓展业务逻辑或者进行操作时间的打点记录。
## 精读
### ORM 的两种设计模式
ORM 有 Active Record 与 Data Mapper 两种设计模式,其中 Active Record 使对象背后完全对应 sql 查询,现在已经不怎么流行了,而 Data Mapper 模式中的对象并不知道数据库的存在,即中间多了一层映射,甚至背后不需要对应数据库,所以可以做一些很轻量的调试功能。
Prisma 采用了 Data Mapper 模式。
### ORM 容易引发性能问题
当数据量大,或者性能、资源敏感的情况下,我们需要对 SQL 进行优化,甚至我们需要对特定的 Mysql 的特定版本的某些内核错误,对 SQL 进行某些看似无意义的申明调优(比如在 where 之前再进行相同条件的 IN 范围限定),有的时候能取得惊人的性能提升。
而 ORM 是建立在一个较为理想化理论基础上的,即数据模型可以很好的转化为对象操作,然而对象操作由于屏蔽了细节,我们无法对 SQL 进行针对性调优。
另外,得益于对象操作的便利性,我们很容易通过 obj.obj. 的方式访问某些属性,但这背后生成的却是一系列未经优化(或者部分自动优化)的复杂 join sql,我们在写这些 sql 时会提前考虑性能因素,但通过对象调用时却因为成本低,或觉得 ORM 有 magic 优化等想法,写出很多实际上不合理的 sql。
### Prisma Schema 的好处
其实从语法上,Prisma Schema 与 Typeorm 基于 Class + 装饰器的拓展几乎可以等价转换,但 Prisma Schema 在实际使用中有一个很不错的优势,即减少样板代码以及稳定数据库模型。
减少样板代码比较好理解,因为 Prisma Schema 并不会出现在代码中,而稳定模型是指,只要不执行 `prisma generate`,数据模型就不会变化,而且 Prisma Schema 也独立于 Node 存在,甚至可以不放在项目源码中,相比之下,修改起来会更加慎重,而完全用 Node 定义的模型因为本身是代码的一部分,可能会突然被修改,而且也没有执行数据库结构同步的操作。
如果项目采用 Prisma,则模型变更后,可以执行 `prisma db pull` 更新数据库结构,再执行 `prisma generate` 更新客户端 API,这个流程比较清晰。
## 总结
Prisma Schema 是 Prisma 的一大特色,因为这部分描述独立于代码,带来了如下几个好处:
1. 定义比 Node Class 更简洁。
2. 不生成冗余的代码结构。
3. Prisma Client 更加轻量,且查询返回的都是 Pure Object。
至于 Prisma Client 的 API 设计其实并没有特别突出之处,无论与 [sequelize](https://sequelize.org/master/) 还是 [typeorm](https://typeorm.io/#/) 的 API 设计相比,都没有太大的优化,只是风格不同。
不过对于记录的创建,我更喜欢 Prisma 的 API
```typescript
// typeorm - save API
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
// typeorm - insert API
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
// sequelize
const user = User.build({
name: 'Alice',
})
await user.save()
// Mongoose
const user = await User.create({
name: 'Alice',
email: 'alice@prisma.io',
})
// prisma
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
```
首先存在 `prisma` 这个顶层变量,使用起来会非常方便,另外从 API 拓展上来说,虽然 Mongoose 设计得更简洁,但添加一些条件时拓展性会不足,导致结构不太稳定,不利于统一记忆。
Prisma Client 的 API 统一采用下面这种结构:
```typescript
await prisma.modelName.operateName({
// 数据,比如 create、update 时会用到
data: /** ... */,
// 条件,大部分情况都可以用到
where: /** ... */,
// 其它特殊参数,或者 operater 特有的参数
})
```
所以总的来说,Prisma 虽然没有对 ORM 做出革命性改变,但在微创新与 API 优化上都做得足够棒,github 更新也比较活跃,如果你决定使用 ORM 开发项目,还是比较推荐 Prisma 的。
在实际使用中,为了规避 ORM 产生笨拙 sql 导致的性能问题,可以利用 Prisma Middleware 监控查询性能,并对性能较差的地方采用 `prisma.$queryRaw` 原生 sql 查询。
> 讨论地址是:[精读《Prisma 的使用》· Issue #362 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/362)
**如果你想参与讨论,请 [点击这里](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)
+300
View File
@@ -0,0 +1,300 @@
Node stream 比较难理解,也比较难用,但 “流” 是个很重要而且会越来越常见的概念(`fetch` 返回值就是流),所以我们有必要认真学习 stream。
好在继 node stream 之后,又推出了比较好用,好理解的 web streams API,我们结合 [Web Streams Everywhere (and Fetch for Node.js)](https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/)、[2016 - the year of web streams](https://jakearchibald.com/2016/streams-ftw/)、[ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)、[WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) 这几篇文章学一下。
> node stream 与 web stream 可以相互转换:`.fromWeb()` 将 web stream 转换为 node stream`.toWeb()` 将 node stream 转换为 web stream。
## 精读
stream(流)是什么?
stream 是一种抽象 API。我们可以和 promise 做一下类比,如果说 promise 是异步标准 API,则 stream 希望成为 I/O 的标准 API。
什么是 I/O?就是输入输出,即信息的读取与写入,比如看视频、加载图片、浏览网页、编码解码器等等都属于 I/O 场景,所以并不一定非要大数据量才算 I/O,比如读取一个磁盘文件算 I/O,同样读取 `"hello world"` 字符串也可以算 I/O。
stream 就是当下对 I/O 的标准抽象。
为了更好理解 stream 的 API 设计,以及让你理解的更深刻,我们先自己想一想一个标准 I/O API 应该如何设计?
### I/O 场景应该如何抽象 API
`read()``write()` 是我们第一个想到的 API,继续补充的话还有 `open()``close()` 等等。
这些 API 确实可以称得上 I/O 场景标准 API,而且也足够简单。但这些 API 有一个不足,就是缺乏对大数据量下读写的优化考虑。什么是大数据量的读写?比如读一个几 GB 的视频文件,在 2G 慢网络环境下访问网页,这些情况下,如果我们只有 `read``write` API,那么可能一个读取命令需要 2 个小时才能返回,而一个写入命令需要 3 个小时执行时间,同时对用户来说,不论是看视频还是看网页,都无法接受这么长的白屏时间。
但为什么我们看视频和看网页的时候没有等待这么久?因为看网页时,并不是等待所有资源都加载完毕才能浏览与交互的,许多资源都是在首屏渲染后再异步加载的,视频更是如此,我们不会加载完 30GB 的电影后再开始播放,而是先下载 300kb 片头后就可以开始播放了。
无论是视频还是网页,为了快速响应内容,资源都是 **在操作过程中持续加载的**,如果我们设计一个支持这种模式的 API,无论资源大还是小都可以覆盖,自然比 `read``wirte` 设计更合理。
这种持续加载资源的行为就是 stream(流)。
### 什么是 stream
stream 可以认为在形容资源持续流动的状态,我们需要把 I/O 场景看作一个持续的场景,就像把一条河的河水导流到另一条河。
做一个类比,我们在发送 http 请求、浏览网页、看视频时,可以看作一个南水北调的过程,把 A 河的水持续调到 B 河。
在发送 http 请求时,A 河就是后端服务器,B 河就是客户端;浏览网页时,A 河就是别人的网站,B 河就是你的手机;看视频时,A 河是网络上的视频资源(当然也可能是本地的),B 河是你的视频播放器。
所以流是一个持续的过程,而且可能有多个节点,不仅网络请求是流,资源加载到本地硬盘后,读取到内存,视频解码也是流,所以这个南水北调过程中还有许多中途蓄水池节点。
将这些事情都考虑到一起,最后形成了 web stream API。
一共有三种流,分别是:writable streams、readable streams、transform streams,它们的关系如下:
<img width=400 src="https://z3.ax1x.com/2021/10/25/55kQtP.png">
- readable streams 代表 A 河流,是数据的源头,因为是数据源头,所以只可读不可写。
- writable streams 代表 B 河流,是数据的目的地,因为要持续蓄水,所以是只可写不可读。
- transform streams 是中间对数据进行变换的节点,比如 A 与 B 河中间有一个大坝,这个大坝可以通过蓄水的方式控制水运输的速度,还可以安装滤网净化水源,所以它一头是 writable streams 输入 A 河流的水,另一头提供 readable streams 供 B 河流读取。
乍一看很复杂的概念,但映射到河水引流就非常自然了,stream 的设计非常贴近生活概念。
要理解 stream,需要思考下面三个问题:
1. readable streams 从哪来?
2. 是否要使用 transform streams 进行中间件加工?
3. 消费的 writable streams 逻辑是什么?
还是再解释一下,为什么相比 `read()``write()`stream 要多这三个思考:stream 既然将 I/O 抽象为流的概念,也就是具有持续性,那么读取的资源就必须是一个 readable 流,所以我们要构造一个 readable streams(未来可能越来越多函数返回值就是流,也就是在流的环境下工作,就不用考虑如何构造流了)。对流的读取是一个持续的过程,所以不是调用一个函数一次性读取那么简单,因此 writable streams 也有一定 API 语法。正是因为对资源进行了抽象,所以无论是读取还是消费,都被包装了一层 stream API,而普通的 `read` 函数读取的资源都是其本身,所以才没有这些额外思维负担。
好在 web streams API 设计都比较简单易用,而且作为一种标准规范,更加有掌握的必要,下面分别说明:
### readable streams
读取流不可写,所以只有初始化时才能设置值:
```typescript
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('h')
controller.enqueue('e')
controller.enqueue('l')
controller.enqueue('l')
controller.enqueue('o')
controller.close()
}
})
```
`controller.enqueue()` 可以填入任意值,相当于是将值加入队列,`controller.close()` 关闭后,就无法继续 `enqueue` 了,并且这里的关闭时机,会在 writable streams 的 `close` 回调响应。
上面只是 mock 的例子,实际场景中,读取流往往是一些调用函数返回的对象,最常见的就是 `fetch` 函数:
```typescript
async function fetchStream() {
const response = await fetch('https://example.com')
const stream = response.body;
}
```
可见,`fetch` 函数返回的 `response.body` 就是一个 readable stream。
我们可以通过以下方式直接消费读取流:
```typescript
readableStream.getReader().read().then({ value, done } => {})
```
也可以 `readableStream.pipeThrough(transformStream)` 到一个转换流,也可以 `readableStream.pipeTo(writableStream)` 到一个写入流。
不管是手动 mock 还是函数返回,我们都能猜到,**读取流不一定一开始就充满数据**,比如 `response.body` 就可能因为读的比较早而需要等待,就像接入的水管水流较慢,而源头水池的水很多一样。我们也可以手动模拟读取较慢的情况:
```typescript
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('h')
controller.enqueue('e')
setTimeout(() => {
controller.enqueue('l')
controller.enqueue('l')
controller.enqueue('o')
controller.close()
}, 1000)
}
})
```
上面例子中,如果我们一开始就用写入流对接,必然要等待 1s 才能得到完整的 `'hello'` 数据,但如果 1s 后再对接写入流,那么瞬间就能读取整个 `'hello'`。另外,写入流可能处理的速度也会慢,如果写入流处理每个单词的时间都是 1s,那么写入流无论何时执行,都比读取流更慢。
所以可以体会到,流的设计就是为了让整个数据处理过程最大程度的高效,无论读取流数据 ready 的多迟、开始对接写入流的时间有多晚、写入流处理的多慢,整个链路都是尽可能最高效的:
- 如果 readableStream ready 的迟,我们可以晚一点对接,让 readableStream 准备好再开始快速消费。
- 如果 writableStream 处理的慢,也只是这一处消费的慢,对接的 “水管” readableStream 可能早就 ready 了,此时换一个高效消费的 writableStream 就能提升整体效率。
### writable streams
写入流不可读,可以通过如下方式创建:
```typescript
const writableStream = new WritableStream({
write(chunk) {
return new Promise(resolve => {
// 消费的地方,可以执行插入 dom 等等操作
console.log(chunk)
resolve()
});
},
close() {
// 可读流 controller.close() 时,这里被调用
},
})
```
写入流不用关心读取流是什么,所以只要关心数据写入就行了,实现写入回调 `write`
`write` 回调需要返回一个 Promise,所以如果我们消费 `chunk` 的速度比较慢,写入流执行速度就会变慢,我们可以理解为 A 河流引水到 B 河流,就算 A 河流的河道很宽,一下就把河水全部灌入了,但 B 河流的河道很窄,无法处理那么大的水流量,所以受限于 B 河流河道宽度,整体水流速度还是比较慢的(当然这里不可能发生洪灾)。
那么 writableStream 如何触发写入呢?可以通过 `write()` 函数直接写入:
```typescript
writableStream.getWriter().write('h')
```
也可以通过 `pipeTo()` 直接对接 readableStream,就像本来是手动滴水,现在直接对接一个水管,这样我们只管处理写入就行了:
```typescript
readableStream.pipeTo(writableStream)
```
当然通过最原始的 API 也可以拼装出 `pipeTo` 的效果,为了理解的更深刻,我们用原始方法模拟一个 `pipeTo`
```typescript
const reader = readableStream.getReader()
const writer = writableStream.getWriter()
function tryRead() {
reader.read().then(({ done, value }) => {
if (done) {
return
}
writer.ready().then(() => writer.write(value))
tryRead()
})
}
tryRead()
```
### transform streams
转换流内部是一个写入流 + 读取流,创建转换流的方式如下:
```typescript
const decoder = new TextDecoder()
const decodeStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(decoder.decode(chunk, {stream: true}))
}
})
```
`chunk` 是 writableStream 拿到的包,`controller.enqueue` 是 readableStream 的入列方法,所以它其实底层实现就是两个流的叠加,API 上简化为 `transform` 了,可以一边写入读到的数据,一边转化为读取流,供后面的写入流消费。
当然有很多原生的转换流可以用,比如 `TextDecoderStream`
```typescript
const textDecoderStream = TextDecoderStream()
```
### readable to writable streams
下面是一个包含了编码转码的完整例子:
```typescript
// 创建读取流
const readableStream = new ReadableStream({
start(controller) {
const textEncoder = new TextEncoder()
const chunks = textEncoder.encode('hello', { stream: true })
chunks.forEach(chunk => controller.enqueue(chunk))
controller.close()
}
})
// 创建写入流
const writableStream = new WritableStream({
write(chunk) {
const textDecoder = new TextDecoder()
return new Promise(resolve => {
const buffer = new ArrayBuffer(2);
const view = new Uint16Array(buffer);
view[0] = chunk;
const decoded = textDecoder.decode(view, { stream: true });
console.log('decoded', decoded)
setTimeout(() => {
resolve()
}, 1000)
});
},
close() {
console.log('writable stream close')
},
})
readableStream.pipeTo(writableStream)
```
首先 readableStream 利用 `TextEncoder` 以极快的速度瞬间将 `hello` 这 5 个字母加入队列,并执行 `controller.close()`,意味着这个 readableStream 瞬间就完成了初始化,并且后面无法修改,只能读取了。
我们在 writableStream 的 `write` 方法中,利用 `TextDecoder``chunk` 进行解码,一次解码一个字母,并打印到控制台,然后过了 1s 才 `resolve`,所以写入流会每隔 1s 打印一个字母:
```shell
h
# 1s later
e
# 1s later
l
# 1s later
l
# 1s later
o
writable stream close
```
这个例子转码解码处理的还不够优雅,我们不需要将转码与解码写在流函数里,而是写在转换流中,比如:
```typescript
readableStream
.pipeThrough(new TextEncoderStream())
.pipeThrough(customStream)
.pipeThrough(new TextDecoderStream())
.pipeTo(writableStream)
```
这样 readableStream 与 writableStream 都不需要处理编码与解码,但流在中间被转化为了 Uint8Array,方便被其它转换流处理,最后经过解码转换流转换为文字后,再 `pipeTo` 给写入流,这样写入流拿到的就是文字了。
但也并不总是这样,比如我们要传输一个视频流,可能 readableStream 原始值就已经是 Uint8Array,所以具体要不要对接转换流看情况。
## 总结
streams 是对 I/O 抽象的标准处理 API,其支持持续小片段数据处理的特性并不是偶然,而是对 I/O 场景进行抽象后的必然。
我们通过水流的例子类比了 streams 的概念,当 I/O 发生时,源头的流转换是有固定速度的 x M/s,目标客户端比如视频的转换也是有固定速度的 y M/s,网络请求也有速度并且是个持续的过程,所以 `fetch` 天然也是一个流,速度时 z M/s,我们最终看到视频的速度就是 `min(x, y, z)`,当然如果服务器提前将 readableStream 提供好,那么 x 的速度就可以忽略,此时看到视频的速度是 `min(y, z)`
不仅视频如此,打开文件、打开网页等等都是如此,浏览器处理 html 也是一个流的过程:
```typescript
new Response(stream, {
headers: { 'Content-Type': 'text/html' },
})
```
如果这个 readableStream 的 `controller.enqueue` 过程被刻意处理的比较慢,网页甚至可以一个字一个字的逐步呈现:[Serving a string, slowly Demo](https://jakearchibald.github.io/isserviceworkerready/demos/simple-stream/)。
尽管流的场景如此普遍,但也没有必要将所有代码都改成流式处理,因为代码在内存中执行速度很快,变量的赋值是没必要使用流处理的,但如果这个变量的值来自于一个打开的文件,或者网络请求,那么使用流进行处理是最高效的。
> 讨论地址是:[精读《web streams》· Issue #363 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/363)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,147 @@
LOD 表达式在数据分析领域很常用,其全称为 Level Of Detail,即详细级别。
## 精读
什么是详细级别,为什么需要 LOD?你一定会有这个问题,我们来一步步解答。
### 什么是详细级别
可以尝试这么发问:你这个数据有多详细?
得到的回答可能是:
1. 数据是汇总的,抱歉看不到细节,不过如果您正好要看总销量的话,这儿都给您汇总好了。。
2. 详细?这直接就是原始表数据,30 亿条,这够详细了吧?如果觉得还不够详细,那只好把业务过程再拆分一下重新埋点了。
详细程度越高,数据量越大,详细程度越低,数据就越少,就越是汇总的数据。
人很难在详细程度很高的 30 亿条记录里看到有价值的信息,所以数据分析的过程也可以看作是 **对数据汇总计算的过程,这背后数据详细程度在逐渐降低**
### BI 工具的详细级别
如果没有 LOD 表达式,一个 BI 查询的详细程度是完全固定的:
- 如果表格拖入度量,没有维度,那就是最高详细级别,因为最终只会汇总出一条记录。
- 如果折线图拖入维度,那结果就是根据这个维度内分别聚合度量,数据更详细了,详细粒度为当前维度,比如日期。
如果我们要更详细的数据,就需要在维度上拖入更多字段,直到达到最详细的明细表级别的粒度。然而同一个查询不可能包含不同详细粒度,因为详细粒度由维度组合决定,不可改变,比如下面表格的例子:
```text
行:国家 省 城市
列:GDP
```
这个例子中,详细级别限定在了城市这一级汇总,城市下更细粒度的数据就看不到了,每一条数据都是城市粒度的,我们不可能让查询结果里出现按照国家汇总的 GDP,或者看到更详细粒度的每月 GDP 信息,更不可能让城市粒度的 GDP 与国家粒度 GDP 在一起做计算,算出城市 GDP 在国家中占比。
但是,类似上面例子的需求是很多的,而且很常见,BI 工具必须想出一种解法,因此诞生了 LOD:**LOD 就是一种表达式,允许我们在一个查询中描述不同的详细粒度**。
### 从表达式计算来看详细级别
表达式计算必须限定在同样的详细粒度,这是铁律,为什么呢?
试想一下下面两张不同详细粒度的表:
`总销售额`
```text
10000
```
`各城市销售额`
```text
北京 3000
上海 7000
```
如果我们想在各城市销售额中,计算贡献占比,那么就要写出 `[各城市销售额] / [总销售额]` 的计算公式,但显然这是不可能的,因为前者有两条数据,后者只有一条数据,根本无法计算。
我们能做的一定是数据行数相同,那么无论是 IF ELSE、CASE WHEN,还是加减乘除都可以按照行粒度进行了。
LOD 给了我们跨详细粒度计算的能力,其本质还是将数据详细粒度统一,但我们可以让某列数据来自于一个完全不同详细级别的计算:
```text
城市 销售额 总销售额
北京 3000 10000
上海 7000 10000
```
如图表,LOD 可以把数据加工成这样,即虽然总销售额与城市详细粒度不同,但还是添加到了每一行的末尾,这样就可以进行计算了。
**因此 LOD 可以按照任意详细级别进行计算,将最终产出 “贴合” 到当前查询的详细级别中。**
LOD 表达式分为三种能力,分别是 FIXED、INCLUDE、EXCLUDE。
### FIXED
```text
{ fixed [省份] : sum([GDP]) }
```
按照城市这个固定详细粒度,计算每个省份的 DGP,最后合并到当前详细粒度里。
假如现在的查询粒度是省份、城市,那么 LOD 字段的添加逻辑如下图所示:
![](https://z3.ax1x.com/2021/10/31/ISJ9JK.png)
可见,本质是两个不同 sql 查询后 join 的结果,内部的 `sum` 表示在 FIXED 表达式内的聚合方式,外部的 `sum` 表示,如果 FIXED 详细级别比当前视图详细级别低,应该如何聚合。在这个例子中,FIXED 详细级别较高,所以 `sum` 不起作用,换成 `avg` 效果也相同,因为合并详细级别是,是一对多关系,只有合并时多对一关系才需要聚合。
最外层聚合方式一般在 INCLUDE 表达式中发挥作用。
### EXCLUDE
```text
{ exclude [城市] : sum([GDP]) }
```
在当前查询粒度中,排除城市这个粒度后计算 GDP,最后合并到当前详细粒度中。
假如现在的查询粒度是省份、城市、季节,那么 LOD 字段的添加逻辑如下图所示:
![](https://z3.ax1x.com/2021/10/31/ISGzIx.png)
如图所示,EXCLUDE 在当前视图详细级别的基础上,排除一些维度,所得到的详细级别一定会更高。
### INCLUDE
```text
{ include [城乡] : avg([GDP]) }
```
在当前查询粒度中,额外加上城乡这个粒度后计算 GDP,最后合并到当前详细粒度中。
这类的例子比较难理解,且在 `sum` 情况下一般无实际意义,因为计算结果不会有差异,必须在类似 `avg` 场景下才有意义,我们还是结合下图来看:
![](https://z3.ax1x.com/2021/10/31/ISGvZR.png)
这就是 avg 算不准的问题,即不同详细级别计算的平均值是不同的,但 sum、count 等不会随着详细级别变化而影响计算结果,所以当涉及到 avg 计算时,可以通过 INCLUDE 表达式指定计算的详细级别,以保证数据口径准确性。
### LOD 字段怎么用
除了上面的例子中,直接查出来展示给用户外,LOD 字段更常用的是作为中间计算过程,比如计算省份 GDP 占在国内占比。因为 LOD 已经将不同详细粒度计算结果合并到了当前的详细粒度里,所以如下的计算表达式:
```text
sum([GDP]) / sum({ fixed [国家] : sum([GDP]) })
```
看似是跨详细粒度计算,其实没有,实际计算时还是一行一行来算的,后面的 **LOD 表达式只是在逻辑上按照指定的详细粒度计算,但最终会保持与当前视图详细粒度一致**,因此可以参与计算。
我们后面会继续解读 tableau 整理的 Top 15 LOD 表达式业务场景,更深入的理解 LOD 表达式。
## 总结
LOD 表达式让你轻松创建 “脱离” 当前视图详细级别的计算字段。
或许你会疑惑,为什么不主动改变当前视图详细级别来实现同样的效果?比如新增或减少一个维度。
原因是,LOD 往往用于跨详细级别的计算,比如算部分相对总体的占比,计算当条记录是否为用户首单等等,更多的场景会在下次精读中解读。
> 讨论地址是:[精读《什么是 LOD 表达式》· Issue #365 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/365)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,127 @@
通过上一篇 [精读《什么是 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) 的学习,你已经理解了什么是 LOD 表达式。为了巩固理解,结合场景复习是最有效的手段,所以这次我们结合 [Top 15 LOD Expressions](https://www.tableau.com/about/blog/LOD-expressions) 这篇文章学习 LOD 表达式的 15 大应用场景,因篇幅限制,本文介绍 1~8 场景。
## 1. 客户下单频次
**各下单次数的顾客数量是多少?**
柱状图的 Y 轴显然是 `count([customerID])`,因为要统计 **当前维度下的客户总数**
> 这里插一句,对于柱状图的 Y 轴,在 sql 里就是对 X 轴 `group by` 后的聚合,因此 Y 轴就是对 X 轴各项的汇总。
柱状图的 X 轴要表达的是以何种粒度拆解,比如我们是看各城市数据,还是看各省数据。在这个场景下也不例外,我们要看 **各下单次数下的数据**,那么如何把下单次数转化为维度呢?
我们需要用 FIX 表达式制作一个维度字段,表示各顾客下单次数。很显然数据库是没有这个维度的,而且这个维度需要按照客户 ID group by 后,按照订单 ID count 聚合才能得到,因此可以利用 FIX 表达式:`{ fixed [customerID] : count([orderId]) }` 描述。
![](https://z3.ax1x.com/2021/11/05/IKeDBV.png)
## 2. 阵列分析
当我们看年客户销售量时,即便是逐年增长的,我们也会有一个疑问:**每年销量中,首单在各年份的顾客分别贡献了多少?**
因为关系到老客忠诚度和新客拓展速度,新客与老客差距过大都不好,那我们如何让 2021 年的柱状图按照 2019、2020、2021 年首单的顾客分层呢?这就是阵列分析。
我们要画一个柱状图,X、Y 轴分别是 `[Year]``sum([Sales])`
为了让柱状图分层,我们需要一个表示颜色图例的维度字段,比如我们拖入已有的性别维度,每根柱子就会被划分为男、女两块。但问题是,我们制作并不存在的 “首单年份维度”?
答案是利用 FIX 表达式:`{ fixed [customerID] : min([orderDate]) }`
![](https://z3.ax1x.com/2021/11/05/IKnps1.png)
## 3. 日利润指标
分析 **每年各月份的盈利、亏损天数分布**。如下图:
![](https://z3.ax1x.com/2021/11/06/IQVeET.png)
列是年到月的下钻,比较好实现,只要拖入字段 `[year]` 并下钻到月粒度,移除季度粒度即可。
行是 “高收益”、“正收益”、“亏损” 的透视图,值是在当前月份中天数。
那么如何计算高收益、亏损状态呢?因为最终粒度是天,所以我们要按天计,首先就要得到每天的利润总和,这些中间过程可以利用 LOD 的字段来完成,即创建一个 **日利润字段(profitPerDay**`{ fixed [orderDate] : sum([profit]) }`
由于我们对利润总量不敏感,只希望拆分为三个阶段,所以利用 IF THEN 生成一个新字段 **日利润指标(dailyProfitKPI**`IF [profitPerDay] > 2000 THEN "Highly Profitable" ELSEIF [profitPerDay] <= 0 THEN "unprofitable" ELSE "profitable" END`
所以创建的 `[dailyProfitKPI]` 指标是个维度,即如果当前行所在的天利润汇总如果大于 2000,值就是 "Highly Profitable"。所以在行上拖入 `count(distinct [orderDate])`,把 `[dailyProfitKPI]` 拖入行的颜色透视即可。
## 4. 占总体百分比
LOD 表达式的一大特色就是计算跨详细级别的占比,比如我们要看 **欧洲各国的销量在全世界占比**
![](https://z3.ax1x.com/2021/11/06/IQmRPO.png)
显然这个图里所有国家之和不是 100%,因为欧洲加起来也才不到百分之二十,然而在当前详细级别下,是拿不到全球总销售量的,所以我们可以利用 FIX 表达式来实现:`sum([sales]) / max({ sum([sales]) })`
这里解释两点:
1. 之所以用 `max` 是因为 LOD 表达式只是一个字段,并没有聚合方式,运算必须在相同详细级别下进行,由于总销量只有一条数据,所以我们用 `max` 或者 `min` 甚至 `sum` 都行,结果都是一样的。
2. 如果不加维度限制,就可以省略 “fix” 申明,所以 `{ sum([sales]) }` 实际上就是 FIX 表达式,它表示 `{ fixed : sum([sales]) }`
## 5. 新客增长趋势
看着年客户增长趋势图,你有没有想过,这个趋势图肯定永远是向上的?也就是说,看着趋势图朝上走,不一定说明业务做得好。
如果公司每年都比去年发展的好,每年的新增新客数应该要比去年多,所以 **每年新客增长趋势图** 才比较有意义,如果你看到这个趋势图的趋势朝上,说明每年的新客都比去年多,说明公司摆脱了惯性,每年都获得了新的增长。
所以我们要加一个筛选条件。新增一个维度字段,当这一单客户是今年新客时为 true,否则为 false,这样我们筛选时,只看这个字段为 true 的结果就行了。
那么这个字段怎么来呢?思路是,获取客户首单年份,如果首单年份与当前下单年份相同,值为 true,否则为 false。
我们利用 LOD 创建首单年份字段 `[firstOrderDate]``{ fixed [customerId] : min([orderDate]) }`,然后创建筛选字段 `[newOrExist]`: `IFF([firstOrderDate] = [orderDate], 'true', 'false')`
## 6. 销量对比分析
入下图条形图所示,右侧是每项根据选择的分类的对比数据:
![](https://z3.ax1x.com/2021/11/06/IQ1mOe.png)
对比值计算方式是,用 **当前的销量减去当前选中分类的销量**。相信你可以猜到,但前分类的销量与当前视图详细级别无关,只与用户选择的 Category 有关。
如果我们已经有一个度量字段 - 选中分类销量 `selectedSales`,应该再排除当前 category 维度的干扰,所以可用 EXCLUDE 表达式描述 `selectedCategorySales`: `{ exclude [category] : sum([selectedSales]) }`
接下来是创建 `selectedSales` 字段。背景知识是 `[parameters].[category]` 可以获得当前选中的维度值,那我们可以写个 IF 表达式,在维度等于选中维度时聚合销量,不就是选中销量吗?所以公式是:`IF [category] = [parameters].[category] THEN sales ELSE 0 END`
最后对比差异,只要创建一个 `[diff]` 字段,表达式为 `sum(sales) - sum(selectedCategorySales)` 即可。
## 7. 平均最高交易额
如下图所示,当前的详细级别是国家,但我们却要展示每个国家平均最高交易额:
![](https://z3.ax1x.com/2021/11/06/IQGvN9.png)
显然,要求平均最高交易额,首先要计算每个销售代表的最高交易额,由于这个详细级别比国家低,我们可以利用 INCLUDE 表达式计算销售代表最高交易额 `largestSalesByRep`: `{ include [salesRep] : max([sales]) }`,并对这个度量字段求平均即可。
从这个例子可以看出,如果我们在一个较高的详细级别,比如国家,此时的 `sum([sales])` 是根据国家详细级别汇总的,而忽略了销售代表这个详细级别。但如果要展示每个国家的平均最高交易额,就必须在销售代表这个详细级别求 `max([sales])`,由于是各国家的,所以我们不用 `{ fixed [salesRep] }`,而是 `{ include [salesRep] }`,这样最终计算的详细级别是:`[country][salesRep]`,这样才能算出销售在每个国家的最高交易额(因为也许某些销售同时在不同国家销售)。
## 8. 实际与目标
在第六个例子 - 销量对比分析中,我们可以看到销量绝对值的对比,这次,我们需要计算实际销售额与目标的差距百分比:
![](https://z3.ax1x.com/2021/11/06/IQYHwF.png)
如上图所示,左上角展示了实际与目标的差值;右上角展示了每个地区产品目标完成率;下半部分展示了每个产品实际销量柱状图,并用黑色横线标记出目标值。
左上角非常简单,`[diffActualTraget]`: `[profit] - [targetProfit]`,只要将当前利润与目标利润相减即可。
右上角需要分为几步拆解。我们的最终目标是计算每个地区产品目标完成率,显然公式是 当前完成产品数/总产品数。总产品数比较简单,在已有地区维度拆解下,计算下产品总数就行了,即 `count(distinct [product])`;难点是当前完成产品数,这里我们又要用到 INCLUDE,为什么呢?因为地区粒度比产品粒度高,我们看地区汇总的时候,就不知道各产品的完成情况了,所以必须 INCLUDE product 维度计算利润目标差,公式是 `[diffProductActualTraget]` `{ include [product] : sum(diffActualTraget) }`,然后当这个值大于 0 就认为完成了目标,我们可以再创建一个字段,即完成目标数,如果达成目标就是 1,否则是 0,这样便于求 “当前完成产品数”:`aboveTargetProductCount`: `IFF([diffProductActualTraget] > 0, 1, 0)`,那么当前完成产品数就是 `sum([diffProductActualTraget])`,所以产品目标完成率就是 `sum([diffProductActualTraget]) / count(distinct [product])`,将这个字段拖入指标,按照百分比格式化,就得到结果了。
## 总结
通过上面的例子,我们可以总结出实际业务场景中几条使用心法:
1. 首先对计算公式进行拆解,判断拆解后的字段是否数据集里都有,如果都有的话就结束了,说明是个简单需求。
2. 如果数据集里没有,而且发现数据详细级别与当前不符(比如要得到每个国家销量,但当前维度是城市),就要用 FIXED 表达式固定详细级别。
3. 如果不是明确的按照某个详细级别计算,就不要使用 FIXED,因为不太灵活。
4. 当计算时要跳过某个指定详细级别,但又要保留视图里的详细级别时,使用 EXCLUDE 表达式。
5. 如果计算涉及到比视图低的详细级别,比如计算平均或者最大最小时,使用 INCLUDE 表达式。
6. 使用 FIXED 表达式创建的字段也可以进行二次计算,合理拆解多个计算字段并组合,会让逻辑更加清晰,易于理解。
> 讨论地址是:[精读《15 大 LOD 表达式 - 上》· Issue #369 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/369)
**如果你想参与讨论,请 [点击这里](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)
@@ -0,0 +1,183 @@
接着上一篇 [精读《15 大 LOD 表达式 - 上》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/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) ,这次继续总结 [Top 15 LOD Expressions](https://www.tableau.com/about/blog/LOD-expressions) 这篇文章的 915 场景。
## 9. 某时间段内最后一天的值
如何实现股票平均每日收盘价与当月最后一天收盘价的对比趋势图?
![](https://z3.ax1x.com/2021/11/13/IrbcKe.png)
如图所示,要对比的并非是某个时间段,而是当月最后一天的收盘价,因此必须要借助 LOD 表达式。
设想原表如下:
| Date | Ticker | Adj Close |
| ---- | ---- | ---- |
| 29/08/2013 | SYMC | $1 |
| 28/08/2013 | SYMC | $2 |
| 27/08/2013 | SYMC | $3 |
我们按照月进行聚合作为横轴,求 `avg([Adj Close])` 作为纵轴即可。但计算对比我们需要一个 Max Date 字段如下:
| Date | Ticker | Adj Close | Max, Date |
| ---- | ---- | ---- | ---- |
| 29/08/2013 | SYMC | $1 | 29/08/2013 |
| 28/08/2013 | SYMC | $2 | 29/08/2013 |
| 27/08/2013 | SYMC | $3 | 29/08/2013 |
如果我们使用 `max(Date)` 表达式,在聚合后结果是可以看到 Max Date 的:
| Month of Date | Ticker | Avg, Adj Close | Max, Date
| ---- | ---- | ---- | ---- |
| 08/2013 | SYMC | $2 | 29/08/2013 |
原因是,`max(Date)` 是一个聚合表达式,只能在 group by 聚合 sql 下生效。但如果我们要计算最后一天的收盘价,就要执行 `sum([Close value on last day]`,表达式如下:
`[Close value on last day] = if [Max Date] = [Date] then [Adj Close] else 0 end`
但问题是,这个表达式计算的明细级别是以天为粒度的,我们 `max(Date)` 在天粒度下是算不出来的:
| Date | Ticker | Adj Close | Max, Date |
| ---- | ---- | ---- | ---- |
| 29/08/2013 | SYMC | $1 | |
| 28/08/2013 | SYMC | $2 | |
| 27/08/2013 | SYMC | $3 | |
原因就是上面说过的,聚合表达式不能在非聚合的明细级别中出现。因此我们利用 `{ include : max([Date]) }` 表达式就能轻松实现下面的效果了:
| Date | Ticker | Adj Close | { include : max([Date]) } |
| ---- | ---- | ---- | ---- |
| 29/08/2013 | SYMC | $1 | 29/08/2013 |
| 28/08/2013 | SYMC | $2 | 29/08/2013 |
| 27/08/2013 | SYMC | $3 | 29/08/2013 |
`{ include : max([Date]) }` 表达式没有给定 include 参数,意味着永远以当前视图的明细级别计算,因此这个字段下推到明细表做计算时,也可以出现在明细表的每一行。接着按照上面的思路组装表达式即可。
拓展一下,如果横轴我们按年进行聚合,那么对比值就是每年最后一天的收盘价。原因是 `{ include : max([Date]) }` 会以当前年这个粒度计算 `max([Date])`,自然是当年的最后一天,然后下推到明细表,整整一年 365 行数据中,`[Close value on last day]` 大概是这样:
| Date | Ticker | Adj Close | [Close value on last day] |
| ---- | ---- | ---- | ---- |
| 31/12/2013 | SYMC | $1 | $1 |
| 30/12/2013 | SYMC | $2 | $1 |
| ... | ... | ... | ... |
| 03/01/2013 | SYMC | $7 | $1 |
| 02/01/2013 | SYMC | $8 | $1 |
| 01/01/2013 | SYMC | $9 | $1 |
接着对比值按照 `sum([Close value on last day])` 聚合即可。
## 10. 复购阵列
如下图所示,希望查看客户第一次购买到第二次购买间隔季度的复购阵列:
![](https://z3.ax1x.com/2021/11/13/Is2FGd.png)
关键在于如何求第一次与第二次购买的季度时间差。首先可以通过 `[1st purchase] = { fixed [customer id] : min([order date]) }` 计算每位客户首次购买时间。
如何计算第二次购买时间?这里有个小技巧。首先利用 `[repeat purchase] = iif([order date] > [1st purchase], [order date], null)` 得到一个新列,首次购买的那一行值为 null,我们可以利用 `min` 函数计算时忽略 null 的特性,得到第二次购买时间:`[2nd purchase] = { fixed [customer id] : min([repeat purchase]) }`
最后利用 `datediff` 函数得到间隔的季度数:`[quarters repeat to purchase] = datediff('quarter', [1st prechase], [2nd purchase])`
## 11. 范围平均值差异百分比
如下图所示,我们希望将趋势图的每个点,与选定区域(图中两个虚线范围内)的均值做一个差异百分比,并生成一个新的折线图放在上方。
![](https://z3.ax1x.com/2021/11/13/IsIuXd.png)
重点是上面折线图 y 轴字段,差异百分比如何表示。首先我们要生成一个只包含指定区间的收盘值:
`[Close value in reference period] = IF [Date] >= [Start reference date] AND [Date] <= [End reference date] THEN [Adj close] END`,这段表达式只在日期在制定区间内时,才返回 `[Adj close]`,也就是只包含这个区间内的值。
第二步,计算制定区间的平均值,这个用 FIX 表达式即可:`[Average daily close value between ref date] = { fixed [Ticker] : AVG([Close value in reference period]) }`
第三步,计算百分比差异:`[percent different from ref period] = ([Adj close] - [Average daily close value between ref date]) / [Average daily close value between ref date]`
最后就是用 `[percent different from ref period]` 这个字段绘制上面的图形了。
## 12. 相对周期过滤
如果我们想对比两个周期数据差异,可能会遇到数据不全导致的错误。比如今年 3 月份数据只产出到 6 号,但却和去年 3 月整月的数据进行对比,显然是不合理的。我们可以利用 LOD 表达式解决这个问题:
![](https://z3.ax1x.com/2021/11/13/IsLJVH.png)
相对周期过滤的重点是,不能直接用日期进行对比,因为今年数据总是比去年大。比如因为今年最新数据到 11.11 号,那么去年 11.11 号之后的数据都要被过滤掉。
首先找到最新数据是哪一天,利用不包含条件的 FIX 表达式即可:`[max date] = { max([date]) }`
然后利用 datepart 函数计算当前日期是今年的第几天:
`[day of year of max date] = datepart('dayofyear', [max date])``[day of year of order date] = datepart('dayofyear', [order date])`
所以 `[day of year of max date]` 就是一个卡点,任何超过今年这么多天的数据都要过滤掉。因此我们创建一个过滤条件:`[period filter] = [day of year of order date] <= [day of year of max date]`
`[period filter]` 字段作为筛选条件即可。
## 13. 用户登陆频率
如何绘制一个用户每个月登陆频率?
![](https://z3.ax1x.com/2021/11/13/IyCfAK.png)
要计算这个指标,得用用户总活跃时间除以总登陆次数。
首先计算总活跃时间:利用 FIX 表达式计算用户最早、最晚的登陆时间:
- `[first login] = { fixed [user id] : min([log in date]) }`
- `[last login] = { fixed [user id] : max([log in date]) }`
计算其中月份 diff,就是用户活跃月数:
`[total months user is active] = datediff("month", [first login], [last login])`
总登录次数比较简单,也是固定用户 ID 后,对登陆日期计数即可:
`[numbers of logins per user] = { fixed [user id] : count([login date]) }`
最后,我们用两者相除,得到用户登陆频率:
`[login frequency] = [total months user is active] / [numbers of logins per user]`
制作图表就很简单了,把 `[login frequency]` 移到横轴,count distinct 用户 ID 作为纵轴即可。
## 14. 比例笔刷
这个是 LOD 最常见的场景,比如求各品类销量占此品类总销量的贡献占比?
![](https://z3.ax1x.com/2021/11/13/IyufEQ.png)
`sum(sales) / sum({ fixed [category] : sum(sales) })` 即可。
当前详细级别是 category + country,我们固定品类,就可以得到各品类在所有国家的累积销量。
## 15. 按客户群划分的年度购买频率
如何证明老客户忠诚度更高?
我们可以如下图,按照客户群(2011 年、2012 年客户)作为图例,观察他们每年购买频次分布。
![](https://z3.ax1x.com/2021/11/13/IyuICn.png)
如上图所示,我们发现顾客注册时间越早,各购买频次的比例都更高,所以证明了老顾客忠诚度更高这一结论。注意这里看的是至少购买 N 次,所以每条线相比才具有说服力。如果是购买 N 次,则可能老顾客购买 1 次较少,购买 10 次较多,难以直接对比。
首先我们生成图例字段,即按最早照购买年份划分顾客群:`[Cohort] = { fixed [customer id] : min(Year([order date])) }`
然后就和我们第一个例子类似,计算每个订单数量下,有多少顾客。唯一的区别是,我们不仅按照顾客 ID group,还要进一步对最早购买日期做拆分,即:`{ fixed [customer id], [Cohort] : count([order id]) }`
上面的字段作为 X 轴,Y 轴和第一个例子类似:`count(customer id)`,但我们想查看的是至少购买 N 次,也就是这个购买次数是累计值,即至少购买 9 次 = 购买 9 次 + 购买 10 次 + ... 购买 MAX 次。所以是一种 DESC 的 `windowsum`,整体表达式应该类似 `[Running Total] = WINDOW_SUM(count(customer id)), 0, LAST())`
最后,因为实际 Y 轴计算的是占比,所以用刚才计算的至少购买 N 次指标除以各 Cohort 下总购买次数,即 `[Running Total] / sum({ fixed [Cohort] : count([customer id]) })`
## 总结
上面的几个例子,都是基于 fixed、include、exclude 这几个基本 LOD 用法的叠加。但从实际例子来看,我们会发现真正的难点不在与 LOD 表达式的语法,而在于我们如何精确理解需求,拆解成合理的计算步骤,并在需要运行 LOD 的计算步骤正确的使用。
LOD 表达式看上去很神奇,似乎可以和数据 “神奇” 的贴合在一起,我们要理解到 LOD 背后就是表之间的 join,而不同明细级别就表示不同的 group by 规则这一背后原理,就能比较好的理解为什么 LOD 表达式能这么运作了。
> 讨论地址是:[精读《15 大 LOD 表达式 - 下》· Issue #370 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/370)
**如果你想参与讨论,请 [点击这里](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)
@@ -78,7 +78,7 @@ ES 模块需要借助模块加载器来实现这三步。加载器在不同的
这就意味着我们必须一层一层的遍历文件树,转化文件并找出依赖,最后查找并且加载这些依赖。如果主线程正在等待去下载这些文件,那么很多的任务会堆积在队列中。这是因为浏览器环境下下载用了很长时间。
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种分构建的方式是 ES 模块和 CJS 模块最本质的不同。
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种分构建的方式是 ES 模块和 CJS 模块最本质的不同。
![](https://hacks.mozilla.org/files/2018/03/10_construction-500x302.png)
@@ -289,7 +289,7 @@ declare function createStore(reducer: Reducer, enhancer: Enhancer);
可以清晰的看到,`createStore` 想表现的是对参数个数的重载,如果定义了函数类型重载,TS 会根据函数类型自动判断对应的是哪个定义。
而在 TS `2.3` 版本支持了泛型默认参数,**可以某些场景减少函数类型重载的代码量**,比如对于下面的代码:
而在 TS `2.3` 版本支持了泛型默认参数,**可以减少某些场景函数类型重载的代码量**,比如对于下面的代码:
```typescript
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
@@ -300,7 +300,7 @@ declare function create<T extends HTMLElement, U extends HTMLElement>(
): Container<T, U[]>;
```
通过枚举表达了型默认值,以及 U 与 T 之间可能存在的关系,这些都可以用泛型默认参数解决:
通过枚举表达了型默认值,以及 U 与 T 之间可能存在的关系,这些都可以用泛型默认参数解决:
```typescript
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
@@ -41,7 +41,7 @@
**也就是学习技术细节是没有技术门槛,随着年龄的增加,如果只累积了大家都能学会的内容,那么当旧知识被淘汰后,学习新知识的速度又不如年轻人快,会逐渐失去经验优势。**
那么如何利用无门槛的特征,将其变为门槛呢?那就是任何年龄段学习技术细节都很容易,在你需要深入细节的时候再深入进去,不需要深入的时候把时间花在了解宏观架构上。
那么如何利用无门槛的特征,将其变为门槛呢?任何年龄段学习技术细节都很容易,应该在你需要深入细节的时候再深入进去,不需要深入的时候把时间花在了解宏观架构上。
就是培养高效的学习能力,能准确判断某个技术细节是否有必要掌握,如需要该如何快速掌握核心内容,并在掌握之后不留恋,可以快速抽身出来继续全局性思考。这种思维是有门槛的,技术专家都可以做到这一点。
@@ -53,7 +53,7 @@
这要看怎么理解业务与技术的关系,比如建设 “数据联邦”,光是了解各个不同的存储系统技术细节可能就要花很久,而实际上是没必要将所有技术细节都弄懂的,只要定好一个通用交互规范,各存储系统各自封装一套符合这个规范的交互接口即可。
做成事往往需要宏观的技术思维,需要将许多技术点链接在一起。举个例子,做成事就类似于军官指挥作战,做成的目的是通过制定打法赢得战争,而不是自己冲锋陷阵并测量敌人壕沟的宽度。关心技术细节只最终落实到每个人具体实施项中的一部分,技术细节的目标累加起来才做成事。
做成事往往需要宏观的技术思维,需要将许多技术点链接在一起。举个例子,做成事就类似于军官指挥作战,做成的目的是通过制定打法赢得战争,而不是自己冲锋陷阵并测量敌人壕沟的宽度。关心技术细节只最终落实到每个人具体实施项中的一部分,技术细节的目标累加起来才做成事。
## 2.2 搞清楚业务对技术的真实诉求
@@ -63,7 +63,7 @@
拥有技术思维的人,容易沉迷于解决不切实际的问题,或者是别人解决过的问题。这种思维对技术学习是非常有帮助的,但如果长期不能转变这种思维,对公司来说是无法创造什么价值的。
拥有业务思维的人,首先要懂业务,只有懂业务,跟着对的业务,才能对未来信心,知道自己的付出可以换来回报。
拥有业务思维的人,首先要懂业务,只有懂业务,跟着对的业务,才能对未来信心,知道自己的付出可以换来回报。
懂业务后,才知道如何通过技术帮助业务获得成功。
@@ -83,7 +83,7 @@
现在技术点越来越多,如果什么技术细节都要详细了解,最终一定不能有很好的全局视野。比较好的状态是找几个重点深入了解,其他的技术点在掌握了全局技术视野后再考虑深入。
在互联网初期,很多技术框架还不完善,技术借力的意义不大,毕竟也没有多少东西可用。
在互联网初期,很多技术框架还不完善,技术借力的意义不大,毕竟也没有多少东西可用。
但是现在无论前端还是后端的技术、轮子已经眼花缭乱了,能掌握这些已有技术的人,价值已经逐渐大于会完整了解某些技术细节的人。一个优秀的专家应该能快速定位要解决的业务问题是否有成熟的技术方案,如何以最小的投入产出比实现,同时保持良好的维护性应变业务维护。
+2 -2
View File
@@ -49,9 +49,9 @@ function visitTree(node: TreeNode) {
最后只要递归一下就能解题了,我们将输入不断拆解为左右子树的的输入,直到达到终止条件。
解决此题的关键是,不仅要直到如何写前中后序遍历,还要知道前序遍历第一个节点是根节点,后序遍历最后一个节点是根节点,中序遍历以根节点为中心,左右分别是其左右子树,这几个重要延伸特征。
解决此题的关键是,不仅要知道如何写前中后序遍历,还要知道前序遍历第一个节点是根节点,后序遍历最后一个节点是根节点,中序遍历以根节点为中心,左右分别是其左右子树,这几个重要延伸特征。
说完了反向,我们说正向,即递归一二叉树。
说完了反向,我们说正向,即递归一二叉树。
其实二叉树除了递归,还有一种常见的遍历方法是利用栈进行广度优先遍历,典型题目有从上到下打印二叉树。
@@ -0,0 +1,150 @@
二叉搜索树的特性是,任何一个节点的值:
- 都大于左子树任意节点。
- 都小于右子树任意节点。
因为二叉搜索树的特性,我们可以更高效的应用算法。
## 精读
还记得 [《算法 - 二叉树》](https://github.com/ascoders/weekly/blob/master/%E7%AE%97%E6%B3%95/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) 提到的 [二叉树的最近公公祖先](https://github.com/ascoders/weekly/blob/master/%E7%AE%97%E6%B3%95/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) 问题吗?如果这是一颗二叉搜索树,是不是存在更巧妙的解法?你可以暂停先思考一下。
### 二叉搜索树的最近公共祖先
二叉搜索树的最近公共祖先是一道简单题,题目如下:
> 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
>
> 百度百科中最近公共祖先的定义为:“对于有根树 `T` 的两个结点 `p`、`q`,最近公共祖先表示为一个结点 `x`,满足 `x` 是 `p`、`q` 的祖先且 `x` 的深度尽可能大(一个节点也可以是它自己的祖先)。”
第一个判断条件是相同的,即当前节点值等于 `p``q` 任意一个,则当前节点就是其最近公共祖先。
如果不是呢?同时考虑二叉搜索树与公共祖先的特性可以发现:
1. 如果 `p` `q` 两个节点分别位于当前节点的左 or 右边,则当前节点符合要求。
2. 如果 `p` `q` 值一个大于,一个小于当前节点,说明 `p` `q` 分布在当前节点左右两侧。
基于以上考虑,可以仅通过值大小来判断,因此题目就被简化了。
接下来看一道入门题,即如何验证一颗二叉树是二叉搜索树。
### 验证二叉搜索树
验证二叉搜索树是一道中等题,题目如下:
> 给定一个二叉树,判断其是否是一个有效的二叉搜索树。
>
> 假设一个二叉搜索树具有如下特征:
>
> - 节点的左子树只包含小于当前节点的数。
> - 节点的右子树只包含大于当前节点的数。
> - 所有左子树和右子树自身必须也是二叉搜索树。
这道题看上去就应该用非常优雅的递归来实现。
二叉搜索树最重要的就是对节点值的限制,我们如果能正确卡住每个节点的值,就可以判断了。
如何判断节点值是否正确呢?我们可以用递归的方式倒推,即从根节点开始,假设根节点值为 `x`,那么左树节点的值就必须小于 `x`,再往左,那么值就要小于(假设第一个左节点值为 `x1` `x1`,右树也是一样判断,因此就可以写出答案:
```typescript
function isValidBST(node: TreeNode, min = -Infinity, max = Infinity) {
if (node === null) return true
// 判断值范围是否合理
if (node.val < min || node.val > max) return false
// 继续递归,并且根据二叉搜索树特定,进一步缩小最大、最小值的锁定范围
return
// 左子树值 max 为当前节点值
isValidBST(node.left, min, node.val) &&
// 右子树值 min 为当前节点值
isValidBST(node.right, node.val, max) &&
}
```
接下来看一些简单的二叉搜索树操作问题,比如删除二叉搜索树中的节点。
### 删除二叉搜索树中的节点
删除二叉搜索树中的节点是一道中等题,题目如下:
> 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
>
> 一般来说,删除节点可分为两个步骤:
>
> 1. 首先找到需要删除的节点;
> 2. 如果找到了,删除它。
>
> 说明: 要求算法时间复杂度为 `O(h)`,`h` 为树的高度。
要删除二叉搜索树的节点,找到节点本身并不难,因为如果值小了,就从左子树找;如果值大了,就从右子树找,这本身查找起来是非常简单的。难点在于,如何保证删除元素后,这棵树还是一颗二叉搜索树?
假设我们删除的是叶子结点,很显然,二叉搜索树任意子树都是二叉搜索树,我们又没有破坏其他节点的关系,因此直接删除就行了,最简单。
如果删除的不是叶子结点,那么谁来 “上位” 代替这个节点呢?题目要求复杂度为 `O(h)` 显然不能重新构造,我们需要仔细考虑。
假设删除的节点存在右节点,那么肯定从右节点找到一个代替值移上来,找谁呢?找右节点的最小值呀,最小值很好找的,找完代替后,相当于 **问题转移为删除这个最小值节点,递归就完事了。**
假设删除的节点存在左节点,但是没有右节点,那就从左节点找一个最大的替换掉,同理递归删除找到的节点。
可以看到,删除二叉搜索树,为了让二叉搜索树性质保持不变,需要不断进行重复子问题的递归删除节点。
当你掌握二叉搜索树特性后,可以尝试构造二叉搜索树了,下面就是一道让你任意构造二叉搜索树的题目:不同的二叉搜索树。
### 不同的二叉搜索树
不同的二叉搜索树是一道中等题,题目如下:
> 给你一个整数 `n` ,求恰由 `n` 个节点组成且节点值从 `1` 到 `n` 互不相同的 **二叉搜索树** 有多少种?返回满足题意的二叉搜索树的种数。
这道题重点在于动态规划思维 + 笛卡尔积组合的思维。
需要将所有可能性想象为确定了根节点后,左右子树到底有几种组合方式?
举个例子,假设 `n=10`,那么这 10 个节点,假设我取第 3 个节点为根节点,那么左子树有 2 个节点,右子树有 7 个节点,这种组合情况就有 `DP(2) * DP(7)` 这么多,假设 `DP(n)` 表示 n 个节点能组成任意二叉搜索树的数量。
这仅是第 3 个节点为根节点的情况,实际上每个节点作为根节点都是不同的树(轴对称也算不同的),那么我们就要从第 1 个节点计算到第 `n` 个节点。
因此答案就出来了,我们先考虑特殊情况 `DP(0)=1` `DP(1)=1`,所以:
```typescript
function numTrees(n: number) {
const dp: number[] = [1, 1]
for (let i = 2; i <= n; i++) {
for (let j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j]
}
}
return dp[n]
}
```
最后再看一道找值题,并不是找最大值,而是找第 k 大值。
### 二叉搜索树的第 K 大节点
二叉搜索树的第 K 大节点是一道简单题,题目如下:
> 给定一棵二叉搜索树,请找出其中第 `k` 大的节点。
这道题之所以简单,是因为二叉搜索树的中序遍历是从小到大的,因此只要倒序中序遍历,就可以找到第 `k` 大的节点。
倒序中序遍历,即右、根、左。
这道题就解决啦。
## 总结
二叉搜索树的特性很简单,就是根节点值夹在左右子树中间,利用这个特性几乎可以解决一切相关问题。
但通过上面几个例子可以发现,仅熟悉二叉搜索树特性还是不够的,一些题目需要结合二叉树中序遍历、公共祖先特征等通用算法思路结合来解决,因此学会融会贯通很重要。
> 讨论地址是:[精读《算法 - 二叉搜索树》· Issue #337 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/337)
**如果你想参与讨论,请 [点击这里](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)
@@ -103,7 +103,7 @@ select c.$my_custom_symbol$ from ...
```sql
select a |from b;
# select a $my_custom_symbol$ b;
# select a $my_custom_symbol$ from b;
```
你会发现,“补全光标文字” 法,在关键字位置时,会把原本正确的语句变成错误的语句,根本解析不出语法树。
@@ -32,7 +32,7 @@ Factory Method(工厂方法)属于创建型模式,利用工厂方法创建
对卡牌对战的系统来说,**所有卡牌都应该实现同一种接口**,所以卡牌对战系统拿到的卡牌应该就是简单的 Card 类型,这种类型具备基本的卡片操作交互能力,系统就调用这些能力完成基本流程就好了,如果系统直接实例化具体的卡片,那不同的卡片类型会导致系统难以维护,卡片间操作也无法抽象化。
这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。
这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。
对图形拖拽系统来说,用到了 “连接平行的类层次” 这个特性,所谓连接平行的类层次,就是指一个图形,与其对应的操作类是一个平行抽象类,而一个具体的图形与具体的操作类则是另一个平行关系,系统只要关注最抽象的 “通用图形类” 与 “通用操作类” 即可,操作时,底层可能是某个具体的 “圆类” 与 “圆操作类” 结合使用,具体的类有不同的实现,但都符合同一种接口,因此操作系统才可以把它们一视同仁,统一操作。
@@ -25,7 +25,7 @@ Prototype(原型模式)属于创建型模式,既不是工厂也不是直
### 模版组件
通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意为止,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?
通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意位置,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?
## 意图解释