Compare commits

...
71 Commits
Author SHA1 Message Date
ascoders 94381de60b update readme 2021-09-27 08:51:06 +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
ascoders 022b1fdbbd 202 2021-07-12 13:41:29 +08:00
黄子毅 ff121c7e1b Merge pull request #335 from netcore-jroger/patch-1
Update 201.精读《算法 - 二叉树》.md
2021-07-07 18:06:51 +08:00
JRoger a04380c1c1 Update 201.精读《算法 - 二叉树》.md 2021-07-06 08:54:59 +08:00
Linear-Enter 3646b36db5 fix:typo 2021-07-05 10:59:33 +08:00
ascoders aaaa28953b update readme 2021-07-05 09:06:10 +08:00
ascoders 9609101870 update readme 2021-07-05 09:05:17 +08:00
黄子毅 afb601a303 201 2021-07-05 09:02:37 +08:00
ascoders c72477ed7d update readme 2021-06-28 10:46:52 +08:00
ascoders dc09550e0c Merge branch 'master' of https://github.com/ascoders/weekly 2021-06-28 10:46:20 +08:00
ascoders dc66e76dd1 200 2021-06-28 10:46:14 +08:00
黄子毅 cecef87d13 Merge pull request #329 from Turkyden/patch-1
Update 140.精读《结合 React 使用原生 Drag Drop API》.md
2021-06-18 11:11:39 +08:00
Dengju Deng d0ca3a348f Update 140.精读《结合 React 使用原生 Drag Drop API》.md 2021-06-17 18:01:49 +08:00
ascoders de4e309f39 199 2021-06-15 10:09:22 +08:00
ascoders ea56ac10ce Merge branch 'master' of https://github.com/ascoders/weekly 2021-06-07 09:33:53 +08:00
ascoders 90660c6d34 198 2021-06-07 09:33:47 +08:00
黄子毅 fcf04083a5 Merge pull request #322 from rmlzy/patch-3
Update 41.精读《Ant Design 3.0 背后的故事》.md
2021-05-31 16:05:52 +08:00
黄子毅 a1ea12a736 Merge pull request #323 from rmlzy/patch-4
Update 52.精读《图解 ES 模块》.md
2021-05-31 16:05:25 +08:00
黄子毅 02a795bb8a Merge pull request #324 from rmlzy/patch-5
Update 137.精读《当我在分享的时候,我在做什么?》.md
2021-05-31 16:05:12 +08:00
黄子毅 6bffed8eb5 Merge pull request #325 from rmlzy/patch-6
Update 136.精读《极客公园 IFX - 下》.md
2021-05-31 16:04:47 +08:00
黄子毅 ddd5ceb74a Merge pull request #326 from rmlzy/patch-7
Update 135.精读《极客公园 IFX - 上》.md
2021-05-31 16:04:34 +08:00
rmlzy 3c6cee2f49 Update 135.精读《极客公园 IFX - 上》.md
修正错别字
2021-05-31 16:00:59 +08:00
rmlzy fc2f0c6f68 Update 136.精读《极客公园 IFX - 下》.md
修正错别字
2021-05-31 15:50:42 +08:00
rmlzy 0dcf208315 Update 137.精读《当我在分享的时候,我在做什么?》.md
修正错别字
2021-05-31 15:47:03 +08:00
rmlzy 7730834e83 Update 52.精读《图解 ES 模块》.md
修正错别字
2021-05-31 15:40:39 +08:00
rmlzy 91875ab232 Update 41.精读《Ant Design 3.0 背后的故事》.md
修正格式错误
2021-05-31 15:32:52 +08:00
黄子毅 d81e6d7813 Merge pull request #320 from rmlzy/patch-1
Update 20.精读《Nestjs》文档.md
2021-05-31 10:02:25 +08:00
黄子毅 3e299d85e9 Merge pull request #321 from rmlzy/patch-2
Update 119.精读《前端深水区》.md
2021-05-31 10:02:04 +08:00
ascoders 455e1693f4 197 2021-05-31 10:01:23 +08:00
rmlzy 2a17e62107 Update 119.精读《前端深水区》.md
修正标点符号
2021-05-31 09:54:38 +08:00
rmlzy 0a090cf0a7 Update 20.精读《Nestjs》文档.md
Nextjs 目前是 Providers 配合 `@Injectable()` 装饰器实现 DI
2021-05-31 09:50:16 +08:00
ascoders c13f0af435 Merge branch 'master' of https://github.com/ascoders/weekly 2021-05-24 13:50:42 +08:00
ascoders 8ee7995ddc fix typo error 2021-05-24 13:50:31 +08:00
黄子毅 0e8c749e68 Merge pull request #318 from neighborhood999/fix/typo
fix(196): typo
2021-05-24 10:49:54 +08:00
Jie Peng ee0450ffb2 fix(196): typo
Signed-off-by: Jie Peng <im@jiepeng.me>
2021-05-24 10:32:12 +08:00
ascoders e0d8be6916 196 2021-05-24 10:12:55 +08:00
ascoders 7c4d37a428 fix typo 2021-05-17 13:21:06 +08:00
ascoders eeaebad903 195 2021-05-17 09:43:00 +08:00
黄子毅 c624e7e607 Merge pull request #315 from kamilic/patch-1
Update 194.精读《算法基础数据结构》.md
2021-05-12 13:53:55 +08:00
kamilic 0f724a20cf Update 194.精读《算法基础数据结构》.md
chores: fix typo
2021-05-11 21:08:13 +08:00
黄子毅 c7922c4a0e Merge pull request #314 from jihchi/patch-1
Update 194.精读《算法基础数据结构》.md -- 連結正確但文字錯誤
2021-05-10 14:23:45 +08:00
Jihchi Lee 9d98f72fd3 Update 194.精读《算法基础数据结构》.md 2021-05-10 11:25:53 +08:00
ascoders db9a256efa update readme 2021-05-10 08:59:01 +08:00
ascoders aedfdd4b91 Merge branch 'master' of https://github.com/dt-fe/weekly 2021-05-10 08:57:02 +08:00
ascoders 99d5c51d42 194 2021-05-10 08:56:52 +08:00
黄子毅 086b733288 Merge pull request #313 from jihchi/patch-1
96.精读《useEffect%20完全指南》-- 修正錯誤依賴
2021-05-08 10:46:03 +08:00
Jihchi Lee 6f9ccaef9e Update 96.精读《useEffect 完全指南》.md 2021-05-07 12:01:13 +08:00
ascoders dba92f52b5 update 2021-04-25 10:10:15 +08:00
ascoders 61512fba55 193 2021-04-25 10:06:54 +08:00
ascoders 542faafe04 fix: 修复算法错误 2021-04-22 17:18:24 +08:00
36 changed files with 6510 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 ./"
}
}
}
}
+215 -193
View File
@@ -6,7 +6,7 @@
前端界的好文精读,每周更新!
最新精读:<a href="./前沿技术/192.精读《DOM diff 最长上升子序列》.md">192.精读《DOM diff 最长上升子序列》</a>
最新精读:<a href="./前沿技术/211.%E7%B2%BE%E8%AF%BB%E3%80%8AMicrosoft%20Power%20Fx%E3%80%8B.md">211.精读《Microsoft Power Fx》</a>
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
@@ -19,210 +19,232 @@
### 前沿技术
- <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="./前沿技术/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="./设计模式/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 源码》 copy.md">130.精读《unstated 与 unstated-next 源码》 copy</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.%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>
## 关注前端精读微信公众号
@@ -10,7 +10,7 @@
## 概述
原文对于深水区的想法,讲的很清楚,还是建议读者去读一下原文。
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
- 深水区需要哪些技能
![image.png](https://img.alicdn.com/tfs/TB1oovQe8r0gK0jSZFnXXbRRXXa-1832-1032.png)
@@ -180,7 +180,7 @@ const dragProps = {
```jsx
const dropProps = {
onDragOver: ev => {
// 做一些样式处理,提示用户此时松手会将元素防止在何处
// 做一些样式处理,提示用户此时松手会将元素放置在何处
},
onDrop: ev => {
ev.stopPropagation()
@@ -126,23 +126,19 @@
最后我们看看,如何在找到答案的同时,还能找到正确的序列呢?
其实读到这里,不用说你应该也能猜出来,前面已经说过了,**只要替换了最后一个或者插入的时候,栈顺序就是正确的**。所以我们可以在替换最后一个或者插入的时候,存储下当前栈的拷贝,这样最后留下来的拷贝就是最终正确的顺序。
### 找出正确的序列
那为什么是这样呢?我们最后用一个例子强化一下理解,因为已经很熟练了,因此前几步合并了一下
找出正确的序列并不容易,让我们看下面这个情况
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01Mi7fPY1FLlDhiuGSC_!!6000000000471-2-tps-1200-344.png">
<img width=450 src="https://img.alicdn.com/imgextra/i2/O1CN01DXI8Cm1Uh1qjYGtEM_!!6000000002548-2-tps-1208-486.png">
到目前为止,`7, 8, 9, 13` 是不存在的,但实际上它指代的是 `10, 11, 12, 13`,这个前面已经解释过,就不再赘述。我们此时已经存了队列 `10, 11, 12, 13`,因此此时结束的话,这个队列输出是正确的。我们看下一步
贪心算法结束后,总长度是对的,但很明显顺序还是错的。为了方便计算,我们存储时转化为下标
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01ZtAMAR1V30rKrchB2_!!6000000002596-2-tps-1204-444.png">
<img width=450 src="https://img.alicdn.com/imgextra/i3/O1CN01tflKTQ1soP0NpnIiu_!!6000000005813-2-tps-1216-716.png">
为了方便识别,我给不同分组数字加了背景色,这样更容易观察:我们发现,由于每次替换的都是比它稍大的数字,一旦遇到了一个更小的开始 `1, 2, 3, 4, 5`,即便上一轮 `7, 8, 9` 还没有完全替换完 `10, 11, 12, 13`,更小的也一定从最左边开始替换,因为栈内数字是单调递增的。那么全部替换完,或者从某个数字开始,向右替换完,此时队列中的数字一定都是相对顺序正确的。从这里例子来看,`2, 3` 一定会优先替换掉 `8, 9`,等 `13` 被替换的时候,栈的相对顺序一定符合原数组的相对顺序。
并且使用二维数组存储,这样被替换的数字可以被保留下来。当计算完毕后,我们从最后一位开始向前查找,**一旦发现一个值不是单调递减的,就向数组上方继续查找,直到首节点。**
最后看一个更复杂的例子加深印象:
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01GXWX6G1jaoiMJWC9h_!!6000000004565-2-tps-1102-768.png">
读到这里,恭喜你已经大功告成,完全理解这个 DOM diff 算法啦。
因此上面的例子,最终顺序下标是 `[0, 1, 2, 3, 4, 5, 9]`,对应数字为 `[10, 20, 30, 40, 50, 60, 61]`,**而且这个数字是潜力最大的最长子序列。**
## 总结
@@ -0,0 +1,324 @@
截止目前,React Server Component 还在开发与研究中,因此不适合投入生产环境使用。但其概念非常有趣,值得技术人学习。
目前除了国内各种博客、知乎解读外,最一手的学习资料有下面两处:
1. [Dan 的 Server Component 介绍视频](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
2. [Server Component RFC 草案](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md)
我会结合这些一手资料,与一些业界大牛的解读,系统的讲清楚 React Server Component 的概念,以及我对它的一些理解。
首先我们来看,为什么需要提出 Server Component 这个概念:
Server Component 概念的提出,是为了解决 "用户体验、可维护性、性能" 这个不可能三角,所谓不可能三角就是,最多同时满足两条,而无法三条都同时满足。
简单解释一下,用户体验体现在页面更快的响应、可维护性体现在代码应该高内聚低耦合、性能体现在请求速度。
- 保障 **用户体验、可维护性**,用一个请求拉取全部数据,所有组件一次性渲染。但当模块不断增多,无用模块信息不敢随意删除,请求会越来越大,越来越冗余,导致瓶颈卡在取数这块,也就是 **性能不好**
- 保障 **用户体验、性能**,考虑并行取数,之后流程不变,那么以后业务逻辑新增或减少一个模块,我们就要同时修改并行取数公共逻辑与对应业务模块,**可维护性不好**。
- 保障 **可维护性、性能**,可以每个模块独立取数,但在父级渲染完才渲染子元素的情况下,父子取数就变成了串行,页面加载被阻塞,**用户体验不好**。
一言蔽之,在前后端解耦的模式下,唯一连接的桥梁就是取数请求。要把用户体验做好,取数就要提前并行发起,而前端模块是独立维护的,所以在前端做取数聚合这件事,必然会破坏前端可维护性,而这并行这件事放在后端的话,会因为后端不能解析前端模块,导致给出的聚合信息滞后,久而久之变得冗余。
要解决这个问题,就必须加深前端与后端的联系,所以像 GraphQL 这种前后端约定方案是可行的,但因为其部署成本高,收益又仅在前端,所以难以在后端推广。
Server Component 是另一种方案,通过启动一个 Node 服务辅助前端,但做的不是 API 对接,而是运行前端同构 js 代码,直接解析前端渲染模块,从中自动提取请求并在 Node 端直接与服务器通信,因为服务端间通信成本极低、前端代码又不需要做调整,请求数据也是动态按需聚合的,因此同时解决了 "用户体验、可维护性、性能" 这三个问题。
其核心改进点如下图所示:
<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN01NttXOI21kaFJgNDx1_!!6000000007023-2-tps-720-466.png">
如上图所示,这是前后端正常交互模式,可以看到,`Root``Child` 串行发了两个请求,因为网络耗时与串行都是严重阻塞部分,因此用红线标记。
Server Component 可以理解为下图,不仅减少了一次网络损耗,请求也变成了并行,请求返回结果也从纯数据变成了一个同时描述 UI DSL 与数据的特殊结构:
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN01MDYxZ71K0IkACLmFJ_!!6000000001101-2-tps-1142-468.png">
到此,恭喜你已经理解了 Server Component 核心概念,如果你只想泛泛了解一下,读到这里就可以结束了。如果你还想深入了解其实现细节,请继续阅读。
## 概述
概括的说,Server Component 就是让组件拥有在服务端渲染的能力,从而解决不可能三角问题。也正因为这个特性,使得 Server Component 拥有几种让人眼前一亮的特性,都是纯客户端组件所不具备的:
- **运行在服务端的组件只会返回 DSL 信息,而不包含其他任何依赖**,因此 Server Component 的所有依赖 npm 包都不会被打包到客户端。
- **可以访问服务端任何 API**,也就是让组件拥有了 Nodejs 能拥有的能力,你理论上可以在前端组件里干任何服务端才能干的事情。
- **Server Component 与 Client Component 无缝集成**,可以通过 Server Component 无缝调用 Client Component。
- **Server Component 会按需返回信息**,在当前逻辑下,走不到的分支逻辑的所有引用都不会被客户端引入。比如 Server Component 虽然引用了一个巨大的 npm 包,但某个分支下没有用到这个包提供的函数,那客户端也不会下载这个巨大的 npm 包到本地。
- **由于返回的不是 HTML,而是一个 DSL,所以服务端组件即便重新拉取,已经产生的 State 也会被维持住**。比如说 A 是 ServerComponent,其子元素 B 是 Client Component,此时对 B 组件做了状态修改比如输入一些文字,此时触发 A 重新拉取 DSL 后,B 已经输入的文字还会保留。
- **可以无缝与 Suspense 结合**,并不会因为网络原因导致连 Suspense 的 loading 都不能及时展示。
- **共享组件可以同时在服务端与客户端运行**
### 三种组件
Server Component 将组件分为三种:Server Component、Client Component、Shared Component,分别以 `.server.js``.client.js``.js` 后缀结尾。
其中 `.client.js` 与普通组件一样,但 `.server.js``.js` 都可能在服务端运行,其中:
- `.server.js` 必然在服务端执行。
- `.js` 在哪执行要看谁调用它,如果是 `.server.js` 调用则在服务端执行,如果是 `.client.js` 调用则在客户端执行,因此其本质还要接收服务端组件的约束。
下面是 RFC 中展示的 Server Component 例子:
```typescript
// Note.server.js - Server Component
import db from 'db.server';
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
// (B) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
```
可以看到,**这就是 Node 与 React 混合语法**。服务端组件有着苛刻的限制条件:**不能有状态,且 `props` 必须能被序列化**。
很容易理解,因为服务端组件要被传输到客户端,就必须经过序列化、反序列化的过程,JSX 是可以被序列化的,props 也必须遵循这个规则。另外服务端不能帮客户端存储状态,因此服务端组件不能用任何 `useState` 等状态相关 API。
但这两个问题都可以绕过去,即将状态转化为组件的 `props` 入参,由 `.client.js` 存储,见下图:
<img width=250 src="https://img.alicdn.com/imgextra/i4/O1CN01ChPZdO1ky0Nsu2ygV_!!6000000004751-2-tps-514-278.png">
或者利用 Server Component 与 Client Component 无缝集成的能力,将状态与无法序列化的 `props` 参数都放在 Client Component,由 Server Component 调用。
### 优点
#### 零客户端体积
这句话听起来有点夸张,但其实在 Server Component 限定条件下还真的是。看下面代码:
```typescript
// NoteWithMarkdown.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
```
`marked``sanitize-html` 都不会被下载到本地,所以如果只有这一个文件传输,客户端的理论增加体积就是 `render` 函数序列化后字符串大小,可能不到 1KB。
当然这背后也是限制换来的,首先这个组件没有状态,无法在客户端实时执行,而且在服务端运行也可能消耗额外计算资源,如果某些 npm 包计算复杂度较高的话。
这个好处可以理解为,`marked` 这个包仅在服务端读取到内存一次,以后只要后客户端想用,只需要在服务端执行 `marked` API 并把输出结果返回给客户端,而不需要客户端下载 `marked` 这个包了。
#### 拥有完整服务端能力
由于 Server Component 在服务端执行,因此可以执行 Nodejs 的任何代码。
```typescript
// Note.server.js - Server Component
import fs from 'react-fs';
function Note({id}) {
const note = JSON.parse(fs.readFile(`${id}.json`));
return <NoteWithMarkdown note={note} />;
}
```
我们可以把对请求的理解拔高一个层次,即 `request` 只是客户端发起的一个 Http 请求,其本质是访问一个资源,在服务端就是个 IO 行为。对于 IO,我们还可以通过 `file` 文件系统写入删除资源、`db` 通过 sql 语法直接访问数据库,或者 `request` 直接在服务器本地发出请求。
#### 运行时 Code Split
我们都知道 webpack 可以通过静态分析,将没有使用到的 import 移出打包,而 Server Component 可以在运行时动态分析,将当前分支逻辑下没有用到的 import 移出打包:
```typescript
// PhotoRenderer.js
import React from 'react';
// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (props.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
```
这是因为 Server Component 构建时会进行预打包,运行时就是一个动态的包分发器,完全可以通过当前运行状态比如 `props.xxx` 来区分当前运行到哪些分支逻辑,而没有运行到哪些分支逻辑,并且仅告诉客户端拉取当前运行到的分支逻辑的缺失包。
纯前端模式与之类似的写法是:
```typescript
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
```
只是这种写法不够原生,且实际场景往往只有前端框架把路由自动包一层 Lazy Load,而普通代码里很少出现这种写法。
#### 无客户端往返的数据端取数
一般考虑到取数网络消耗,我们往往会将其处理成异步,然后在数据返回前展示 Loading:
```typescript
// Note.js
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
// NOTE: loads *after* rendering, triggering waterfalls in children
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return (/* render note here... */);
}
}
```
这是因为单页模式下,我们可以快速从 CDN 拿到这个 DOM 结构,但如果再等待取数,整体渲染就变慢了。而 Server Component 因为本身就在服务端执行,因此可以将拿 DOM 结构与取数同时进行:
```typescript
// Note.server.js - Server Component
function Note(props) {
// NOTE: loads *during* render, w low-latency data access on the server
const note = db.notes.get(props.id);
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
```
当然这个前提是网络消耗敏感的情况,如果本身就是一个慢 SQL 查询,耗时几秒的情况下,这样做反而适得其反。
#### 减少 Component 层次
看下面的例子:
```js
// Note.server.js
// ...imports...
function Note({id}) {
const note = db.notes.get(id);
return <NoteWithMarkdown note={note} />;
}
// NoteWithMarkdown.server.js
// ...imports...
function NoteWithMarkdown({note}) {
const html = sanitizeHtml(marked(note.text));
return <div ... />;
}
// client sees:
<div>
<!-- markdown output here -->
</div>
```
虽然在组件层面抽象了 `Note``NoteWithMarkdown` 两个组件,但由于真正 DOM 内容实体只有一个简单的 `div`,所以在 Server Component 模式下,返回内容就会简化为这个 `div`,而无需包含那两个抽象的组件。
### 限制
Server Component 模式下有三种组件,分别是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,如下:
**Server Component**
- ❌ 不能用 `useState``useReducer` 等状态存储 API。
- ❌ 不能用 `useEffect` 等生命周期 API。
- ❌ 不能用 `window` 等仅浏览器支持的 API。
- ❌ 不能用包含了上面情况的自定义 Hooks。
- ✅ 可无缝访问服务端数据、API。
- ✅ 可渲染其他 Server/Client Component
**Client Component**
- ❌ 不能引用 Server Component。
- ✅ 但可以在 Server Component 中出现 Client Component 调用 Server Component 的情况,比如 `<ClientTabBar><ServerTabContent /></ClientTabBar>`
- ❌ 不能调用服务端 API 获取数据。
- ✅ 可以用一切 React 与浏览器完整能力。
**Shared Component**
- ❌ 不能用 `useState``useReducer` 等状态存储 API。
- ❌ 不能用 `useEffect` 等生命周期 API。
- ❌ 不能用 `window` 等仅浏览器支持的 API。
- ❌ 不能用包含了上面情况的自定义 Hooks。
- ❌ 不能引用 Server Component。
- ❌ 不能调用服务端 API 获取数据。
- ✅ 可以同时在服务器与客户端使用。
其实不难理解,因为 Shared Component 同时在服务器与客户端使用,因此兼具它们的劣势,带来的好处就是更强的复用性。
## 精读
要快速理解 Server Component,我觉得最好也是最快的方式,就是找到其与十年前 PHP + HTML 的区别。看下面代码:
```php
$link = mysqli_connect('localhost', 'root', 'root');
mysql_select_db('test', $link);
$result = mysql_query('select * from table');
while($row=mysql_fetch_assoc($result)){
echo "<span>".$row["id"]."</span>";
}
```
其实 PHP 早就是一套 "Server Component" 方案了,在服务端直接访问 DB、并返回给客户端 DOM 片段。
React Server Component 在折腾了这么久后,可以发现,最大的区别是将返回的 HTML 片段改为了 DSL 结构,这其实是浏览器端有一个强大的 React 框架在背后撑腰的结果。而这个带来的好处除了可以让我们在服务端能继续写 React 语法,而不用退化到 "PHP 语法" 以外,更重要的是组件状态得以维持。
另一个重要不同是,PHP 无法解析现在前端生态下任何 npm 包,所以无从解析模块化的前端代码,所以虽然直觉上感觉 PHP 效率与 Server Component 并无区别,但背后的成本是得写另一套不依赖任何 npm 包、JSX 的语法来返回 HTML 片段,Server Component 大部分特性都无法享受到,而且代码也无法复用。
所以,本质上还是 HTML 太简单了,无法适应如今前端的复杂度,而普通后端框架虽然后端能力强大,但在前端能力上还停留在 20 年前(直接返回 DOM),唯有 Node 中间层方案作为桥梁,才能较好的衔接现代后端代码与现代前端代码。
### PHP VS Server Component
其实在 PHP 时代,前后端都可以做模块化。后端模块化显而易见,因为可以将后端代码模块化的开发,最后打包至服务器运行。前端也可以在服务端模块化开发,只要我们将前后端代码剥离出来即可,下图青色是后端部分,红色是前端部分:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01jsKjLq1iWPHi9C4pQ_!!6000000004420-2-tps-894-642.png">
但这有个问题,因为后端服务对浏览器来说是无状态的,所以后端模块化本身就符合其功能特征,但前端页面显示在用户浏览器,每次都通过路由跳转到新页面,显然不能最大程度发挥客户端持续运行的优势,我们希望在保持前端模块化的基础上,在浏览器端有一个持续运行的框架优化用户体验,因此 Server Component 其实做了下面的事情:
<img width=550 src="https://img.alicdn.com/imgextra/i3/O1CN01gzaZNY1lBkGbGJKUy_!!6000000004781-2-tps-1332-760.png">
这样做有两大好处:
1. 兼顾了 PHP 模式下优势,即前后端代码无缝混合,带来一系列体验和能力增强。
2. 前后端还是各自模块化编写,图中红色部分是随前端项目整体打包的,因此开发还是保留了模块化特点,且在浏览器上还保持了 React 现代框架运行,无论是单页还是数据驱动等特性都能继续使用。
## 总结
Server Component 还没有成熟,但其理念还是很靠谱的。
想要同时实现 "用户体验、可维护性、性能",重后端,或者重前端的方案都不可行,只有在前后端取得一种平衡才能达到。Server Component 表达了一种职业发展理念,即未来前后端还是会走向全栈,这种全栈是前后端同时做深,从而让程序开发达到纯前端或纯后端无法达到的高度。
2021 年国内开发环境依然比较落后,所谓全栈,往往指的是 “前后端都懂一点”,各端都做不深,难以孵化出 Server Component 这种概念。当然,这也是我们继续向世界学习的动力。
也许 PHP 与 Server Component 的区别,就是检验一个人是真全栈还是伪全栈的试金石,快去问问你的同事吧!
> 讨论地址是:[精读《React Server Component》· Issue #311 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/311)
**如果你想参与讨论,请 [点击这里](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,150 @@
掌握了不同数据结构的特点,可以让你在面对不同问题时,采用合适的数据结构处理,达到事半功倍的效果。
所以这次我们详细介绍各类数据结构的特点,希望你可以融会贯通。
## 精读
### 数组
<img width=200 src="https://img.alicdn.com/imgextra/i2/O1CN01noho9m1Vltg5ISaq2_!!6000000002694-2-tps-418-110.png">
数组非常常用,它是一块连续的内存空间,因此可以根据下标直接访问,其查找效率为 O(1)。
但数组的插入、删除效率较低,只有 O(n),原因是为了保持数组的连续性,必须在插入或删除后对数组进行一些操作:比如插入第 K 个元素,需要将后面元素后移;而删除第 K 个元素,需要将后面元素前移。
### 链表
<img width=280 src="https://img.alicdn.com/imgextra/i3/O1CN010JfUOo1b0A5muE4sE_!!6000000003402-2-tps-584-112.png">
链表是为了解决数组问题而发明出来的,它提升了插入、删除效率,而牺牲了查找效率。
链表的插入、删除效率是 O(1),因为只要将对应位置元素断链、重连就可以完成插入、删除,而无需关心其他节点。
相应的查找效率就低了,因为存储空间不是连续的,所以无法像数组一样通过下标直接查找,而需要通过指针不断搜索,所以查找效率为 O(n)。
顺带一提,链表可以通过增加 `.prev` 属性改造为双向链表,也可以通过定义两个 `.next` 形成二叉树(`.left` `.right`)或者多叉树(N 个 `.next`)。
<img width=160 src="https://img.alicdn.com/imgextra/i2/O1CN01IqNVQI1m5ABrZXV4i_!!6000000004902-2-tps-318-316.png">
### 栈
<img width=240 src="https://img.alicdn.com/imgextra/i3/O1CN01xSS8e21xbe3LP1khU_!!6000000006462-2-tps-466-122.png">
栈是一种先入后出的结构,可以用数组模拟。
```typescript
const stack: number[] = []
// 入栈
stack.push(1)
// 出栈
stack.pop()
```
### 堆
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN019O42yy1qE6NlA8w6V_!!6000000005463-2-tps-1154-952.png">
堆是一种特殊的完全二叉树,分为大顶堆与小顶堆。
大顶堆指二叉树根节点是最大的数,小顶堆指二叉树根节点是最小的数。为了方便说明,以下以大顶堆举例,小顶堆的逻辑与之相反即可。
大顶堆中,任意节点都比其叶子结点大,所以根节点是最大的节点。这种数据结构的优势是可以以 O(1) 效率找到最大值(小顶堆找最小值),因为直接取 `stack[0]` 就是根节点。
这里稍微提一下二叉树与数组结构的映射,因为采用数组方式操作二叉数,无论操作还是空间都有优势:第一项存储的是节点总数,对于下标为 K 的节点,其父节点下标是 `floor(K / 2)`,其子节点下标分别是 `K * 2``K * 2 + 1`,所以可以快速定位父子位置。
而利用这个特性,可以将插入、删除的效率达到 `O(logn)`,因为可以通过上下移动的方式调整其他节点顺序,而对于一个拥有 n 个节点的完全二叉树,树的深度为 `logn`
### 哈希表
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01u3J1JF1Sl25HB6Q0I_!!6000000002286-2-tps-740-598.png">
哈希表就是所谓的 Map,不同 Map 实现方式不同,常见的有 HashMap、TreeMap、HashSet、TreeSet。
其中 Map 和 Set 实现类似,所以以 Map 为例讲解。
首先将要存储的字符求出其 ASCII 码值,再根据比如余数等方法,定位到一个数组的下标,同一个下标可能对应多个值,因此这个下标可能对应一个链表,根据链表进一步查找,这种方法称为拉链法。
如果存储的值超过一定数量,链表的查询效率就会降低,可能会升级为红黑树存储,总之这样的增、删、查效率为 `O(1)`,但缺点是其内容是无序的。
为了保证内容有序,可以使用树状结构存储,这种数据结构称为 HashTree,这样时间复杂度退化为 `O(logn)`,但好处是内容可以是有序的。
### 树 & 二叉搜索树
<img width=380 src="https://img.alicdn.com/imgextra/i4/O1CN01vOCoG91w82pzSITaQ_!!6000000006262-2-tps-800-504.png">
二叉搜索树是一种特殊二叉树,更复杂的还有红黑树,但这里就不深入了,只介绍二叉搜索树。
二叉搜索树满足对于任意节点,`left 的所有节点 < 根节点 < right 的所有节点`,注意这里是所有节点,因此在判断时需要递归考虑所有情况。
二叉搜索树的好处在于,访问、查找、插入、删除的时间复杂度均为 O(logn),因为无论何种操作都可以通过二分方式进行。但在最坏的情况会降级为 O(n),原因是多次操作后,二叉搜索树可能不再平衡,最后退化为一个链表,就变成了链表的时间复杂度。
更好的方案有 AVL 树、红黑树等,像 JAVA、C++ 标准库实现的二叉搜索树都是红黑树。
### 字典树
<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN01TqDeaL1ll0lTX75y3_!!6000000004858-2-tps-872-510.png">
字典树多用于单词搜索场景,只要给定一个单独开头,就可以快速查找到后面有几种推荐词。
比如上面的例子,输入 "o",就可以快速查找到后面有 "ok" 与 "ol" 两个单词。要注意的是,每个节点都要有一个属性 `isEndOfWord` 表示到当前为止是否为一个完整的单词:比如 `go``good` 两个都是完整的单词,但 `goo` 不是,因此第二个 `o` 与第四个 `d` 都有 `isEndOfWord` 标记,表示读到这里就查到一个完整的单词了,叶子结点的标记也可以省略。
### 并查集
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01B5xA5r21rSBj442z3_!!6000000007038-2-tps-622-172.png">
并查集用来解决团伙问题,或者岛屿问题,即判断多个元素之间是属于某个集合。并查集的英文是 Union and Find,即归并与查找,因此并查集数据结构可以写成一个类,提供两个最基础的方法 `union``find`
其中 `union` 可以将任意两个元素放在一个集合,而 `find` 可以查找任意元素属于哪个根集合。
并查集使用数组的数据结构,只是有以下特殊含义,设下标为 k:
- `nums[k]` 表示其所属的集合,如果 `nums[k] === k` 表示它是这个集合的根节点。
如果要数一共有几个集合,只要数有多少满足 `nums[k] === k` 条件的数目即可,就像数有几个团伙,只要数有几个老大即可。
并查集的实现不同,数据也会有微妙的不同,高效的并查集在插入时,会递归将元素的值尽量指向根老大,这样查找判断时计算的快一些,但即便指向的不是根老大,也可以通过递归的方式找到根老大。
### 布隆过滤器
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01CWabYX26RPkR0T3zs_!!6000000007658-2-tps-650-334.png">
Bloom Filter 只是一个过滤器,可以用远远超过其他算法的速度把未命中的数据排除掉,但未排除的也可能实际不存在,所以需要进一步查询。
布隆过滤器是如何做到这一点的呢?就是通过二进制判断。
如上图所示,我们先存储了 a、b 两个数据,将其转化为二进制,将对应位置改为 1,那么当我们再查询 a 或 b 时,因为映射关系相同,所以查到的结果肯定存在。
但查询 c 时,发现有一项是 0,说明 c 一定不存在;但查询 d 时,恰好两个都查到是 1,但实际 d 是不存在的,这就是其产生误差的原因。
布隆过滤器在比特币与分布式系统中使用广泛,比如比特币查询交易是否在某个节点上,就先利用布隆过滤器挡一下,以快速跳过不必要的搜索,而分布式系统计算比如 Map Reduce,也通过布隆过滤器快速过滤掉不在某个节点的计算。
## 总结
最后给出各数据结构 “访问、查询、插入、删除” 的平均、最差时间复杂度图:
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01LV4sSl20vkHWdZ7nr_!!6000000006912-2-tps-2398-1272.png">
这个图来自 [bigocheatsheet](https://www.bigocheatsheet.com/#graphs),你也可以点开链接直接访问。
学习了这些基础数据结构之后,希望你可以融会贯通,善于组合这些数据结构解决实际的问题,同时还要意识到没有任何一个数据结构是万能的,否则就不会有这么多数据结构需要学习了,只用一个万能的数据结构就行了。
对于数据结构的组合,我举两个例子:
第一个例子是如何以 O(1) 平均时间复杂度查询一个栈的最大或最小值。此时一个栈是不够的,需要另一个栈 B 辅助,遇到更大或更小值的时候才入栈 B,这样栈 B 的第一个数就是当前栈内最大或最小的值,查询效率是 O(1),而且只有在出栈时才需要更新,所以平均时间复杂度整体是 O(1)。
第二个例子是如何提升链表查找效率,可以通过哈希表与链表结合的思路,通过空间换时间的方式,用哈希表快速定位任意值在链表中的位置,就可以通过空间翻倍的牺牲换来插入、删除、查询时间复杂度均为 O(1)。虽然哈希表就能达到这个时间复杂度,但哈希表是无序的;虽然 HashTree 是有序的,但时间复杂度是 O(logn),所以只有通过组合 HashMap 与链表才能达到有序且时间复杂度更优,但牺牲了空间复杂度。
包括最后说的布隆过滤器也不是单独使用的,它只是一个防火墙,用极高的效率阻挡一些非法数据,但没有阻挡住的不一定就是合法的,需要进一步查询。
所以希望你能了解到各个数据结构的特征、局限以及组合的用法,相信你可以在实际场景中灵活使用不同的数据结构,以实现当前业务场景的最优解。
> 讨论地址是:[精读《算法基础数据结构》· Issue #312 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/312)
**如果你想参与讨论,请 [点击这里](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,121 @@
本周精读的文章是 [Comparing the New Generation of Build Tools](https://css-tricks.com/comparing-the-new-generation-of-build-tools/)。
前端工程领域近期出了不少新工具,这些新工具都运用了一些新技术或者跨领域技术,实现了一些突破,因此有必要了解一下这些工具都有什么特性,以及是否可以投入生产环境。
由于原文比较啰嗦,所以具体用法和支持细节不在这里展开,如果想进一步了解细节,可以直接阅读 [原文]((https://css-tricks.com/comparing-the-new-generation-of-build-tools/))。
## 精读
按照从底层到上层的封装粒度,以 esbuild、snowpack、vite、wmr 的顺序介绍。
### esbuild
esbuild 使用 go 语言编写,由于相对 node 更为底层,且不提供 AST 操作能力,所以代码执行效率更高,根据其官方 benchmark 介绍提速有 10100 倍:
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01hzHuDP1JXuBvRgX7x_!!6000000001039-2-tps-800-170.png">
esbuild 有两大功能,分别是 bundler 与 minifier,其中 bundler 用于代码编译,类似 babel-loader、ts-loaderminifier 用于代码压缩,类似 terser。
使用 esbuild 编译代码方法如下:
```typescript
esbuild.build({
entryPoints: ["src/app.jsx"],
outdir: "dist",
define: { "process.env.NODE_ENV": '"production"' },
watch: true,
});
```
但由于 esbuild 无法操作 AST,所以一些需要操作 AST 的 babel 插件无法与之兼容,导致生产环境很少直接使用 esbuild 的 bundler 模块。
幸运的是 minifier 模块可以直接替换 terser 使用,可以用于生产环境:
```typescript
esbuild.transform(code, {
minify: true,
});
```
由于 esbuild 牺牲了一些包大小换取了更高的执行效率,因此压缩后包体积会稍微大一些,不过也就是 177KB 与 165KB 的区别,几乎可以忽略。
esbuild 比较底层,所以可以与后续介绍的上层构建工具结合使用,当然根据工具设计理念,是否内置,内置到什么程度,以及是否允许通过插件替换就是另一回事了。
### snowpack
snowpack 是一个相对轻量的 bundless 方案,之前也写过一篇 [精读 snowpack](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md),其实 bundless 就是利用浏览器支持的 [ESM import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 特性,利用浏览器进行模块间依赖加载,而不需要在编译时进行。
跳过编译时依赖加载可以省很多事,比如不用考虑 tree shaking 问题,也不用为了最终产物加速而使用缓存,相当于这些工作交给最终执行的浏览器了,而浏览器作为最终运行时容器,比编译时工具更了解应该如何按需加载。
仅从编译时来看,修改单个文件的编译速度与项目整体大小有关,而若不考虑整体项目,仅编译单个文件(最多递归一下有限的依赖模块,解决比如 TS 类型变量判断问题)时间复杂度一定是 O(1) 的。
实际上我们很少单独使用 snowpack,因为其编译使用的 esbuild 还未达到 1.0 稳定版本,在生态兼容与产物稳定性上存在风险,所以编译打包时往往采用 rollup 或 webpack,但这种割裂也导致了开发与生产环境不一致,这往往代表着更大的风险,因此在 vite 框架可以看到这块的取舍。
snowpack 是开箱即用的:
```json
// package.json
"scripts": {
"start": "snowpack dev",
"build": "snowpack build"
},
```
我们还可以增加 `snowpack.config.js` 配置文件开启 `remote` 模式:
```js
// snowpack.config.js
module.exports = {
packageOptions: {
"source": "remote",
}
};
```
`remote` 模式是 [Streaming Imports](https://www.snowpack.dev/guides/streaming-imports#how-streaming-imports-work),即不用安装对应的 npm 包到本地,snowpack 自动从 [skypack](https://www.skypack.dev/) 读取文件并缓存起来。
snowpack 看起来更多是对 bundless 纯粹的尝试,而不是一个适合满足日常开发的工具,因为日常开发需要一个一站式工具,这就是后面说的 vite 与 wmr。
### vite
可以理解为结合了 snowpack 特色的一站式构建工具,从开发到发布全套流程都帮你搞定。
涉及的用法非常多,具体内容可以看 [官方文档](https://vitejs.dev/)。
与 snowpack 不同的是,snowpack 生产打包的产物是独立的文件,而 vite 没有采用 esbuild 而是 rollup 打包,目的是为了打包为一个整体,并规避 esbuild 不稳定的风险。
另外由于 vite 集成化更高,比 snowpack 多了许多功能,比如 css 拆分、多页、使用 esbuild 进行依赖预构建、monorepo 支持、对多框架支持、SSR 等等。具体可以看 [文档介绍](https://vitejs.dev/guide/comparisons.html#snowpack)。然而原文说这有利有弊,好处是开箱即用,弊端是缺乏定制的灵活性。
其实革命性突破主要是 bundless,在这基础上发展出一系列便捷的功能,这值得每一个工程化团队学习。其实就算决定再造一个轮子,也是维持 90% 功能不变的基础上,在默认的偏好设置做一些微调,而这些大多可以用 [插件](https://vitejs.dev/guide/api-plugin.html) 解决。
总结下来,Vite 是一个既积极拥抱新特性,又为生产环境考虑的工程化全家桶,相比之下,技术栈过于前沿的工具只能称为玩具,而 Vite 是真的可以用一用的。
### wmr
由 preact 作者开发,可以理解为 preact 版的 vite。所以对于 preact 技术栈的开发者更加友好,集成度更高。
原文提到的另一个特色是,wmr 使用了 [htm](https://github.com/developit/htm) 转换 JSX,使其获得了更加精确的报错体验,即可以精确到源码行的同时指定到具体列。
综合功能和 vite 差不多,单页 + ssr 都支持,如果你平时使用 preact,或者想开发一个体积极小的项目,可以考虑用 wmr 全家桶。
## 总结
新一代前端构建工具最大特色有两个:更底层的语言编写、bundless,如果用一个词描述就是高性能。积极拥抱浏览器新特性或者知识跨界都可以帮助前端领域取得新的突破。
另外构建工具已经变得越来越集成化,从仅用于编译的 esbuild,到支持开发的 snowpack,再到内置了最佳实践、甚至支持比如 ssr 等后端能力、最后到垂直场景的 [vitePress](https://github.com/vuejs/vitepress),每抽象一次,都更开箱即用,但带来的灵活性降低也成为各团队自己造轮子的理由,越上层越是有自己造轮子的冲动。
这和可视化领域很像,可视化从最底层的 svg、canvas、webgl 到基于其封装的命令式框架,再到数据驱动开发框架、完全 JSON 配置化的图表库、甚至到零配置,根据数据猜配置的智能化项目,也是配置越来越少,但灵活度越来越低,使用什么层次的完全看项目对细节的要求。
不过工程化相对还是标准化的,因为可视化面向的是用户,而工程化面向的是程序员,我们不能控制用户需求,但可以控制程序员的开发习惯 :P。
最后,除了升级你的构建工具外,换一台 M1 芯片电脑也可以极大提升开发效率,笔者亲测 webpack 构建速度提升 3 倍!
> 讨论地址是:[精读《新一代前端构建工具对比》· Issue #316 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/316)
**如果你想参与讨论,请 [点击这里](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,125 @@
不知道你上次思考前端职业规划是什么时候?
如果你是一位学生,你肯定对前端这个职业感到陌生,你虽然没有经验,但却对未来充满好奇,你有大把时间来思考,但可能摸不着方向,有种拳头打在棉花上的无力感。
如果你已经参加了工作,不论是刚开始实习,还是工作了 3 年、5 年甚至 10 年,一定觉得非常充实,但真正用于思考的时间足够吗?如果维持现状,再过 5 年自己的提升点在哪里?如果你对这些结论不清晰,很可能是缺乏了对职业规划的思考。
这种缺乏职业规划的焦虑已经发展成为了商机。当你没有清晰职业规划,正在迷茫的时候,培训机构站出来说,是不是对职业规划充满焦虑?如果是,可以订购我们的课程,名牌大厂 P10 带你跑赢职场。其实课程确实是干货,但一个具体课程并不能代替你自己的思考,你需要自己想明白自己想要的,而不是被别人灌输思想,因为职场没有标准路线,但培训机构的文案确实有标准写法。
所以这篇前端职业规划是站在我自己角度写的,你如果也在思考长线发展问题,可以作为参考。
我总结出三个主要思考方向,分别是 **知识分类**、**领域深耕**、**经济视角**。
**知识分类** 指的是你对知识的理解是否成体系。现在全球每天新增的知识,一个人穷尽一生也学不完,如果不建立一套你自己的知识筛选标准,长期发展就无从谈起。
**领域深耕** 是实践,天天学习也是没有用的,你必须要做出什么有价值的事情,才能为行业带来贡献,或者说将知识转化为财富。当然不同职业学习与实践的比例是不同的,比如理论物理可能模糊了学习与实践的边界,而在职场环境的工程师,更容易区分什么是学习,什么是实践。
**经济视角** 是说你要能够带着经济视角看问题。可以说没有经济活动,我们一切学习、生产、职业都没有任何意义,因为推动我们学习、推动社会生产的动力是交易,没有经济活动就没有需求,需求是推动一切活动的基础。稍微理解了经济和生产的关系,就能理解为什么技术要为商业服务,因为任何技术都要有转化为商业价值的潜力才值得被研究,大到社会价值,小到产品价值,都一样。
下面我分别讲讲自己对每个方向的理解。
## 知识分类
作为前端,为了保持技术敏锐度,我们会订阅许多专栏了解新知识。仅我知道的周更专栏就有 30 个,其实根据一些专门整理好的专栏检索网站,每周甚至可以看到超过 100 种不同的前端专栏。大部分专栏都在做文章聚合,每篇专栏聚合的文章一般有 5 篇到 30 篇不等,这样即便去除重复,一周至少有几百篇新的前端技术文章等你去读,所以有些同学会觉得焦虑,甚至喊出学不动了。
我每周写前端精读恰好也要找一些文章阅读,但几年下来,我恰恰觉得每周根本找不到有用的素材。就以本周的 [javascript weekly](https://javascriptweekly.com/issues/539) 为例,我摘了一些文章标题:
- [DOM Events: A Way to Visualize and Experiment with the DOM Event System](https://javascriptweekly.com/link/108484/web)。
- [Introducing WebContainers: Run Node.js Natively in the Browser](https://blog.stackblitz.com/posts/introducing-webcontainers/)。
- [New & Updated Course: Complete Intro to React v6 with Brian Holt](https://javascriptweekly.com/link/108483/web)
- [Parcel 2 Beta 3: A Wild Rust Appears!](https://v2.parceljs.org/blog/beta3/)
- [2D Optics Demos in JavaScript](https://javascriptweekly.com/link/108493/web)
- [A Complete Beginner's Guide to Next.js](https://www.youtube.com/watch?v=nBkRxwHMrto)
- [How to Create Reusable Web Components with Lit and Vue](https://javascriptweekly.com/link/108496/web)
第一篇是通过可视化帮你理解 DOM 事件的文章,UI 很有意思,但 DOM 事件作为前端基础,精读实在不适合拿过来炒冷饭,这个知识点讲一遍就行了,没必要做成 UI 后再讲一遍。
第二篇是讲一项技术可以让 Node 运行在浏览器的,这确实是一个新技术,但现阶段我们没必要为这项技术找场景,只要知道有这个东西就行了,没必要仔细阅读。第三篇是对 React 的完整教程,非常体系化,但没有新东西,适合前端新人读,所以也不需要看。
再后面几篇分别是框架升级带来的特性介绍、一个有趣的可视化效果、Next.js 新手入门、如何用 [Lit](https://lit.dev/) 框架开发组件。这些知识从直觉来看属于可读可不读的,读了吧觉得好像对自己没什么成长,不读又觉得错过了什么,真的像鸡肋。
如果你看到这些 Feed 流也有犹豫的感觉,我建议你建立一套前端知识分类体系。就像学习武功,如果你不了解什么是基本功,什么是花拳绣腿,那么每天面临几百本推送过来的 “武学新闻” 确实是无从学起,而且也学不过来。
在技术领域,知识分类体系是有规可循的,大致可以讲知识分为两种类型:通用、行业知识。
通用知识是指最为基础、适用面也最大的知识,比如数理化,这些知识我们上学时都学过,工作中用到的知识都是建立在这些通用知识基础之上的,比如没有一定数学基础就难以学习计算机可视化领域,因为其中会大量运用数学知识。
通用知识最有用,也最保值,所以学校时就安排给我们了,那么大学其实就在教通用行业知识,所以这个阶段如果没有打牢的基础,想要弥补也很简单,只要按照大学教材温习一遍就好了,对于计算机领域的通用知识一般有计算机原理、操作系统、设计模式、编译原理、数据结构、算法等。
领域通用知识看上去比较死板,而初入工作的同学一般都在做拧螺丝钉的事,往往会忽略行业通用知识的重要性,但当你不断深入接触公司核心技术时,会发现大量运用了大学里教的那些通用知识,等用到的时候再学就迟了。
如果说行业通用知识的保值时间是 30 年,那接下来提到的行业专用知识的保值时间只有 1 年。行业专用知识就是我们在 Weekly 上看到的大部分内容,也包括培训班帮我们速成的前端框架、API 等知识。这些知识非常有用,接地气,而且刚接触工作时第一时间就要用到,但这些知识最大的问题就是太过于上层,以至于同类产品过多,可替代性强,知识点可以随着新版本发布全变了样。
就像项目脚手架工具,现在每天都会出一个基于 webpack 或者 rollup 包装的新品牌,这种脚手架就不值得学习,你也不需要把新出的脚手架当作新知识,因为这些知识的生命周期大部分不到一年,大多没有人用,最重要的是除了名字以外,组成要素里没有任何新知识,所以读完源码也学不到新知识。更最重要的是,你无法根据这些知识生产同类产品,所以如果你真的想学脚手架相关知识,认真读好一个主流脚手架源码就行了,以后除了工作中用到,不需要看任何使用文档。
对于架构能力也一样,我们在工作中通过踩坑甚至把一个项目做失败得出的经验,可能只是设计模式这本书里提到的一个常见误区;我们在设计一个非常复杂的系统时,用到的模块通信设计,可能只是操作系统设计里的一种常见通信方法。一个能理解操作系统复杂度的人,基本上可以处理与其等价复杂度的软件工程问题,而软件工程的复杂度其实很难超越操作系统,所以与其在项目里试错,不如从这些基础知识里找答案。
所以如果你想在职业规划上更进一步,检查一下自己的基础是否牢固。如果你通用知识特别扎实,就可以快速学会行业基础知识,根据行业基础知识,你甚至可以独立创造任何一个新的框架,这些框架都会成为别人学习到的行业专用知识,如果另一位同学没有打基础,把时间都用在学习你做的框架上,那么他的职业发展一定程度会被你左右,而他如果只停留在用的阶段,而不了解实现原理,从长期来看,你的职业天花板一定会更高。
关于哪些是通用基础知识、行业基础知识、行业专用知识,这里不给出具体的建议,相信每个人都会有自己的判断。
## 领域深耕
> 这段思考 **不适用于** 刚参加工作的前端同学。
前端有一句有名的鸡汤 “前端不是因为做交互界面,而是因为站在业务的最前端”,其实这句话是有问题的,我觉得每一位工作经验超过三年的前端同学都有一种在业务领域的无力感。
其实最核心的业务模型天然在后端,这是因为前端只是一个用户与业务系统交互的窗口,没有前端,用户也可以和接口直接交互,只是这么做成本很大,所以为了降低用户上手难度,或者带来更好的用户体验,才需要不断升级 UI 界面,所以 UI 界面和后端往往是多对一的关系,移动端、小程序、网页对应的接口都是一套,目的就是为了方便任何场景用户都能轻松触达业务,所以作为前端,首先要对前端存在的原因有正确的认识。
注意这里说的是业务模型,没有提到体验深度,如果讲究体验深度,自然只有前端能做到。然而前端本质还是锦上添花的部分,因为在任何行业耕耘久了,如果仅仅只考虑前端,那么目标永远是体验度量、研发提效的事情,很少触及到业务层,以至于前端在业务价值的体现不直接,比较难解释体验度量、研发提效与最终业务增长之间的关系。
所以对于有一定工作经验的前端同学,想要更进一步,一定要在业务领域深耕。
那么如何在业务领域深耕呢?首先你要抛开前端视角,用业务眼光看问题,否则还是会陷入无尽的交互细节。首先要了解你所在的领域,比如笔者在的数据领域,要知道行业的历史、现状和未来,有哪些产品,每种产品的商业模式是什么,产品之间有什么关联,现在的产品距离头部产品还有哪些差距,今年产品目标主要解决什么问题,三年目标是什么等等。每个同学首先都应该理解产品,其次再产生研发、产品经理的分工。
然后审视一下自己的工作,在产品核心能力里扮演者什么角色?比如做 BI 工具,其核心是数据分析能力与报表可视化分析能力,如果你总在做类似报表列表页、个人中心这种通用中后台的工作,你就要想想,这些工作是不是可以外包出去,如果不行,那就想办法做一些领域搭建,往通用领域转吧。
当你审视了自己工作,发现核心产品能力与你工作内容不相符,而你又不想转到前端中后台通用领域一直做研发提效的事情,这时候你就要想办法和老板沟通改变一下工作内容了,你可以找一些前端也能接触强业务模型的领域,比如 BI 分析,数据可视化等等。其实通用领域也有不少深水区,比如语雀背后的富文本编辑器、流程图、研发工作台、业务组件库等等都是可以做深的通用领域,当你想再上一层楼时,就要像玉伯一样成为语雀整个产品的引领者,这样你其实又进入了知识协作、生产力工具这个专业领域。
如果你既不想往通用技术领域发展,又无法改变工作内容,就尝试承担更多职责吧,如果可能的话,尝试参与后端业务逻辑的开发,这样可以帮助你深入、全面理解业务逻辑。其实前端 + 产品的路线也可以很好在专业领域做深,前端 + 后端路线也可以,你需要根据自己团队实际情况做出调整。
任何产品的研发团队都要有产品全局观,这就是刚才说的在技术之外,你对你所在业务领域的理解程度,理解程度越高,技术方向就越明确,但如果你的职业规划是再继续攀爬,就要成为整个产品负责人了。现在的年轻人非常上进,许多公司都在尝试采取活水政策,让想更进一步的年轻人尝试新方向开疆拓土,而不是留在一个成熟的团队里内卷。
## 经济视角
做职业规划的另一个目的当然是升职加薪了,但是你的薪资并不能无限膨胀,其增长大致还是符合市场规律的。另外任何工作都是一笔经济账,我们要带着技术、产品和经济视角看业务,才能做出合理的判断。
因为去年疫情原因,全球远程办公得到了积极实践,并且在未来依然有增长潜力,因此作为用人单位方,必定会逐渐放眼全球去看人力成本问题,因为在哪都能办公。从全球软件开发数据来看,美国的工资水平最高,中国软件工程师的工资也紧随其后,所以在软件领域中国已经不存在劳动力成本低廉的优势了,尤其当你工作经验丰富后,要竞争中高级岗位,中国软件公司开的薪资放眼全球都不低。
然而国家之间技术发展阶段、教育水平仍然存在差距,如果同样的资深技术专家岗位,国内与国外开的薪资持平,但中国的软件工程师架构水平完全不及美国的软件工程师,那么长期来看,这种错配会造成企业用人成本浪费,企业会在一定程度想办法优化一下人员构成的。因此作为前端,或者软件工程师,你必须清楚长期而言,你要和全球的软件工程师竞争,所以你还要充分了解你的领域在全球范围的发展阶段,人才水平如何。
以上是个人的经济账,接下来谈谈业务的经济账。
首先你要了解自己的技术是怎样转化为收入,覆盖自己工资的。我们首先看市场竞争,市场竞争通过价格调节供需关系,我们做的产品成本、售价很清楚,是否值得做一目了然。然而对于复杂产品需要多人协作,如果人与人之间再通过市场化机制合作,往往容易产生低效的结果,比如我做的按钮按照 3 元一个的价格卖给后端,那为了提升我的价值,我会提价到 5 元一个,然而倾向于给产品加更多的按钮,这样都在看短期利益,谁也不会为产品长期发展负责。
所以公司是一个相对大锅饭的组织,谁也不要给自己工作定价,大家都尽可能的打磨产品,月底按照合同约定给固定薪酬。这样做确实解决了产品长期发展的问题,但这套机制成熟后,尤其在大公司,刚毕业就去拧螺丝钉的同学很可能永远没有机会了解何为成本,没有成本概念,就难以想清楚为什么做事要考虑投入产出比,或者觉得 ROI 这个词很高级,其实这个词一点不高级,只是公司将它屏蔽了,但如果这导致你做技术完全不考虑成本,只追求让你激动的技术细节,或者只做你感兴趣的技术方向,那其实是不成熟的表现,你做的事情可能也难以被业务认可。
如果你想往更高层次发展,成本意识是一定要培养的,可以了解一下人力成本、机器成本、以及接入二方、三方服务的外部成本,了解这些成本后,再算算产品年营收是否能覆盖这些成本,如果想继续加人,那明年产品营收相应要翻多少,现在市场空间允许产品翻这么多吗?如果想提供更好的服务,要加机器,那么你的业务方是否会因为服务变好变得更多?衡量业务方增多带来的价值一般从订单价格,MAU 来看,如果服务外部,直接看价格是否覆盖成本就行了,如果服务内部,就看 MAU 是否值得投入这些机器成本。
然而也不能只看钱,市场份额也很重要。如果 Chrome 对研发投入只看年营收,那现在 IE 估计还是主流浏览器。其实 Chrome 在确立霸主地位后,对谷歌产品生态的打通、W3C 的话语权、开发者吸引力有很大提升,这些看不见的影响面难以直接转化为金钱来统计,所以如果你认为产品市场份额的提升可以带来长线价值,那么也可以把市场份额作为目标之一。
最后经济视角也不仅仅让我们停留在算业务帐上,经济学的边际收益理论可以指导我们优先做边际收益更大的事。当前业务产品矩阵中,拓展哪些产品可以快速弥补不足,如果做技术优化,优化哪些模块带来产品收益、可维护性收益最大,如果时刻能想清楚这些问题,那每年的产品、技术方向就不会跑偏。
## 总结
总结一下文中提到的三个思考方向,其实是职业生涯发展中可能遇到的三种问题。
工作时间久了就会发现,哪怕依然有学习的激情,但保持刚毕业那会的学习方式已经难有突破了,你会发现:工作实践用到的知识不会很多,反复读或者写入门技术文章,只会让自己停留在校招生的技术水平;自己所处的职业也限制了进一步发展,你需要思考怎么打破职业天花板;甚至只钻研技术领域都是不够的,大家都在谈成本,你在谈技术,天然就不在一个频道上。
本文也给出了对应的三个解决方案,**知识分类** 帮助你解决反复学习无用的、入门知识的问题;**领域深耕** 帮助你解决职业天花板的问题;**经济视角** 帮助你解决技术单一视角的问题。
其实职业有天花板很正常,没有哪个职业上升通道是一路无阻的,但人是活的,你可以逐渐改变自己,在适当的时候多看看业务、经济问题,学习知识也不要仅停留在表面,虽然这些你工作中可能根本用不到,但这其实是悖论,因为你没掌握某些知识,所以也没机会接触那些工作,想打破悖论只能从痛苦的自我打破边界开始。
与一般前端职业规划不同,我并没有说很多前端领域专有名词,或者点名要学哪些框架,因为我觉得人之间智商差距并不大,必须掌握的知识工作几年都能学会,而真正能拉开人之间差距的,不是智商,而是学习方法,或者学习路线,如果你把时间用在错误的地方,或者错误的阶段,终将积累成巨大差距。
希望我的思考可以对你有帮助。
> 讨论地址是:[精读《前端职业规划 - 2021 年》· Issue #317 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/317)
**如果你想参与讨论,请 [点击这里](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,388 @@
逻辑编排是用可视化方式描述逻辑,在一般搭建场景中用于代替逻辑描述部分。
更进一步的逻辑编排是前后端逻辑混排,一般出现在一站式 paas 平台,今天就介绍一个全面实现了逻辑编排的 paas 工具 [node-red](https://github.com/node-red/node-red),本周精读的内容是其介绍视频:[How To Create Your First Flow In Node-RED](https://www.youtube.com/watch?v=cVWVr_T7kQ0),介绍了如果利用纯逻辑编排实现一个天气查询应用,以及部署与应用迁移。
## 概述
想要在本地运行 Node-RED 很简单,只要下面两条命令:
```bash
npm install -g --unsafe-perm node-red
node-red
```
之后你就可以看到这个逻辑编排界面了:
<img width=800 src="https://img.alicdn.com/imgextra/i3/O1CN01XBNkBE1km65n40X5S_!!6000000004725-2-tps-2870-1564.png">
我们可以利用这些逻辑节点构建前端网站、后端服务,以及大部分开发工作。光这么说还比较抽象,我们接下来会详细介绍每个逻辑节点的作用,让你了解这些逻辑节点是如何规划设计的,以及逻辑编排到底是怎么控制研发规范来提高研发效率的。
Node-RED 截止目前共有 42 个逻辑节点,按照通用、功能、网络、序列、解析、存储分为六大类。
所有节点都可能有左右连接点,左连接点是输入,右连接点是输出,特殊节点可能有多个输入或多个输出,其实对应代码也不难理解,就是入参和出参。
下面依次介绍每个节点的功能。
### 通用
通用节点处理通用逻辑,比如手动输入数据、调试、错误捕获、注释等。
### inject
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01Bnf5ob1gvTk4e8nXR_!!6000000004204-2-tps-262-66.png">
手动输入节点。可以定期产生一些输入,由下一个节点消费。
举个例子,比如可以定期产生一些固定值,如这样一个这个对象:
```javascript
return {
payload: new Date(),
topic: "abc",
};
```
当然这里是用 UI 表单配置的:
<img width=350 src="https://img.alicdn.com/imgextra/i3/O1CN015CP9Vu1cAWfuNVNiK_!!6000000003560-2-tps-988-226.png">
之后就是消费,几乎后面任何节点都可以消费,比如利用 `change` 节点来设置一些环境变量时,或者利用 `template` 节点设置 html 模版时,都可以拿到这里输入的变量。如果在模版里,变量通过 `{{msg.payload}}` 访问,如果是其它表单,甚至可以通过下拉框直接枚举选择。
然而这个节点往往用来设置静态变量,更多的输入情况是来自其它程序或者用户的,比如 `http in`,这个后面会讲到。其实通过这种组合关系,我们可以把任意节点的输入从生产节点替换为 `inject` 节点,从而实现一些 mock 效果,而 `inject` 节点也支持配置定时自动触发:
<img width=300 src="https://img.alicdn.com/imgextra/i3/O1CN011Pk9ld1TIvOiQNp26_!!6000000002360-2-tps-694-268.png">
### debug
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN018jKYhO1phaGduUHB8_!!6000000005392-2-tps-256-64.png">
用来调试的,当任何输出节点连接到 debug 的输入后,将会在控制台打印出输出信息,方便调试。
比如我们将 `inject` 的输入连上 `debug` 的输入,就可以在触发数据后在控制台看到打印结果:
<img width=500 src="https://img.alicdn.com/imgextra/i4/O1CN01C01riZ1OzuUZSLETu_!!6000000001777-2-tps-1428-374.png">
当然如果你把输入连接到 debug,那么原有逻辑就中断了,然而任何输出节点都可以无限制的输出给其它节点,你只要同时把输出连接到 debug 与功能节点就行了:
<img width=250 src="https://img.alicdn.com/imgextra/i3/O1CN01nzMC5T1m7SB2DgqE8_!!6000000004907-2-tps-670-184.png">
### complete
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01F2H5IN1h6vdyqezT0_!!6000000004229-2-tps-262-66.png">
监听某些节点触发完成动作。通过这个节点,我们可以捕获任意节点触发的动作,可以接入 `debug` 节点打印日志,或者 `function` 节点处理一下逻辑。
可以监听全部节点,也可以用可视化方式选择要监听哪些节点:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01Sufo4B1bY3PIajzVI_!!6000000003476-2-tps-696-244.png">
#### catch
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01B5Q21C1yIhHuQLBfk_!!6000000006556-2-tps-258-66.png">
错误捕获节点,当任何或指定节点触发错误时输出,输出的格式为:
```text
error.message 字符串
错误消息。
error.source.id 字符串
引发错误的节点的ID。
error.source.type 字符串
引发错误的节点的类型。
error.source.name 字符串
引发错误的节点的名称。(如果已设置)
```
其实每个节点都有固定输出格式,这些固定格式限制了开发灵活度,但熟练掌握后可以大大提升开发效率,因为所有同类型节点格式都是一样的,这是逻辑编排带来规则约束的好处。
#### status
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01Wh5aZ01H4TVt0t1LJ_!!6000000000704-2-tps-260-66.png">
监听节点状态变化。
#### link in
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01FjAGpo1nJBwcdDDjV_!!6000000005068-2-tps-260-66.png">
只能连接 `link out``link in``link out` 就像一个传送门,用来整理逻辑编排节点,使之看上去易于维护。
比如下面的例子,在一个天气 `http in` 服务后,穿插了许多逻辑处理节点,有处理响应 html 内容的 `template` 节点,也有处理请求查询城市天气的 `http request` 服务,整体逻辑虽然聚合,但比较杂乱:
<img width=500 src="https://img.alicdn.com/imgextra/i4/O1CN01g81mDp1PtmoQID4dg_!!6000000001899-2-tps-1564-356.png">
较好的方式是分类,即类似代码开发中的模块化行为,将天气服务导出,其他任何用到的模块直接导入,这个导入动作就是通过 `link in` 实现的,`link out` -> `link in` 只是一个空间位置的变换,传输值是不会变的:
<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN017V2j9t1IGffzi0jNk_!!6000000000866-2-tps-1076-588.png">
这样模块看起来清晰了许多,如果要知道各个 “传送门” 见连接关系,只要鼠标点击其中一个就可以给出提示,看起来十分方便:
<img width=350 src="https://img.alicdn.com/imgextra/i4/O1CN01Dv0gju1MVZ0vYIR8K_!!6000000001440-2-tps-966-358.png">
#### link out
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01725F7V1YbJNMXzfea_!!6000000003077-2-tps-258-66.png">
`link in` 成对出现,用来导出输入值,后面对接 `link out` 可以像传送门一样将值传送过去,在视觉上不会形成连接线。
#### comment
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01hVy17l23PdMs9zEH7_!!6000000007248-2-tps-248-64.png">
注释,配合 `link` 系列使用,可以让逻辑编排 UI 更易于维护。
结合原视频的例子,对于天气服务,有创建环境变量逻辑,有查询逻辑,其中查询天气还分为查询当前天气、连续 5 天天气、查询国家信息,我们可以在 UI 上讲每块逻辑分组,并利用 `comment` 组件标记好注释,方便阅读:
<img width=500 src="https://img.alicdn.com/imgextra/i3/O1CN015NiEqe1FU0MLwZwdU_!!6000000000489-2-tps-1540-990.png">
### 功能
功能型节点,一般用于处理业务逻辑,所以包含了基础的 if else、js 代码、模版处理等等功能模块。
#### function
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01WLyQo81lBkGqbZgsO_!!6000000004781-2-tps-270-72.png">
最核心的 js 函数模块,你可以用它做任何事:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN010IMUYY1yUbaRguc8N_!!6000000006582-2-tps-910-386.png">
其输入会传导到 `msg` 对象,可以通过代码修改 `msg` 对象后再通过输出节点传导出去。
当然也可以访问和修改节点、流程、全局变量,这个在 `change` 节点里介绍。
#### switch
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01HqpK5Q1P0pHZbNkx3_!!6000000001779-2-tps-268-68.png">
对应代码的 switch,只是用起来更加方便,因为我们可以根据不同 case 导出不同的节点:
<img width=450 src="https://img.alicdn.com/imgextra/i4/O1CN013cbv1y1aArjtRZgyC_!!6000000003290-2-tps-1456-438.png">
注意看上图,因为有三条分支,所以节点的导出项也变成了三个,我们可以根据不同逻辑走不同的连接:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01NPqb4W1lFrpn0CgR1_!!6000000004790-2-tps-730-326.png">
#### change
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01YQ7jN428WZRbbXnSP_!!6000000007940-2-tps-270-70.png">
用来改变环境变量。环境变量分为三种,分别是当前节点、流程(画布)、全局(跨应用)。也就是说,变量可以存储在某个节点上,也可以存储在整个画布上,也可以跨画布存储在全局。
访问参数分别为 `msg.``flow.``global.`,设置这些参数后,就像全局变量一样,任何节点都可以在任何地方使用,比较方便。
比如应用固定了一些 URL 地址,直接把一串字符串写死在某个 `http in` 节点里并不明智,因为后面的 html 或者其它节点里可能会访问它,一旦你进行修改,影响面会非常广,因此最好将其设置为全局变量,在节点中通过变量方式访问:
<img width=280 src="https://img.alicdn.com/imgextra/i2/O1CN01m4pnL520HRQzKfylb_!!6000000006824-2-tps-724-94.png">
其实在控制台,可以看到这三种变量的值:
<img width=200 src="https://img.alicdn.com/imgextra/i2/O1CN01zkx3bt1hEFyyORfVN_!!6000000004245-2-tps-610-682.png">
当我们利用 `change` 节点赋值后,可以通过调试面板查看不同作用域全局变量的值:
<img width=200 src="https://img.alicdn.com/imgextra/i1/O1CN01ghAKzs1ROIoiWcQ1A_!!6000000002101-2-tps-600-222.png">
#### range
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01RfMrp7237JWZPKFfJ_!!6000000007208-2-tps-270-68.png">
区间映射,将一个范围的值映射到另一个范围。其实通过 `function` 模块也能完成,只是因为比较常用所以封装了一个特殊节点。其实用户也可以自己封装节点,具体方式可以参考 [官方文档](https://nodered.org/docs/creating-nodes/)。
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN0108N1Y81beSx6ofF89_!!6000000003490-2-tps-1212-324.png">
上图很容易理解,比如数据分析中归一化就可以用这个节点实现。
#### template
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01enr3Y61wnit7hcexe_!!6000000006353-2-tps-272-66.png">
以模版方式生成字符串或 json。
其实本质上也可以被 `function` 代替,只是用来写模版的话有高亮,维护起来比较方便。
内置了 [mustache](https://github.com/janl/mustache.js) 模版语法,通过 `{{}}` 方式使用变量。
比如我们通过 `inject` 注入一个变量给 `template`,并通过 `debug` 打印,流程是这样的:
<img width=600 src="https://img.alicdn.com/imgextra/i2/O1CN01Jj9eM21Mtq0Ym8sk6_!!6000000001493-2-tps-1660-322.png">
其中 `inject` 是这么配置的:
<img width=350 src="https://img.alicdn.com/imgextra/i2/O1CN01q23oD920maMfLkpah_!!6000000006892-2-tps-920-100.png">
可以看到,将 `msg.name` 设置为一个字符串,然后通过 `template` 访问 `name`:
<img width=450 src="https://img.alicdn.com/imgextra/i3/O1CN01IrgPL81IIxefRDgUz_!!6000000000871-2-tps-942-132.png">
#### delay
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01YannSw1xtxtvFQiGP_!!6000000006502-2-tps-270-68.png">
延迟发消息,一个快捷的工具,可以放在任何输入与输出中间,比如让上面的例子中,`inject` 触发后 5s 再打印结果,可以这么配置:
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN012ZqRoP1W1vXy6fjyD_!!6000000002729-2-tps-1288-122.png">
#### trigger
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01WGfLxF24PvERo8K09_!!6000000007384-2-tps-268-66.png">
一个消息触发器,相比 `inject`,可以更灵活的设置何时重新触发。
<img width=300 src="https://img.alicdn.com/imgextra/i3/O1CN013x6HjE1pAbkewAwhI_!!6000000005320-2-tps-916-958.png">
从配置可以看出,首先和 `inject` 一样发送一条消息,然后可以等待,或者等待被重置,或者周期性触发(这样就和 `inject` 一样),其中 “发送第二条消息到单独的输出” 和 `switch` 一样会多一个输出口。
然后有重置条件,即 `payload` 为什么值时重置。
通过这个组件可以看出来,其实每个节点都可以用 `function` 节点实现,只不过通过定制一个节点,可以用 UI 而非代码的方式配置,使用起来更方便。
#### exec
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01lUJSvx1LiYdYvfRgu_!!6000000001333-2-tps-268-70.png">
执行系统命令,比如 `ls` 等,这个在系统后台执行而非前端,所以是一个相当危险的节点。
我们可以在配置中写入任何命令:
<img width=450 src="https://img.alicdn.com/imgextra/i4/O1CN017Z9jN91TYxGBdlTrE_!!6000000002395-2-tps-1058-164.png">
#### rbe
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01r1lhS821eAhsVdjQN_!!6000000007009-2-tps-268-66.png">
异常报告节点(Report by Exception),比如说当输入变化时进行阻塞。
### 网络
用于创建网络服务,比如 http、socket、tcp、udp 等等,因为其它都不常用,这次仅介绍 http 服务。
#### http in
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01zo2L6k1eFgMlLHK1v_!!6000000003842-2-tps-262-66.png">
创建一个 http 服务,可以是任何接口或者 web 服务。
当你把 Method 设置为 `post`,连接到 `http response` 就创建了后端接口;当设置为 `get` 请求,并连接 `template` 写上 html 模版,并连接到 `http response` 就创建了 web 服务。
虽然这种方式创建 web 服务难以使用 react 或 vue 框架,不过自定义节点还是为其创造了可能性,或许真的可以把前端模块化文件定义为节点相互串联。
#### http response
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01ElYlgm1jkR2WGCDx4_!!6000000004586-2-tps-266-70.png">
http 返回,只能对接 `http in` 的输出,总是与 `http in` 成对使用。
如果只用了 `http in` 但没有用 `http response`,就相当于后端代码里处理了请求,但没有调用类似:
```typescript
res.send("hello word");
```
来向客户端发送内容。
#### http request
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01TkO4p11wRHTuXqCHX_!!6000000006304-2-tps-268-68.png">
`http in` 创建一个 http 服务不同,`http request` 直接发送一个网络请求并将返回值导入到输出节点。
视频中获取天气的例子,就用了 `http request` 发起请求获取天气信息:
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN01B6PIz61GTppGaeXgP_!!6000000000624-2-tps-1482-536.png">
不难看出,发送请求后,又使用了 `function` 节点处理返回结果。不过在逻辑编排中还是期望少使用 `function` 节点,因为除非有很好的命名,否则难以看出来节点含义,如果 `function` 处理内容过多或者 `function` 区块过多,就失去了逻辑编排的意义。
### 序列
序列是对数组进行处理的节点。
#### split
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01wQO6Hl23Dj4PliP2F_!!6000000007222-2-tps-268-68.png">
对应代码的 `split`,将字符串变为数组。
#### join
<img width=120 src="https://img.alicdn.com/imgextra/i1/O1CN01PTpi9Q22NVusDGfX8_!!6000000007108-2-tps-270-68.png">
对应代码的 `join`,一般与 `split` 配合使用,方便处理字符串。
#### sort
<img width=120 src="https://img.alicdn.com/imgextra/i3/O1CN01mHZRX3285Y4gq3w4y_!!6000000007881-2-tps-270-68.png">
对应代码 `sort`,只能根据 `key` 做简单的升序降序处理,对于简单场景比较方便,但对于复杂场景可能还会使用 `function` 节点代替。
#### batch
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01vvRGV71SXkbSkFCUt_!!6000000002257-2-tps-272-70.png">
批量接收输入流后,根据数量进行打包后统一输出,等于批量打包,可以按照数量或者时间间隔进行分组:
<img width=350 src="https://img.alicdn.com/imgextra/i3/O1CN014oSO1t1CAov3Bc5Jg_!!6000000000041-2-tps-826-292.png">
### 解析
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01VZcAqV29jE0GfTvU6_!!6000000008103-2-tps-270-390.png">
很容易理解,专门处理上述格式的数据,并按照数据特征输出,比如 csv 数据,可以每行一条消息的方式输出,或者打包为一个大数组以一条消息输出。
当然也可以被 `function` 节点代替,那么解析方式与输出方式都可以自定义。
### 存储
持久化存储,一般存储为文件。
#### file
<img width=120 src="https://img.alicdn.com/imgextra/i4/O1CN01ndGqqL1T716IwPxcd_!!6000000002334-2-tps-266-74.png">
输出为文件。
#### file in
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN0144b1Jt23aATTxnfDo_!!6000000007271-2-tps-270-68.png">
以文件作为输入,并将文件结果作为输出。
#### watch
<img width=120 src="https://img.alicdn.com/imgextra/i2/O1CN01UlIPAu1mNwR15k4cL_!!6000000004943-2-tps-260-68.png">
监听目录或文件的修改。
## 精读
看了上面 node-red 功能后,相信你对逻辑编排已经有较为体系化的认识了。
逻辑编排的目的是为了让非研发人群也可以快速上手研发工作,因此注定是为 paas 工具服务的,而逻辑编排到底好不好用,取决于节点功能是否完备,以及各节点之间通信是否顺畅,像 node-red 逻辑编排方案,在完备性上做的较为成熟,可以说只要熟练掌握了几个核心节点规则,使用起来还是非常提效的。
逻辑编排也有天然缺点,就是当所有节点都退化为 `function` 节点后,会存在两个问题:
- 所有节点都是 `function` 节点,即便有说明,但内部实现逻辑非常自由,导致逻辑编排无法起到约束输入输出的作用。
- 退化到代码函数式调用,本质上与写代码无异。逻辑编排之所以提效,很大程度上是我要的业务逻辑刚好与节点功能匹配,以低成本 UI 配置的方式实现效率才高。
然而这也是有解决方法的,如果你的业务无法被现有的逻辑编排节点满足,你可以尝试抽象一下,自己梳理出业务常用的节点,并用合理的配置封装,只要常用业务逻辑可以被封装为逻辑节点,逻辑编排就还有为业务提效的空间。
## 总结
逻辑编排是一种极端,即用 UI 方式描述通用业务逻辑,降低非专业开发人员的上手门槛。通过对 [node-red](https://github.com/node-red/node-red) 的分析可以发现,一个较为完备的逻辑编排系统还是能带来价值的。
然而针对非专业开发人员降本提效还有一种极端,就是完全代码化,但是把代码模块化、函数库、工具链甚至低代码平台建设的非常完备,以至于写代码的效率根本不低,这条路走到极致也不错,因为既然要深入开发系统,同样是投入时间学习,为什么学习写代码就一定比学习拖拽 UI 效率低呢?如果有高度封装的函数与工具辅助,效率不见得比 UI 拖拽来的低。
然而 node-red 在创建前端 UI 的模版上还可以再增强一下,把 `template` 从节点升级为 UI 搭建画布,逻辑编排仅用来处理逻辑,这样对大型全栈项目的前端开发体验会更好。
> 讨论地址是:[精读《低代码逻辑编排》· Issue #319 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/319)
**如果你想参与讨论,请 [点击这里](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)
+6 -6
View File
@@ -14,9 +14,9 @@ Nestjs 是我见过的,将 Typescript 与 Nodejs Framework 结合的最好的
Nestjs 不是一个新轮子,它是基于 Express、socket.io 封装的 nodejs 后端开发框架,对 Typescript 开发者提供类型支持,也能优雅降级供 Js 使用,拥有诸多特性,像中间件等就不展开了,本文重点列举其亮点特性。
## 2.1 Modules, Controllers, Components
## 2.1 Modules, Controllers, Providers
Nestjs 开发围绕着这三个单词,Modules 是最大粒度的拆分,表示应用或者模块。Controllers 是传统意义的控制器,一个 Module 拥有多个 Controller。Components 一般用于做 Services,比如将数据库 CRUD 封装在 Services 中,每个 Service 就是一个 Component
Nestjs 开发围绕着这三个单词,Modules 是最大粒度的拆分,表示应用或者模块。Controllers 是传统意义的控制器,一个 Module 拥有多个 Controller。Providers 一般用于做 Services,比如将数据库 CRUD 封装在 Services 中,每个 Service 就是一个 Provider
## 2.2 装饰器路由
@@ -50,7 +50,7 @@ export class UsersController {
## 2.3 模块间依赖注入
Modules, Controllers, Components 之间通过依赖注入相互关联,它们通过同名的 `@Module` `@Controller` `@Component` 装饰器申明,如:
Modules, Controllers, Providers 之间通过依赖注入相互关联,它们通过同名的 `@Module` `@Controller` `@Injectable` 装饰器申明,如:
```typescript
@Controller()
@@ -61,7 +61,7 @@ export class UsersController {
```
```typescript
@Component()
@Injectable()
export class UsersService {
getAllUsers() {
return []
@@ -72,12 +72,12 @@ export class UsersService {
```typescript
@Module({
controllers: [ UsersController ],
components: [ UsersService ],
providers: [ UsersService ],
})
export class ApplicationModule {}
```
`ApplicationModule` 申明其内部 Controllers 与 Components 后,就可以在 Controllers 中注入 Components 了:
`ApplicationModule` 申明其内部 Controllers 与 Providers 后,就可以在 Controllers 中注入 Providers 了:
```typescript
@Controller()
+195
View File
@@ -0,0 +1,195 @@
React 18 带来了几个非常实用的新特性,同时也没有额外的升级成本,值得仔细看一看。
下面是几个关键信息:
- [React 18 工作小组](https://github.com/reactwg/react-18)。利用社区讨论 React 18 发布节奏与新特性。
- [发布计划](https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html)。目前还没有正式发布,不过 `@alpha` 版已经可用了,[安装 alpha 版](https://github.com/reactwg/react-18/discussions/9)。
- [React 18 新特性介绍](https://github.com/reactwg/react-18/discussions/4)。虽然还未正式发布,但特性介绍可以先行,本周精读主要就是解读这篇文档。
## 精读
总的来说,React 18 带来了 3 大新特性:
- Automatic batching。
- Concurrent APIS。
- SSR for Suspense。
同时为了开启新的特性,需要进行简单的 `render` 函数升级。
### Automatic batching
batching 是指,React 可以将回调函数中多个 `setState` 事件合并为一次渲染。
也就是说,`setState` 并不是实时修改 State 的,而将多次 `setState` 调用合并起来仅触发一次渲染,既可以减少程序数据状态存在中间值导致的不稳定性,也可以提升渲染性能。可以理解为如下代码所示:
```typescript
function handleClick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// 仅触发一次渲染
}
```
但可惜的是,React 18 以前,如果在回调函数的异步调用中执行 `setState`,由于丢失了上下文,无法做合并处理,所以每次 `setState` 调用都会立即触发一次重渲染:
```typescript
function handleClick() {
// React 18 以前的版本
fetch(/*...*/).then(() => {
setCount((c) => c + 1); // 立刻重渲染
setFlag((f) => !f); // 立刻重渲染
});
}
```
而 React 18 带来的优化便是,任何情况都可以合并渲染了!即使在 `promise``timeout` 或者 `event` 回调中调用多次 `setState`,也都会合并为一次渲染:
```typescript
function handleClick() {
// React 18+
fetch(/*...*/).then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
// 仅触发一次渲染
});
}
```
当然如果你非要 `setState` 调用后立即重渲染也行,只需要用 `flushSync` 包裹:
```typescript
function handleClick() {
// React 18+
fetch(/*...*/).then(() => {
ReactDOM.flushSync(() => {
setCount((c) => c + 1); // 立刻重渲染
setFlag((f) => !f); // 立刻重渲染
});
});
}
```
开启这个特性的前提是,将 `ReactDOM.render` 替换为 `ReactDOM.createRoot` 调用方式。
### 新的 ReactDOM Render API
升级方式很简单:
```typescript
const container = document.getElementById("app");
// 旧 render API
ReactDOM.render(<App tab="home" />, container);
// 新 createRoot API
const root = ReactDOM.createRoot(container);
root.render(<App tab="home" />);
```
API 修改的主要原因还是语义化,即当我们多次调用 `render` 时,不再需要重复传入 `container` 参数,因为在新的 API 中,`container` 已经提前绑定到 `root` 了。
`ReactDOM.hydrate` 也被 `ReactDOM.hydrateRoot` 代替:
```typescript
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// 注意这里不用调用 root.render()
```
这样的好处是,后续如果再调用 `root.render(<Appx />)` 进行重渲染,我们不用关心这个 `root` 来自 `createRoot` 或者 `hydrateRoot`,因为后续 API 行为表现都一样,减少了理解成本。
### Concurrent APIS
首先要了解 Concurrent Mode 是什么。
简单来说,Concurrent Mode 就是一种可中断渲染的设计架构。什么时候中断渲染呢?当一个更高优先级渲染到来时,通过放弃当前的渲染,立即执行更高优先级的渲染,换来视觉上更快的响应速度。
有人可能会说,不对啊,中断渲染后,之前渲染的 CPU 执行不就浪费了吗,换句话说,整体执行时常增加了。这句话是对的,但实际上用户对页面交互及时性的感知是分为两种的,第一种是即时输入反馈,第二种是这个输入带来的副作用反馈,比如更新列表。其中,即使输入反馈只要能优先满足,即便副作用反馈更慢一些,也会带来更好的体验,更不用说副作用反馈大部分情况会因为即使输入反馈的变化而作废。
由于 React 将渲染 DOM 树机制改为两个双向链表,并且渲染树指针只有一个,指向其中一个链表,因此可以在更新完全发生后再切换指针指向,而在指针切换之前,随时可以放弃对另一颗树的修改。
以上是背景输入。React 18 提供了三个新的 API 支持这一模式,分别是:
- startTransition。
- useDeferredValue。
- &lt;SuspenseList&gt;。
后两个文档还未放出,所以本文只介绍第一个 API:startTransition。首先看一下用法:
```typescript
import { startTransition } from "react";
// 紧急更新:
setInputValue(input);
// 标记回调函数内的更新为非紧急更新:
startTransition(() => {
setSearchQuery(input);
});
```
简单来说,就是被 `startTransition` 回调包裹的 `setState` **触发的渲染** 被标记为不紧急的渲染,这些渲染可能被其他紧急渲染所抢占。
比如这个例子,当 `setSearchQuery` 更新的列表内容很多,导致渲染时 CPU 占用 100% 时,此时用户又进行了一个输入,即触发了由 `setInputValue` 引起的渲染,此时由 `setSearchQuery` 引发的渲染会立刻停止,转而对 `setInputValue` 渲染进行支持,这样用户的输入就能快速反映在 UI 上,代价是搜索列表响应稍慢了一些。而一个 `transition` 被打断的状态可以通过 `isPending` 访问到:
```typescript
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
```
其实这比较符合操作系统的设计理念,我们知道在操作系统是通过中断响应底层硬件事件的,中断都非常紧急(因为硬件能存储的消息队列非常有限,操作系统不能即使响应,硬件的输入可能就丢失了),因此要支持抢占式内核,并在中断到来时立刻执行中断(可能把不太紧急的操作放到下半部执行)。
对前端交互来说,用户角度发出的 “中断” 一般来自键盘或鼠标的操作,但不幸的是,前端框架甚至是 JS 都过于上层,它们无法自动识别:
1. 哪些代码是紧急中断产生的。比如 `onClick` 就一定是用户鼠标点击产生的吗?不一定,可能是 `xxx.onClick` 主动触发的,而非用户触发。
2. 用户触发的就一定是紧急中断吗?不一定,比如键盘输入后,`setInputValue` 是紧急的,而更新查询列表的 `setSearchQuery` 就是非紧急的。
我们要理解到前端场景对用户操作感知的局限性,才能理解为什么必须手动指定更新的紧急程度,而不能像操作系统一样,上层程序无需感知中断的存在。
### SSR for Suspense
完整名称是:Streaming SSR with selective hydration。
即像水流一样,打造一个从服务端到客户端持续不断的渲染管线,而不是 `renderToString` 那样一次性渲染机制。selective hydration 表示选择性水合,水合指的是后端内容打到前端后,JS 需要将事件绑定其上,才能响应用户交互或者 DOM 更新行为,而在 React 18 之前,这个操作必须是整体性的,而水合过程可能比较慢,会引起全局的卡顿,所以选择性水合可以按需优先进行水合。
所以这个特性其实是转为 SSR 准备的,而功能启用载体就是 Suspense(所以以后不要再认为 Suspense 只是一个 loading 作用)。其实在 Suspense 设计之初,就是为了解决服务端渲染问题,只是一开始只实装了客户端测的按需加载功能,后面你会逐渐发现 React 团地逐渐赋予了 Suspense 更多强大能力。
SSR for Suspense 解决三个主要问题:
- SSR 模式下,如果不同模块取数效率不同,会因为最慢的一个模块拖慢整体 HTML 吞吐时间,这可能导致体验还不如非 SSR 来的好。举一个极端情况,假设报表中一个组件依赖了慢查询,需要五分钟数据才能出来,那么 SSR 的后果就是白屏时间拉长到 5 分钟。
- 即便 SSR 内容打到了页面上,由于 JS 没有加载完毕,所以根本无法进行 hydration,整个页面处于无法交互状态。
- 即便 JS 加载完了,由于 React 18 之前只能进行整体 hydration,可能导致卡顿,导致首次交互响应不及时。
在 React 18 的 server render 中,只要使用 `pipeToNodeWritable` 代替 `renderToString` 并配合 `Suspense` 就能解决上面三个问题。
使用 `pipeToNodeWriteable` 可以看 [这个例子](https://codesandbox.io/s/festive-star-9hfqt?file=/server/render.js:1043-1575)。
最大的区别在于,服务端渲染由简单的 `res.send` 改成了 `res.socket`,这样渲染就从单次行为变成了持续性的行为。
那么 React 18 的 SSR 到底有怎样的效果呢?[这篇介绍文档](https://github.com/reactwg/react-18/discussions/37) 的图建议看一看,非常直观,这里我简要描述一下:
1. 被 `<Suspense>` 包裹的区块,在服务端渲染时不会阻塞首次吞吐,而且在这个区块准备完毕后(包括异步取数)再实时打到页面中(以 HTML 模式,此时还没有 hydration),在此之前返回的是 `fallback` 的内容。
2. hydration 的过程也是逐步的,这样不会导致一下执行所有完整的 js 导致页面卡顿(hydration 其实就是 React 里写的回调注册、各类 Hooks,整个应用的量非常庞大)。
3. hydration 因为被拆成多部,React 还会提前监听鼠标点击,并提前对点击区域优先级进行 hydration,甚至能抢占已经在其他区域正在进行中的 hydration。
那么总结一下,新版 SSR 性能提高的秘诀在于两个字:按需。
而这个难点在于,SSR 需要后端到前端的配合,在 React 18 之前,后端到前端的过程完全没有优化,而现在将 SSR HTML 的吞吐改成多次,按需,并且水合过程中还支持抢占,因此性能得到进一步提升。
## 总结
结合起来看,React 18 关注点在于更快的性能以及用户交互响应效率,其设计理念处处包含了中断与抢占概念。
以后提起前端性能优化,我们就多了一些应用侧的视角(而不仅仅是工程化视角),从以下两个应用优化视角有效提升交互反馈速度:
1. 随时中断的框架设计,第一优先级渲染用户最关注的 UI 交互模块。
2. 从后端到前端 “顺滑” 的管道式 SSR,并将 hydration 过程按需化,且支持被更高优先级用户交互行为打断,第一优先水合用户正在交互的部分。
> 讨论地址是:[精读《React 18》· Issue #336 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/336)
**如果你想参与讨论,请 [点击这里](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,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)
@@ -31,14 +31,15 @@ Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法
体验策略的核心思路是以任务为导向的。主要通过四个方面去构建体验策略:流程与方法、度量体系、运营活动和最佳实践。
## 精读
整个分享感受颇深,但就『自然』这个关键词才生了我自己的疑问主字号、字阶和行高是否存在关系
整个分享感受颇深,但就『自然』这个关键词才生了我自己的疑问主字号、字阶和行高是否存在关系
在梳理的这层关系上,缺少了对字体的讨论,而字体又是非常关键的因素。我在之后,断断续续查阅了不少资料。写下关系背后还要考虑的问题。
1. 字体
我在查阅资料的时候,发现 x-height 在西文字体中的概念。在英文字体的设计中,字体的高度体包含三部份,以基线 (baseline) 为中央,以上称之上行区域 (ascender area),基准线内称之为 x-height,以下称为下行区域 (descender area)。小写西文字母中的核心部件都位于 x-height 位置中,这一位置也被称为排版的核心位置,是引导视线流动的关键。放一张在 wikipedie 上的图:
[image:CCC0E19B-0E90-4989-BD91-A2B33D38E8CD-6272-000031C10BD416C0/820px-Typography_Line_Terms.svg.png]
我在查阅资料的时候,发现 x-height 在西文字体中的概念。在英文字体的设计中,字体的高度体包含三部份,以基线 (baseline) 为中央,以上称之上行区域 (ascender area),基准线内称之为 x-height,以下称为下行区域 (descender area)。小写西文字母中的核心部件都位于 x-height 位置中,这一位置也被称为排版的核心位置,是引导视线流动的关键。放一张在 wikimedia 上的图:
![](https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Typography_Line_Terms.svg/800px-Typography_Line_Terms.svg.png)
每一种西文字体的 x-height 是不一样的。非常幸运,Jukka Korpela 做了一网站专门可以测量 web 上字体的 x-height。其中,Arial 的 X-HEIGHT RATIO 是 0.519,而 Tahoma 是 0.545Times New Roman 是 0.448。Arial 和 Times New Roman 之间的比例差距大概 17%。
@@ -47,6 +48,7 @@ Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法
这是第一个问题,第二个问题是中文字体没有 x-height,也就是说中文字体就等同于西文字体的全大写,错落的美感都没有。而且中文有一个问题是因为字形之前的差异,每个字之间的留白都不尽相同,看上去又会差一些。一般情况下,靠行间距来弥补视觉差,但总体上要排版达到西文字体的效果要花一些功夫。
2. 屏幕
我们的字体大小使用的是 points(pt),points 是一个物理衡量,它的标准是 72 points per inchPPI)。但我们不同设备的 PPI 都是不一样的,那么造成了同样的设定在不同屏幕下看到的字体也会有差异。
Macbook Pro 的 PPI 是 220Dell XPS 的 PPI 是 165iPhone 7 有 326,但 iPhone 7p 的 PPI 有 401,而一般 HDTV 的 PPI 是 30。其中,iPhoneMacbook 都是 retina 屏。
@@ -64,4 +66,4 @@ Macbook Pro 的 PPI 是 220Dell XPS 的 PPI 是 165iPhone 7 有 326,但
## 总结
曾经有国外的设计师有写文用黄金比例来构建字号与行高的关系,在一片喝彩中看到了资深设计师的反对,主要也是从以上和一些其它因素来说关系是比较难设定。
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
@@ -5,14 +5,14 @@ ES 模块为 JavaScript 开发者带来了官方并且标准化的模块系统
## 1. 引言
精读文章主要讨论了下面几点:
- 模块旨在解决些问题;
- 模块旨在解决些问题;
- 模块为开发者带来哪些;
- ES 模块化的工作机制;
- ES 模块化的现状;
## 2. 内容概要
### 模块旨在解决些问题
### 模块旨在解决些问题
JavaScript 开发可以简单地抽象成维护变量,赋值和计算操作。大量的代码在用于操作变量,开发者需要懂得如何去组织和维护这些变量。JavaScript 提供了一种方式,即函数作用域。在一个函数内只需要考虑这个函数的变量问题。不必去担心其他函数会操作这些变量。当然,随之带来的问题是,变量无法共享,无法在不同的函数之间相互共享变量。如果想要在作用域外共享变量,只能通过外层作用域,或者全局作用域。
@@ -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[]>(
@@ -422,7 +422,7 @@ function Article({ id }) {
return () => {
didCancel = true;
};
}, [API.fetchArticle]);
}, [id]);
// ...
}
@@ -14,7 +14,7 @@
大家都知道移动端即时通讯是一个唯一寡头市场,因此当米聊看到微信开始反超的时候,就已经知道这场战争已经结束。当时小米重点业务还在手机,米聊是团队试水的一款产品,但看到歪打误撞进入一个如此蓝海的市场,小米自己也很纠结要不要把资源都投入到米聊上。
反观微信,当时手机 QQ 也在做,本来怎么也轮不到微信出场,但张小龙、马化腾、张志东在微信简历了深夜小组,每天晚上都即时同步微信的进展,这让微信即时获取到了腾讯内部资源,在各种关键节点帮了很多忙,甚至让手机 QQ 技术大牛直接支持微信改善高并发问题,快速完成 QQ 好友导入功能。
反观微信,当时手机 QQ 也在做,本来怎么也轮不到微信出场,但张小龙、马化腾、张志东在微信建立了深夜小组,每天晚上都即时同步微信的进展,这让微信即时获取到了腾讯内部资源,在各种关键节点帮了很多忙,甚至让手机 QQ 技术大牛直接支持微信改善高并发问题,快速完成 QQ 好友导入功能。
雷军总结到 “如果腾讯一年后才有所反应,米聊胜率是 50%,如果是腾讯两三个月就有反应,米聊应该 100% 会死掉”。
@@ -32,7 +32,7 @@
前十年,手机设备制造厂商的格局发生了很大变化。国内经历了从小米,到 OPPO、VIVO,再到华为的演化。
印象深刻的是看了一个雷军创办小米前夕的访谈视频,雷军说 “大家看到苹果的成功,却没有看到这片蓝海的机会,现在手机制造领域竞争太不激烈了”。同时为了对抗苹果,谷歌开源了安卓源代码,小米利用这个机会打造一款符合中国人口味的手机操作系统,并借助用户社区与性价比优势一举占领了早市场。
印象深刻的是看了一个雷军创办小米前夕的访谈视频,雷军说 “大家看到苹果的成功,却没有看到这片蓝海的机会,现在手机制造领域竞争太不激烈了”。同时为了对抗苹果,谷歌开源了安卓源代码,小米利用这个机会打造一款符合中国人口味的手机操作系统,并借助用户社区与性价比优势一举占领了早市场。
2015-2018 年出现了 OV 领跑的情况,即 OPPO、VIVO 后来居上,有两点原因:小米还在强调各项参数指标,但 OV 宣传的概念很易懂 “充电五分钟,通话两小时”;同时 OV 还注意到了下沉市场,通过各种综艺节目冠名与 **平均 25 万家线下门店布局**,超越了小米。
@@ -96,7 +96,7 @@
切入点是 **融资**。BAT 上市融资额度分别是:百度:1.112 亿美元、**阿里巴巴 69.88 亿美元**、腾讯 0.2188 亿美元,总额 71.2 亿美元。**而滴滴到目前为止的融资已经达到 208 亿美元,** 滴滴融资超过 BAT 总和,这说明了什么?这说明滴滴走了一条不正常的商业路线,即先疯狂再冷静的烧钱路线。
当一个行业增长速度极速增加时,老玩家将失去优势和壁垒,所以谁能更快扩张谁就能成为最终赢家,此时如果有大量资本投入快速占领市场,让企业成为这个领域的绝对霸主,投资者就可以通过上市退出的方式把之前烧的赚回来。然而这种烧钱商业模式是有前提的,即 **极度充裕的资本 + 清晰的结构性机会**,滴滴的结构性机会非常清晰,先垄断再收割。
当一个行业增长速度极速增加时,老玩家将失去优势和壁垒,所以谁能更快扩张谁就能成为最终赢家,此时如果有大量资本投入快速占领市场,让企业成为这个领域的绝对霸主,投资者就可以通过上市退出的方式把之前烧的赚回来。然而这种烧钱商业模式是有前提的,即 **极度充裕的资本 + 清晰的结构性机会**,滴滴的结构性机会非常清晰,先垄断再收割。
> 传统商业模式:融资 -> 赚钱。
>
@@ -128,7 +128,7 @@ Uber 创始人 特拉维斯·卡兰尼克 说了一句很经典的话,翻译
如果资本不充裕了,对创业者来说也还有机会,比如相应的会带来低人力成本与低广告投放成本。
最后,周航宣传了一个创业孵化项目,即投资人与创业者深度交流几个月,在这几个月内让创业者得到成长,让投资人能看清创业者是否具备潜力,这种投资者与创业者培养感情的孵化方式是比较新颖的,相对面试来说,有更多机会呆在一起可以看人看得更清楚,投资者与创业者更容易简历信任关系。
最后,周航宣传了一个创业孵化项目,即投资人与创业者深度交流几个月,在这几个月内让创业者得到成长,让投资人能看清创业者是否具备潜力,这种投资者与创业者培养感情的孵化方式是比较新颖的,相对面试来说,有更多机会呆在一起可以看人看得更清楚,投资者与创业者更容易建立信任关系。
### 语言 AI 的未来构想
@@ -247,7 +247,7 @@ Uber 创始人 特拉维斯·卡兰尼克 说了一句很经典的话,翻译
1. 食材可见:比如大块杏仁碎、大块黄桃粒等。
2. 口味丰富:芝士、椰子、巧克力、曲奇。
以为朋友当场就订购了几箱,说实话还是蛮有诱惑力的,产品叫 ffit8,可以天猫自行搜索。
一位朋友当场就订购了几箱,说实话还是蛮有诱惑力的,产品叫 ffit8,可以天猫自行搜索。
极客大会每个人都送了几袋,尝了一下还是蛮好吃的,有甜味,但为什么说无糖呢,查了一下原因,原来用的是低聚异麦芽糖,这种麦芽糖难以被吸收,所以也就可以认为是无糖的啦。
@@ -93,7 +93,7 @@ VIPKID 起步是依靠朋友圈传播,但随着项目的起量,需要通过
一加手机做的是高端手机,操作系统主打的是简洁,不会有任何广告,盈利方式则是其较高的定价。而相比手机大厂,一加手机的突破点在于集中力量做旗舰手机,通过集中投入研发资源达到单点突破。
最近一加也在做电视了,目的是为了占领客厅市场,可能因为手机的比较火,资金链比较充裕所以做了更大的布局。
最近一加也在做电视了,目的是为了占领客厅市场,可能因为手机的比较火,资金链比较充裕所以做了更大的布局。
### 解题 - 社区零售新物种的进化之道
@@ -50,7 +50,7 @@
上面是最基本的写作技巧,我就不继续展开了,接下来要重点聊聊的是前端精读是怎么做分享的。我会从如何写作、如何坚持、如何形成正循环三个方面谈谈自己的感受。
首先是写作方式,前端精读的命题很明确,就是基于某个文章或者观点进行精读,因此每篇文章都有一个明确的主题。第二步是摘要,文章内容精简的表达出来,这可以锻炼你的总结能力,也让读者能了解到背景知识。第三步是精读,这一步需要你有一些私藏干货,毕竟把文章直接翻译一遍是没有任何价值的,我在精读自己不熟悉领域的文章时经常遇到这个问题,此时我一般会找几篇类似的文章结合阅读,并找到一些可以互补的观点,这样的精读可以让文章的观点更加饱满。最后是总结,总结时可以点题,将重要内容再梳理一遍,也可以进行延伸,指出更进一步的思考方向。
首先是写作方式,前端精读的命题很明确,就是基于某个文章或者观点进行精读,因此每篇文章都有一个明确的主题。第二步是摘要,文章内容精简的表达出来,这可以锻炼你的总结能力,也让读者能了解到背景知识。第三步是精读,这一步需要你有一些私藏干货,毕竟把文章直接翻译一遍是没有任何价值的,我在精读自己不熟悉领域的文章时经常遇到这个问题,此时我一般会找几篇类似的文章结合阅读,并找到一些可以互补的观点,这样的精读可以让文章的观点更加饱满。最后是总结,总结时可以点题,将重要内容再梳理一遍,也可以进行延伸,指出更进一步的思考方向。
为了让分享坚持下来,我在每周结束之前都会提前立好下周精读的 Flag,在 Github 开一个 issue,这样不仅可以提醒我周末的写作,还可以收获很多来自社区的讨论与反馈,让文章聚集了社区的智慧。这种提前立 Flag 的做法让我想到了自家小区物业费的收取方式,每年年初都会提前征收一整年的物业费,抛开商业手法不谈,这至少意味着物业对业务整整一年的承诺,这种承诺支撑了物业后续一整年的服务,也支撑了每周下一次的精读文章。
+108
View File
@@ -0,0 +1,108 @@
我站在前端角度梳理出数据领域技术专家应该学习的技术点,以下是能力要点:
- 同时具备产品、数据、技术能力。
- 以前端为切入点,深入各技术领域,包括前后端、数据技术。
- 打牢基础知识,熟悉领域知识。
对于知识,可以分为通用、领域的知识,其中越通用、越基础的知识越保值,比如数学知识的保质期可能持续到地球毁灭,而 Webpack5 的 API 可能保质期只有三个月。
所以在学习前,要对各类知识的保质期有个大概的了解:
- **通用基础知识**100 年以上。
- **行业基础知识**,如产品、计算机基础知识:30 年以上。
- **行业通用领域知识**10 年以内。
- **行业专用领域知识**1 年左右。
### 能力模型
- **通用基础知识**
- 数学
- 坐标系
- 参数方程
- 向量
- 点乘
- 叉乘
- 矩阵
- 矩阵乘法
- 线性变换
- 仿射变换
- 计算机 **行业基础知识**
- 数据结构
- 数据
- 链表
- 栈
- 堆
- 哈希表
- 树 & 二叉树 & 二叉搜索树
- 字典树
- 并查集
- 布隆过滤器
- 算法
- 位运算
- 滑动窗口
- 双指针
- 贪心
- 回溯
- 递归
- 分治
- 动态规划
- 编译原理
- 设计模式
- 产品
- **行业基础知识**
- 经济学
- 商学
- 心理学
- 数据
- **行业基础知识**
- 数据明细与聚合
- SQL
- **行业通用领域知识**
- 计算字段
- **行业专用领域知识**
- 数据分析表达式
- 前端
- **行业基础知识**
- 编程范式
- 命令式
- 过程式
- 面向对象
- 声明式
- 逻辑式
- 函数式
- 数据修改
- 可变数据
- 不可变数据
- 图形学
- **行业通用领域知识**
- javascript、typescript
- css
- dom、svg、canvas
- webgl
- **行业专用领域知识**
- 前端框架
- React
- Vue
- 数据流框架
- Redux
- Mobx
- 构建工具
- Webpack
- Snowpack
- 脚手架
- Vite
- 全栈框架
- Next.js
- 后端
- **行业基础知识**
- 架构模式
- 一主多从
- **行业专用领域知识**
- 后端框架
- Spring
### 前端精读与能力模型
前端精读不可能覆盖上述所有知识,有的知识是课本学的,有的知识是步入社会后,看书或者通过专门渠道学习的,所以本篇只对能力模型做一个指导,而学海无涯,前端精读即便持续创作一百年,也仍然挂一漏万。
不过这个能力模型会对前端精读选材起到主导作用,我会尽量挑选重要的,保质期长的基础知识解读,而保质期较短的行业专用领域知识则尽量少讲,希望前端精读能形成一个正金字塔,底座是厚厚的基础知识,金字塔尖是重要的行业知识,风沙会经常侵蚀金字塔尖,所以金字塔尖需要不断的更新迭代,但我相信,只要有一个稳定的底座,上层的修缮总不是难事。
@@ -0,0 +1,337 @@
很多人觉得动态规划很难,甚至认为面试出动态规划题目是在为难候选人,这可能产生一个错误潜意识:认为动态规划不需要掌握。
其实动态规划非常有必要掌握:
1. 非常锻炼思维。动态规划是非常锻炼脑力的题目,虽然有套路,但每道题解法思路差异很大,作为思维练习非常合适。
2. 非常实用。动态规划听起来很高级,但实际上思路和解决的问题都很常见。
动态规划用来解决一定条件下的最优解,比如:
- 自动寻路哪种走法最优?
- 背包装哪些物品空间利用率最大?
- 怎么用最少的硬币凑零钱?
其实这些问题乍一看都挺难的,毕竟都不是一眼能看出答案的问题。但得到最优解又非常重要,谁能忍受游戏中寻路算法绕路呢?谁不希望背包放的东西更多呢?所以我们一定要学好动态规划。
## 精读
动态规划不是魔法,它也是通过暴力方法尝试答案,只是方式更加 “聪明”,使得实际上时间复杂度并不高。
### 动态规划与暴力、回溯算法的区别
上面这句话也说明了,所有动态规划问题都能通过暴力方法解决!是的,所有最优解问题都可以通过暴力方法尝试(以及回溯算法),最终找出最优的那个。
暴力算法几乎可以解决一切问题。回溯算法的特点是,通过暴力尝试不同分支,最终选择结果最优的线路。
而动态规划也有分支概念,但不用把每条分支尝试到终点,而是在走到分叉路口时,可以直接根据前面各分支的表现,直接推导出下一步的最优解!然而无论是直接推导,还是前面各分支判断,都是有条件的。动态规划可解问题需同时满足以下三个特点:
1. 存在最优子结构。
2. 存在重复子问题。
3. 无后效性。
### 存在最优子结构
即子问题的最优解可以推导出全局最优解。
什么是子问题?比如寻路算法中,走完前几步就是相对于走完全程的子问题,必须保证走完全程的最短路径可以通过走完前几步推导出来,才可以用动态规划。
不要小看这第一条,动态规划就难在这里,你到底如何将最优子结构与全局最优解建立上关系?
- 对于爬楼梯问题,由于每层台阶都是由前面台阶爬上来的,因此必然存在一个线性关系推导。
- 如果变成二维平面寻路呢?那么就升级为二维问题,存在两个变量 `i,j` 与上一步之间关系了。
- 如果是背包问题,同时存在物品数量 `i`、物品重量 `j` 和物品质量 `k` 三个变量呢?那就升级为三位问题,需要寻找三个之间的关系。
依此类推,复杂度可以上升到 N 维,维度越高思考的复杂度就越高,空间复杂度就越需要优化。
### 存在重复子问题
即同一个子问题在不同场景下存在重复计算。
比如寻路算法中,同样两条路线的计算中,有一段路线是公共的,是计算的必经之路,那么只算一次就好了,当计算下一条路时,遇到这个子路,直接拿第一次计算的缓存即可。典型例子是斐波那契数列,对于 `f(3)``f(4)`,都要计算 `f(1)``f(2)`,因为 `f(3) = f(2) + f(1)`,而 `f(4) = f(3) + f(2) = f(2) + f(1) + f(2)`
这个是动态规划与暴力解法的关键区别,动态规划之所以性能高,是因为 **不会对重复子问题进行重复计算**,算法上一般通过缓存计算结果或者自底向上迭代的方式解决,但核心是这个场景要存在重复子问题。
当你觉得暴力解法可能很傻,存在大量重复计算时,就要想想是哪里存在重复子问题,是否可以用动态规划解决了。
### 无后效性
即前面的选择不会影响后面的游戏规则。
寻路算法中,不会因为前面走了 B 路线而对后面路线产生影响。斐波那契数列因为第 N 项与前面的项是确定关联,没有选择一说,所以也不存在后效性问题。
什么场景存在后效性呢?比如你的人生是否能通过动态规划求最优解?其实是不行的,因为你今天的选择可能影响未来人生轨迹,比如你选择了计算机这个职业,会直接影响到工作的领域,接触到的人,后面的人生路线因此就完全变了,所以根本无法与选择了土木工程的你进行比较,因为人生赛道都变了。
有同学可能觉得这样局限是不是很大?其实不然,无后效性的问题仍然很多,比如背包放哪件物品、当前走哪条路线、用了哪些零钱,都不会影响整个背包大小、整张地图的地形、以及你最重要付款的金额。
### 解法套路 - 状态转移方程
解决动态规划问题的核心就是写出状态转移方程,所谓状态转移,即通过某些之前步骤推导出未来步骤。
状态转移方程一般写为 `dp(i) = 一系列 dp(j) 的计算`,其中 `j < i`
其中 `i``dp(i)` 的含义很重要,一般 `dp(i)` 直接代表题目的答案,`i` 就有技巧了。比如斐波那契数列,`dp(i)` 表示的答案就是最终结果,`i` 表示下标,由于斐波那契数列直接把状态转移方程告诉你了 `f(x) = f(x-1) + f(x-2)`,那么根本连推导都不必了。
**对于复杂问题,难在如何定义 `i` 的含义,以及下一步状态如何通过之前状态推导。** 这个做多了题目就有体会,如果没有,那即便再如何解释也难以说明,所以后面还是直接看例子吧。
先举一个最简单的动态规划例子 - 爬楼梯来说明问题。
### 爬楼梯问题
爬楼梯是一道简单题,题目如下:
> 假设你正在爬楼梯。需要 `n` 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?(给定 `n` 是一个正整数)
首先 `dp(i)` 就是问题的答案(解法套路,`dp(i)` 大部分情况就是答案,这样解题思路会最简化),即爬到第 `i` 阶台阶的方法数量,那么 `i` 自然就是要爬到第几阶台阶。
我们首先看是否存在 **最优子结构**?因为只能往上爬,所以第 `i` 阶台阶有几种爬方完全取决于前面有几种爬方,**而一次只能爬 1 或 2 个台阶,所以第 `i` 阶台阶只可能从第 `i-1``i-2` 个台阶爬上来的**,所以第 `i` 个台阶的爬法就是 `i-1``i-2` 总爬法之和。所以显然有最优子结构,连状态转移方程都呼之欲出了。
再看是否存在 **存在重复子问题**,其实爬楼梯和斐波那契数列类似,最终的状态转移方程是一样的,所以显然存在重复子问题。当然直观来看也容易分析出,10 阶台阶的爬法包含了 8、9 阶的爬法,而 9 阶台阶爬法包含了 8 阶的,所以存在重复子问题。
最后看是否 **无后效性**?由于前面选择一次爬 1 个或 2 个台阶并不会影响总台阶数,也不会影响你下一次能爬的台阶数,所以无后效性。如果你爬了 2 个台阶,因为太累,下次只能爬 1 个台阶,就属于有后效性了。或者只要你一共爬了 3 次 2 阶,就会因为太累而放弃爬楼梯,直接下楼休息,那么问题提前结束,也属于有后效性。
所以爬楼梯的状态转移方程为:
- `dp(i) = dp(i-1) + dp(i-2)`
- `dp(1) = 1`
- `dp(2) = 2`
注意,因为 1、2 阶台阶无法应用通用状态转移方程,所以要特殊枚举。这种枚举思路在代码里其实就是 **递归终结条件**,也就是作为函数 `dp(i)` 不能无限递归,当 `i` 取值为 1 或 2 时直接返回枚举结果(对这道题而言)。所以在写递归时,一定要优先写上递归终结条件。
然后我们考虑,对于第一阶台阶,只有一种爬法,这个没有争议吧。对于第二阶台阶,可以直接两步跨上来,也可以走两个一步,所以有两种爬法,也很容易理解,到这里此题得解。
关于代码部分,仅这道题写一下,后面的题目如无特殊原因就不写代码了:
```typescript
function dp(i: number) {
switch (i) {
case 1:
return 1;
case 2:
return 2;
default:
return dp(i - 1) + dp(i - 2);
}
}
return dp(n);
```
当然这样写重复计算了子结构,所以我们不要每次傻傻的执行 `dp(i - 1)`(因为这样计算了超多重复子问题),我们需要用缓存兜底:
```typescript
const cache: number[] = [];
function dp(i: number) {
switch (i) {
case 1:
cache[i] = 1;
break;
case 2:
cache[i] = 2;
break;
default:
cache[i] = cache[i - 1] + cache[i - 2];
}
return cache[i];
}
// 既然用了缓存,最好子底向上递归,这样前面的缓存才能优先算出来
for (let i = 1; i <= n; i++) {
dp(i);
}
return cache[n];
```
当然这只是简单的一维线性缓存,更高级的缓存模式还有 **滚动缓存**。我们观察发现,这道题缓存空间开销是 `O(n)`,但每次缓存只用了上两次的值,所以计算到 `dp(4)` 时,`cache[1]` 就可以扔掉了,或者说,我们可以滚动利用缓存,让 `cache[3]` 占用 `cache[1]` 的空间,那么整体空间复杂度可以降低到 `O(1)`,具体做法是:
```typescript
const cache: [number, number] = [];
function dp(i: number) {
switch (i) {
case 1:
cache[i % 2] = 1;
break;
case 2:
cache[i % 2] = 2;
break;
default:
cache[i % 2] = cache[(i - 1) % 2] + cache[(i - 2) % 2];
}
return cache[i % 2];
}
for (let i = 1; i <= n; i++) {
dp(i);
}
return cache[n % 2];
```
通过取余,巧妙的让缓存永远交替占用 `cache[0]``cache[1]`,达到空间利用最大化。当然,这道题因为状态转移方程是连续用了前两个,所以可以这么优化,如果遇到用到之前所有缓存的状态转移方程,就无法使用滚动缓存方案了。然而还有更高级的多维缓存,这个后面提到的时候再说。
接下来看一个进阶题目,最大子序和。
### 最大子序和
最大子序和是一道简单题,题目如下:
> 给定一个整数数组 `nums` ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
首先按照爬楼梯的套路,`dp(i)` 就表示最大和,由于整数数组可能存在负数,所以越多数相加,和不一定越大。
接着看 `i`,对于数组问题,大部分 `i` 都可以代表以第 `i` 位结尾的字符串,那么 `dp(i)` 就表示以第 `i` 位结尾的字符串的最大和。
可能你觉得以 `i` 结尾,就只能是 `[0-i]` 范围的值,那么 `[j-i]` 范围的字符串不就被忽略了?其实不然,`[j-i]` 如果是最大和,也会被包含在 `dp(i)` 里,因为我们状态转移方程可以选择不连上 `dp(i-1)`
现在开始解题:首先题目是最大和的连续子数组,一般连续的都比较简单,因为对于 `dp(i)`,要么和前面连上,要么和前面断掉,所以状态转移方程为:
- `dp(i) = dp(i-1) + nums[i]` 如果 `dp(i-1) > 0`
- `dp(i) = nums[i]` 如果 `dp(i-1) <= 0`
怎么理解呢?就是第 `i` 个状态可以直接由第 `i-1` 个状态推导出来,既然 `dp(i)` 是指以第 `i` 个字符串结尾的最大和,那么 `dp(i-1)` 就是以第 `i-1` 个字符串结尾的最大和,而且此时 `dp(i-1)` 已经算出来了,那么 `dp(i)` 怎么推导就清楚了:
因为字符串是连续的,所以 `dp(i)` 要么是 `dp(i-1)` + `nums[i]`,要么就直接是 `nums[i]`,所以选择哪种,取决于前面的 `dp(i-1)` 是否是正数,**因为以 `i` 结尾一定包含 `nums[i]`,所以 `nums[i]` 不管是正还是负,都一定要带上。** 所以容易得知,`dp(i-1)` 如果是正数就连起来,否则就不连。
好了,经过这么详细的解释,相信你已经完全了解动态规划的解题套路,后面的题目解释方式我就不会这么啰嗦了!
这道题如果再复杂一点,不连续怎么办呢?让我们看看最长递增子序列问题吧。
### 最长递增子序列
最长递增子序列是一道中等题,题目如下:
> 给你一个整数数组 `nums` ,找到其中最长严格递增子序列的长度。
>
> 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,`[3,6,2,7]` 是数组 `[0,3,1,6,2,2,7]` 的子序列。
其实之前的 [精读《DOM diff 最长上升子序列》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/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) 有详细解析过这道题,包括还有更优的贪心解法,不过我们这次还是聚焦在动态规划方法上。
这道题与上一道的区别就是,首先递增,其次不连续。
按照套路,`dp(i)` 就表示以第 `i` 个字符串结尾的最长上升子序列长度,那么重点是,`dp(i)` 怎么通过之前的推导出来呢?
由于是不连续的,因此不能只看 `dp(i-1)` 了,因为 `nums[i]` 项与 `dp(j)`(其中 `0 <= j < i`)组合后都可能达到最大长度,因此需要遍历所有 `j`,尝试其中最大长度的组合。
所以状态转移方程为:
`dp[i] = max(dp[j]) + 1`,其中 `0<=j<i``num[j]<num[i]`
这道题的出现,预示着较为复杂的状态转移方程的出现,即第 `i` 项不是简单由 `i-1` 推导,而是由之前所有 `dp(j)` 推导,其中 `0<=j<i`
除此之外,还有推导变种,即根据 `dp(dp(i))` 推导,即函数里套函数,这类问题由于加深了一层思考脑回路,所以相对更难。我们看一道这样的题目:最长有效括号。
### 最长有效括号
最长有效括号是道困难题,题目如下:
> 给你一个只包含 `'('``')'` 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
这道题之所以是困难题,就因为状态转移方程存在嵌套思维。
我们首先按套路定义 `dp(i)` 为答案,即以第 `i` 下标结尾的字符串中最长有效括号长度。看出来了吗?一般字符串题目中,`i` 都是以字符串下标结尾来定义,很少有定义为开头或者别的定义行为。当然非字符串问题就不是这样了,这个在后面再说。
我们继续题目,如果 `s[i]``(`,那么不可能组成有效括号,因为最右边一定不闭合,所以考虑 `s[i]``)` 的场景。
如果 `s[i-1]``(`,那么构成了 `...()` 之势,最后两个自成合法闭合,所以只要看前面的即可,即 `dp(i-2)`,所以这种场景的状态转移方程为:
`dp(i) = dp(i-2) + 2`
如果 `s[i-1]``)` 呢?构成了 `...))` 的状态,那么只有 `i-1` 是合法闭合的,且这个合法闭合段之前必须是 `(` 与第 `i` 项形成闭合,才构成此时最长有效括号长度,所以这种场景的状态转移方程为:
`dp(i) = dp(i-1) + dp(i - dp(i-1) - 2) + 2`,你可以结合下面的图来理解:
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN016tRvXm1o4p8U1Plfk_!!6000000005172-2-tps-1088-378.png">
可以看到,`dp(i-1)` 就是第二条横线的长度,然后如果红色括号匹配的话,长度又 +2,最后别忘了最左边如果有满足匹配的也要带上,这就是 `dp(i - dp(i-1) - 2)`,所以加到一起就是这种场景的括号最大长度。
到这里,一维动态规划问题深度基本上探索完了,在进入多维动态规划问题前,还有一类一维动态规划问题,属于表达式不难,也没有这题这么复杂的嵌套 DP,但是思维复杂度极高,**你一定不要盯着全流程看,那样复杂度太高,你需要充分认可 dp(i-x) 已经算出来部分的含义,进行高度抽象的思考。**
### 栅栏涂色
栅栏涂色是一道困难题,题目如下:
> 有 `k` 种颜色的涂料和一个包含 `n` 个栅栏柱的栅栏,每个栅栏柱可以用其中一种颜色进行上色。
>
> 你需要给所有栅栏柱上色,并且保证其中相邻的栅栏柱 **最多连续两个** 颜色相同。然后,返回所有有效涂色的方案数。
这道题 `k``n` 都非常巨大,常规暴力解法甚至普通 DP 都会超时。选择 `i` 的含义也很重要,这里 `i` 到底代表用几种颜色还是几个栅栏呢?选择栅栏会好做一些,因为栅栏是上色的主体。这样 `dp(i)` 就表示上色前 `i` 个栅栏的所有涂色方案。
首先看下递归终止条件。由于最多连续两个颜色相同,因此 `dp(0)``dp(1)` 分别是 `k``k*k`,因为每个栅栏随便刷颜色,自由组合。那么 `dp(2)` 有三个栅栏,非法情况是三个栅栏全同色,所以用所有可能减掉非法即可,非法场景只有 `k` 中,所以结果是 `k*k*k - k`
那么考虑一般情况,对于 `dp(i)` 有几种涂色方案呢?直接思考情况太多,我们把情况一分为二,考虑 `i``i-1` 颜色相同与不同两种情况考虑。
如果 `i``i-1` 颜色相同,那么为了合法,`i-1` 肯定不能与 `i-2` 颜色相同了,否则就三个同色,这样的话,不管 `i-2` 是什么颜色,`i-1``i` 都只能少取一种颜色,少取的颜色就是 `i-2` 的颜色,因此 `[i-1,i]` 这个区间有 `k-1` 中取色方案,前面有 `dp(i-2)` 种取色方案,相乘就是最终方案数:`dp(i-2) * (k-1)`
**这背后其实存在动态思维,即每种场景的 `k-1` 都是不同的颜色组合,只是无论前面 `dp(i-2)` 是何种组合,后面两个栅栏一定有 `k-1` 种取法,虽然颜色组合的色值不同,但颜色组合数量是不变的,所以可以统一计算。理解这一点非常关键。**
如果 `i``i-1` 颜色不同,那么第 `i` 项只有 `k-1` 种取法,一样也是动态的,因为永远不能和 `i-1` 颜色相同。最后乘上 `dp(i-1)` 的取色方案,就是总方案数:`dp(i-1) * (k-1)`
所以最后总方案数就是两者之和,即 `dp(i) = dp(i-2) * (k-1) + dp(i-1) * (k-1)`
这道题的不同之处在于,变化太多,任何一个栅栏取的颜色都会影响后面栅栏要取的颜色,**乍一看觉得是个有后效性的题目,无法用动态规划解决**。但实际上,虽然有后效性,但如果进行合理的拆解,后面栅栏的总可能性 `k-1` 是不变的,**所以考虑总可能性数量,是无后效性的**,因此站在方案总数上进行抽象思考,才可能破解此题。
接下来介绍多维动态规划,从二维开始。二维动态规划就是用两个变量表示 DP,即 `dp(i,j)`,一般在二维数组场景出现较多,当然也有一些两个数组之间的关系,也属于二维动态规划,为了继续探讨字符串问题,我选择了字符串问题的二维动态规划范例,编辑距离这道题来说明。
### 编辑距离
编辑距离是一道困难题,题目如下:
> 给你两个单词 `word1``word2`,请你计算出将 `word1` 转换成 `word2` 所使用的最少操作数。
>
> 你可以对一个单词进行如下三种操作:
>
> - 插入一个字符
> - 删除一个字符
> - 替换一个字符
只要是字符串问题,基本上 `i` 都表示以第 `i` 项结尾的字符串,但这道题有两个单词字符串,**为了考虑任意匹配场景,必须用两个变量表示,即 `i` `j` 分别表示 `word1``word2` 结尾下标时,最少操作次数。**
那么对于 `dp(i,j)` 考虑 `word1[i]``word2[j]` 是否相同,最后通过双重递归,先递归 `i`,在递归内再递归 `j`,答案就出来了。
假设最后一个字符相同,即 `word1[i] === word2[j]` 时,**由于最后一个字符不用改就相同了,所以操作次数就等价于考虑到前一个字符**,即 `dp(i,j) = dp(i-1,j-1)`
假设最后一个字符不同,那么 **最后一步** 有三种模式可以得到:
1. 假设是替换,即 `dp(i,j) = dp(i-1,j-1) + 1`,因为替换最后一个字符只要一步,并且和前面字符没什么关系,所以前面的最小操作次数直接加过来。
2. 假设是插入,即 `word1` 插入一个字符变成 `word2`,那么只要变换到这一步再 +1 插入操作就行了,变换到这一步由于插入一个就行了,因此 `word1``word2` 少一个单词,其它都一样,要变换到这一步,就要进行 `dp(i,j-1)` 的变换,因此 `dp(i,j) = dp(i,j-1) + 1`。。
3. 假设是删除,即 `word1` 删除一个字符变成 `word2`,同理,要进行 `dp(i-1,j)` 的变化后多一步删除,因此 `dp(i,j) = dp(i-1,j) + 1`
由于题目取操作最少次数,所以这三种情况取最小即可,即 `dp(i,j) = min(dp(i-1,j-1), dp(i,j-1), dp(i-1,j)) + 1`
所以同时考虑了最后一个字符是否相同后,合并了的状态转移方程就是最终答案。
我们再考虑终止条件,即 `i``j` 为 -1 时的情况,因为状态转移方程 `i``j` 不断减小,肯定会减少到 0 或 -1,因为 0 是字符串还有一个字符,相对比如考虑 -1 字符串为空时方便,因此我们考虑 -1 时作为边界条件。
`i` 为 -1 时,即 `word1` 为空,此时要变换为 `word2` 很显然,只有插入 `j` 次是最小操作次数,因此此时 `dp(i,j) = j`;同理,当 `j` 为 -1 时,即 `word2` 为空,此时要删除 `i` 次,因此操作次数为 `i`,所以 `dp(i,j) = i`
### 非字符串问题
说到这,相信你在字符串动规问题上已经如鱼得水了,我们再看看非字符串场景的动规问题。非字符串场景的动规比较经典的有三个,第一是矩形路径最小距离,或者最大收益;第二是背包问题以及变种;第三是打家劫舍问题。
这些问题解决方式都一样,只是对于 `dp(i)` 的定义略有区别,比如对于矩形问题来说,`dp(i,j)` 表示走到 `i,j` 格子时的最小路径;对于背包问题,`dp(i,j)` 表示装了第 `i` 个物品时,背包还剩 `j` 空间时最大价格;对于打家劫舍问题,`dp(i)` 表示打劫到第 `i` 个房间时最大收益。
因为篇幅问题这里就不一详细介绍了,只简单说明一下矩形问题于打家劫舍问题。
对于矩形问题,状态转移方程重点看上个状态是如何转移过来的,一般矩形只能向右或者向下移动,路途可能有一些障碍物不能走,我们要做分支判断,然后选择一条符合题目最值要求的路线作为当前 `dp(i)` 的转移方程即可。
对于打家劫舍问题,由于不能同时打劫相邻的房屋,所以对于 `dp(i)`,要么为了打劫 `i-1` 而不打劫第 `i` 间,或者打劫 `i-2` 于第 `i` 间,取这两种终态的收益最大值即可,即 `dp(i) = max(dp(i-1), dp(i-2) + coins[i])`
## 总结
动态规划的核心分为三步,首先定义清楚状态,即 `dp(i)` 是什么;然后定义状态转移方程,这一步需要一些思考技巧;最后思考验证一下正确性,即尝试证明你写的状态转移方程是正确的,在这个过程要做到状态转移的不重不漏,所有情况都被涵盖了进来。
动态规划最经典的还是背包问题,由于篇幅原因,可能下次单独出一篇文章介绍。
> 讨论地址是:[精读《算法 - 动态规划》· Issue #327 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/327)
**如果你想参与讨论,请 [点击这里](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,235 @@
滑动窗口算法是较为入门题目的算法,一般是一些有规律数组问题的最优解,也就是说,如果一个数组问题可以用动态规划解,但又可以使用滑动窗口解决,那么往往滑动窗口的效率更高。
双指针也并不局限在数组问题,像链表场景的 “快慢指针” 也属于双指针的场景,其快慢指针滑动过程中本身就会产生一个窗口,比如当窗口收缩到某种程度,可以得到一些结论。
因此掌握滑动窗口非常基础且重要,接下来按照我的经验给大家介绍这个算法。
## 精读
滑动窗口使用双指针解决问题,所以一般也叫双指针算法,因为两个指针间形成一个窗口。
什么情况适合用双指针呢?一般双指针是暴力算法的优化版,所以:
1. 如果题目较为简单,且是数组或链表问题,往往可以尝试双指针是否可解。
2. 如果数组存在规律,可以尝试双指针。
3. 如果链表问题限制较多,比如要求 O(1) 空间复杂度解决,也许只有双指针可解。
也就是说,当一个问题比较有规律,或者较为简单,或较为巧妙时,可以尝试双指针(滑动窗口)解法。
我们还是拿例子说明,首先是两数之和。
### 两数之和
两数之和是一道简单题,实际上和滑动窗口没什么关系,但为了引出三数之和,还是先讲这道题。题目如下:
> 给定一个整数数组 `nums` 和一个整数目标值 `target`,请你在该数组中找出 **和为目标值** `target`  的那 **两个** 整数,并返回它们的数组下标。
>
> 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
暴力解法就是穷举所有两数之和,发现和为 `target` 结束,显然这种做法有点慢,我们换一种思路。
由于可以用空间换时间,又只有两个数,我们可以对题目进行转化,即通过一次遍历,将 `nums` 每一项都减去 `target`,然后找到后面任意一项值为前面的结果,即表示它们和为 `target`
可以用哈希表 `map` 加速查询,即将每一项 `target - num` 作为 key,如果后面任何一个 `num` 作为 key 可以在 `map` 中找到,则得解,且上一个数的原始值可以存在 `map` 的 value 中。这要仅需遍历一次,时间复杂度为 O(n)。
之所以说这道题,是因为这道题是单指针,即只有一个指针在数组中移动,并配合哈希表快速求解。对于稍微复杂的问题,单指针就不够了,需要用双指针解决(一般来说不会用到三或以上指针),那复杂点的题目就是三数之和了。
### 三数之和
三数之和是一道中等题,别以为只是两数之和的加强版,其思路完全不同。题目如下:
> 给你一个包含 `n` 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 `a``b``c` ,使得 `a + b + c = 0` ?请你找出所有和为 `0` 且不重复的三元组。
由于超过了两个数,所以不能像双指针一样求解了,因为即便用了哈希表存储,也会在遍历时遇到 “两数之和” 的问题,而哈希表方案无法继续嵌套使用,即无法进一步降低复杂度。
为了降低时间复杂度,我们希望只遍历一次数组,这就需要数组满足一定条件我们才能用滑动窗口,所以我们对数组进行排序,使用快排的时间复杂度为 O(nlogn),时间复杂度已超出两数之和,不过因为题目复杂,这个牺牲是无法避免的。
假设从小到大排序,那我们就拿到一个递增数组了,此时经典滑动窗口方法就可用了!怎么滑动呢?首先创建两个指针,分别叫 `left``right`,通过不断修改 `left``right`,让它们在数组间滑动,这个窗口大小就是符合题目要求的,当滑动完毕时,返回所有满足条件的窗口即可,记录其实很简单,只要在滑动过程中记录一下就行。
首先排除异常值,即数组长度过小,然后对于常规情况,我们拿一个全局变量存储当前窗口数的和,这样 `right + 1` 只要累加 `nums[right+1]``left + 1` 只要减去 `nums[left]` 即可快速拿到求和。
由于需要考虑所有情况,所以需要一次数组遍历,对于每次遍历的起始点 `i`,如果 `nums[i] > 0` 则直接跳过,因为数组排序后是递增的,后面的和只会永远大于 0;否则进行窗口滑动,先形成三个点 `[i, i+1, n-1]`,这样保持 `i` 不动,不断包夹后两个数字即可,只要它们的和大于 0,就将第三个点左移(数字会变小),否则将第二个点右移(数字会变大),其实第二个和第三个数就是滑动窗口。
这样的话时间复杂度是 O(n²),因为存在两次遍历,忽略快排较小的时间复杂度。
那么四数之和,五数之和呢?
### 四数之和
该题和三数之和完全一样,除了要求变成四个数。
首先还是排序,然后双重递归,即确定前两个数不变,不断包夹后两个数,后两个数就是 `i+1``n-1`,算法和三数之和一样,所以最终时间复杂度为 O(n³)。
那么 N 数之和(N > 2)都可以采用这个思路解决。
为什么没有更优的方法呢?我想可能因为:
1. 无论几数之和,快排一次时间复杂度都是固定的,所以沿用三数之和的方案其实占了排序算法便宜。
2. 滑动窗口只能用两个指针进行移动,而没有三指针但又保持时间复杂度不变的窗口滑动算法存在。
所以对于 N 数之和,通过排序付出了 O(nlogn) 时间复杂度之后,可以用滑动窗口,将 2 个数时间复杂度优化为 O(n),所以整体时间复杂度就是 O(N - 2 + 1 个 n),即 O(N-1 个 n),而最小的时间复杂度 O(n²) 比 O(nlogn) 大,所以总是忽略快排的时间复杂度,所以三数之和时间复杂度是 O(n²),四数之和时间复杂度为 O(n³),依此类推。
可以看到,我们从最简单的两数之和,到三数之和、四数之和,跨入了滑动窗口的门槛,**本质上是利用排序后数组有序的特性,让我们在不用遍历数组的前提下,可以对窗口进行滑动**,这是滑动窗口算法的核心思想。
为了加强这个理解,再看一道类似的题目,无重复字符的最长子串。
### 无重复字符的最长子串
无重复字符的最长子串是一道中等题,题目如下:
> 给定一个字符串,请你找出其中不含有重复字符的 **最长子串** 的长度。
由于最长子串是连续的,所以显然可以考虑滑动窗口解法。其实确定了滑动窗口解法后,问题很简单,只要设定 `left``right`,并用一个哈希 Set 记录哪些元素存在过,在过程中记录最大长度,并尝试 `right` 右移,如果右移过程中发现出现重复字符,则 `left` 右移,直到消除这个重复字符为止。
解法并不难,但问题是,我们要想清楚,为什么用滑动窗口遍历一次就可以做到 **不重不漏**?即这道题时间复杂度只有 O(n) 呢?
只要想明白两个问题:
1. 由于子串是连续的,既然不存在跳跃的情况,只要一次滑动窗口内能包含所有解,就涵盖了所有情况。
2. 一次滑动窗口内不包含什么?由于我们只将 `right` 右移,且出现重复后尝试将 `left` 右移到不重复后,`right` 再继续右移,这忽略了出现重复后, `right` 左移的情况。
我们重点看二个问题,显然,如果 `abcd` 这四个连续的字符不重复,那么 `left` 右移后,`bcd` 也显然不重复,所以如果此时就可以将 `right` 右移形成 `bcda` 的窗口继续找下去,而不需要尝试 `bc` 这种情况,因为这种情况虽然不重复,但一定不是最优解。
好了,通过这个例子我们看到,滑动窗口如何缩小窗口范围其实不难,但更要注重的是,背后对于为什么可以用滑动窗口的思考,滑动窗口有没有做到不重不漏,如果没有想清楚,可能整个思路都错了。
那么滑动窗口的应用已经说透了?其实没有,我们上面只说了缩小窗口这种比较单一的脑回路,其实双指针构成的滑动窗口不一定都是那么正常滑的,一种有意思的场景是快慢指针,即是以相对速度决定窗口如何滑动。
关于快慢指针,经典的题目有环形链表、删除有序数组中的重复项。
### 环形链表
环形链表是一道简单题,题目如下:
> 给定一个链表,判断链表中是否有环。
如果不是进阶要求空间复杂度 O(1),我们可以在遍历时稍稍 “污染” 一下原始链表,这样总能发现是否走了回头路。
但要求空间开销必须是常数,我们不得不考虑快慢指针。说实话第一次看到这道题时,如果能想到快慢指针的解法,绝对是相当聪明的,因为必须要有知识迁移的能力。怎么迁移呢?想象学校在开运动会,相信每次都有一个跑的最慢的同学,慢到被最快的同学追了一圈。
等等,操场不就是环形链表吗?**只要有人跑得慢,就会被跑得快的追上,追上不就是相遇了吗?** 所以快慢指针分别跑,只要相遇则判定为环形链表,否则不是环形链表,且一定有一个指针先走完。
那么细枝末节就是优化效率了,慢指针到底慢多少呢?
有人会说,运动会上,跑步慢的人如果想被快的人追上,最好就不要跑。对,但环形链表问题中,链表不是操场,可能只有某一段是环,也就是跑步慢的人至少要跑到环里,才可能与跑得快人的相遇,但跑得慢的人又不知道哪里开始成环,这就是难点。
你有没有想过,为什么快排用二分法,而不是三分法?为什么每次中间来一刀,可以最快排完?原因是二分可以用最小的 “深度” 将数组切割为最小粒度。那么同理,快慢指针中,慢指针要想被尽快追上,速度可能最好是快指针的一半。那从逻辑上分析,为什么呢?
直观来看,如果慢指针太慢,可能大部分时间都在进入环形之前的位置转悠,快指针虽然快,但永远在环里跑,所以总是无法遇到慢指针,这给我们的启示是,慢指针不能太慢;如果慢指针太快,几乎速度和快指针一样,就像两个运动员都互不相让的争夺第一一样,他们真的想相遇,估计得连续跑几个小时吧,所以慢指针也不能过快。所以这样分析下来,慢指针只能取折中的一半速度。
但用一半的慢速真的能最快相遇吗?不一定,举一个例子,假设链表是完美环形,一共有 [1,6] 共 6 个节点,那么慢指针一次走 1 步,快指针一次走 2 步,那么一共是 `2,3 3,5 4,1 5,3 6,5 1,1` 共走 6 步,但如果快指针一次走 3 步呢?一共是 `2,4 3,1 4,4` 3 步。这么说一般速度不一定最优?其实不是的,计算机在链表寻址时,节点访问的消耗也要考虑进去,后者虽然看上去更快,但其实访问链表 `next` 的次数更多,对计算机来说,还不如第一种来得快。
所以准确来说,不是快指针比慢指针快一倍速度,而是慢指针一次走一步,快指针一次走两步最优,因为相遇时,总移动步数最少。
再说一个简单问题,即用快慢指针判断链表中倒数第k个节点或者链表中点。
### 判断链表中点
快指针是慢指针速度 2 倍,当快指针到达尾部,慢指针的位置就是链表中点。
### 链表中倒数第k个节点
链表中倒数第k个节点是一道简单题,题目如下:
> 输入一个链表,输出该链表中倒数第 `k` 个节点。为了符合大多数人的习惯,本题从 `1` 开始计数,即链表的尾节点是倒数第 `1` 个节点。
这道题就是判断链表中点的变种,只要让慢指针比快指针慢 `k` 个节点,当快指针到达末尾时,慢指针就指向倒数第 `k+1` 个节点了。这道题注意一下数数别数错了即可。
接下来终于说道快慢指针的另一种经典用法题型,删除有序数组中的重复项了。
### 删除有序数组中的重复项
删除有序数组中的重复项是一道简单题,题目如下:
> 给你一个有序数组 `nums` ,请你 **原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
这道题,要原地删除重复元素,并返回长度,所以只能用快慢指针。但怎么用呢?快多少慢多少?
其实这道题快多少慢多少并不像前面题目一样预设好了,而是根据遇到的实际数字来判断。
我们假设慢指针是 `slow` 快指针是 `fast`,注意变量命名也有意思,同样是双指针问题,有的是 `slow right`,有的是 `slow fast`,重点在于用何种方法移动指针。
我们只要让 `fast` 扫描完全表,把所有不重复的挪到一起就好了,这样时间复杂度是 O(n),具体做法是:
1. 让 `slow``fast` 初始都指向 index 0。
2. 由于是 **有序数组**,所以就算有重复也一定连在一起,所以可以让 `fast` 直接往后扫描,只有遇到和 `slow` 不同的值,才把其和 `slow+1` 交换,然后 `slow` 自增,继续递归,直到 `fast` 走到数组尾部结束。
做完这套操作后,`slow` 的下标值就是答案。
可以看到,这道题对于慢指针要如何慢,其实是根据值来判断的,如果 `fast` 的值与 `slow` 一样,那么 `slow` 就一直等着,因为相同的值要被忽略掉,让 `fast` 走就是在跳过重复值。
说完了常见的双指针用法,我们再来看一些比较难啃的特殊问题,这里主要讲两个,分别是 **盛最多水的容器****接雨水**
### 盛最多水的容器
盛最多水的容器是一道中等题,题目如下:
> 给你 `n` 个非负整数 `a1a2...an`,每个数代表坐标中的一个点 `(i, ai)` 。在坐标内画 `n` 条垂直线,垂直线 `i` 的两个端点分别为 `(i, ai)``(i, 0)` 。找出其中的两条线,使得它们与 `x` 轴共同构成的容器可以容纳最多的水。
<img width=400 src="https://z3.ax1x.com/2021/06/12/25WZZt.png">
建议先仔细读一读题目再继续,这道题相对比较复杂。
好了,为什么说这是一道双指针题目呢?因为我们看怎么计算容纳水的体积?其实这道题就简化为长乘宽。
长度就是选取的两个柱子的间距,宽就是其中最短柱子的高度。问题就是,虽然柱子间距越远,长度越大,但宽度不一定最大,一眼是没法看出来最优解的。
所以还是得多次尝试,那怎么样可以用最少的尝试次数,但又不重不漏呢?定义 `left` `right` 两个指针,分别指向 `0``n-1` 即首尾两个位置,此时长度是最大的(柱子间距离是最远的),接下来尝试一下别的柱子,试哪个呢?
- 较长的那个?如果新的比较短的更短,那么宽度更短了;如果新的比较短的更长,也没用,因为较短的决定了水位。
- 较短的那个?如果新的较长,那么才有机会整体体积更大。
所以我们移动较短的那个,并每次计算一下体积,最后当两根柱子相遇时结束,过程中最大体积就是全局最大体积。
这道题双指针的移动规则比较巧妙,与上面普通题目不一样,重点不是在是否会运用滑动窗口算法,而是能否找到移动指针的规则。
当然你可能会说,为什么两个指针要定义在最两端,而非别的地方?因为这样就无法控制变量了。
如果指针选在中间位置,那么指针外移时,柱子的间距与柱子长度同时变化,就很难找到一条完美路线。比如我们移动较短的柱子,是因为较短的柱子确定了最低水位,改变它,可能让最低水位变高,但问题是两根柱子的间距也在变大,这样移动较短还是较长的柱子哪个更优就说不准了。
说实话这种方法不太容易想到,需要多找几种选择尝试才能发现。当然,算法如果按照固定套路就能推导出来,也就没有难度了,所以要接受这种思维跳跃。
接下来我们看一道更特殊的滑动窗口问题,接雨水,它甚至分为多段滑动窗口。
### 接雨水
接雨水是一道困难题,题目如下:
> 给定 `n` 个非负整数表示每个宽度为 `1` 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
<img width=400 src="https://z3.ax1x.com/2021/06/12/25OejP.png">
与盛雨水不同,这道接雨水看的是整体,我们要算出能接的所有水的数量。
其实相比上一道题,这道题还算比较好切入,因为我们从左到右计算即可。思考发现,只有产生了 “凹槽” 才能接到雨水,而凹槽由它两边最高的柱子决定,那什么范围算一段凹槽呢?
显然凹槽是可以明确分组的,一个凹槽也无法被分割为多个凹槽,就像你看水坑一样,无论有多少,多深的坑在一起,总能一个一个数清楚,所以我们就从左到右开始数。
怎么数凹槽呢?用滑动窗口办法,每个窗口就是一个凹槽,那么窗口的起点 `left` 就是左边第一根柱子,有以下情况:
- 如果直接相邻的右边柱子更高(或一样高),那从它开始向右看,根本无法接雨水,所以直接抛弃,`left++`
- 如果直接相邻的右边柱子更矮,那就有产生凹槽的机会。
- 那么继续往右看,如果右边一直都更矮,那也接不到雨水。
- 如果右边出现一个高一些的,就可以接到雨水,那问题是怎么算能接多少,以及找到哪结束呢?
- 只要记录最左边柱子高度,右边柱子的结束判断条件是 “遇到一个与最左边一样高的柱子”,因为一个凹槽能接多少水,取决于最短的柱子。当然,如果右边没有柱子了,虽然比最左边低一点,但只要比最深的高,也算一个结束点。
这道题,一旦遇到凹槽结束点,`left` 就会更新,开始新的一轮凹槽计算,所以存在多个滑动窗口。从这道题可以看出,滑动窗口题型相当灵活,不仅判断条件因题而异,窗口数量可能也有多个。
## 总结
滑动窗口本质是双指针的玩法,不同题目有不同的套路,从最简单的按照规律包夹,到快慢指针,再到无固定套路的因题而异的特殊算法。
其实按照规律包夹的套路属于碰撞指针范畴,一般对于排序好的数组,可以一步一步判断,或者用二分法判断,总之不用根据整体遍历来判断,效率自然高。
快慢指针也有套路可循,但具体快多少,或者慢多少,可能具体场景要具体看。
对于无固定套路的滑动窗口,就要根据题目仔细品味啦,如果所有套路都能总结出来,算法也少了乐趣。
> 讨论地址是:[精读《算法 - 滑动窗口》· Issue #328 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/328)
**如果你想参与讨论,请 [点击这里](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)
+213
View File
@@ -0,0 +1,213 @@
如何尝试走迷宫呢?遇到障碍物就从头 “回溯” 继续探索,这就是回溯算法的形象解释。
更抽象的,可以将回溯算法理解为深度遍历一颗树,每个叶子结点都是一种方案的终态,而对某条路线的判断可能在访问到叶子结点之前就结束。
<img width=250 src="https://z3.ax1x.com/2021/06/26/R3HBoq.png">
相比动态规划,回溯可以解决的问题更复杂,尤其是针对具有后效性的问题。
动态规划之所以无法处理有后效性问题,原因是其 `dp(i)=F(dp(j))` 其中 `0<=j<i` 导致的,因为 `i` 通过 `i-1` 推导,如果 `i-1` 的某种选择会对 `i` 的选择产生影响,那么这个推导就是无效的。
而回溯,由于每条分支判断是相互独立的,互不影响,所以即便前面的选择具有后效性,这个后效性也可以在这条选择线路持续影响下去,而不影响其他分支。
所以回溯是一种适用性更广的算法,但相对的,其代价(时间复杂度)也更高,所以只有当没有更优算法时,才应当考虑回溯算法。
## 精读
经过上述思考,回溯算法的实现思路就清晰了:递归或迭代。由于两者可以相互转换,而递归理解成本较低,因此我更倾向于递归方式解决问题。
这里必须提到一点,即工作与算法竞赛思维的区别:由于递归调用堆栈深度较大,整体性能不如迭代好,且迭代写法不如递归自然,所以做算法题时,为了提升那么一点儿性能,以及不经意间流露自己的实力,可能大家更倾向用迭代方式解决问题。
但工作中,大部分是性能不敏感场景,可维护性反而是更重要的,所以工程代码建议用更易理解的递归方式解决问题,把堆栈调用交给计算机去做。
其实算法代码追求更简短,能写成一行的绝不换行也是同样的道理,希望大家能在不同环境里自由切换习惯,而不要拘泥于一种风格。
用递归解决回溯的套路不止一种,我介绍一下自己常用的 TS 语言方法:
```typescript
function func(params: any[], results: any[] = []) {
// 消耗 params 生成 currentResult
const { currentResult, restParams } = doSomething(params);
// 如果 params 还有剩余,则递归消耗,直到 params 耗尽为止
if (restParams.length > 0) func(restParams, results.concat(currentResult));
}
```
这里 `params` 就类似迷宫后面的路线,而 `results` 记录了已走的最佳路线,当 `params` 路线消耗完了,就走出了迷宫,否则终止,让其它递归继续走。
所以回溯逻辑其实挺好写的,难在如何判断这道题应该用回溯做,以及如何优化算法复杂度。
先从两道入门题讲起,分别是电话号码的字母组合与复原 IP 地址。
### 电话号码的字母组合
电话号码的字母组合是一道中等题,题目如下:
> 给定一个仅包含数字  `2-9`  的字符串,返回所有它能表示的字母组合。答案可以按 **任意顺序** 返回。
>
> 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
>
> <img width=200 src="https://z3.ax1x.com/2021/06/26/R3L0wd.png">
电话号码数字对应的字母其实是个映射表,比如 `2` 映射 `a,b,c``3` 映射 `d,e,f`,那么 `2,3` 能表示的字母组合就有 `3x3=9` 种,而要打印出比如 `ad``ae` 这种组合,肯定要用穷举法,穷举法也是回溯的一种,只不过每一种可能性都要而已,而复杂点儿的回溯可能并不是每条路径都符合要求。
所以这道题就好做了,只要构造出所有可能的组合就行。
接下来我们看一道类似,但有一定分支合法判断的题目,复原 IP 地址。
### 复原 IP 地址
复原 IP 地址是一道中等题,题目如下:
> 给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 **有效 IP 地址** 。你可以按任何顺序返回答案。
>
> **有效 IP 地址** 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
>
> 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 **无效 IP 地址**
首先肯定一个一个字符读取,问题就在于,一个字符串可能表示多种可能的 IP,比如 `25525511135` 可以表示为 `255.255.11.135``255.255.111.35`,原因在于,`11.135``111.35` 都是合法的表示,所以我们必须用回溯法解决问题,只是回溯过程中,会根据读取数据动态判定增加哪些新分支,以及哪些分支是非法的。
比如读取到 `[1,1,1,3,5]` 时,由于 `11``111` 都是合法的,因为这个位置的数字只要在 `0~255` 之间即可,而 `1113` 超过这个范围,所以被忽略,所以从这个场景中分叉出两条路:
- 当前项:`11`,余项 `135`
- 当前项:`111`,余项 `35`
之后再递归,直到非法情况终止,比如以及满了 4 项但还有剩余数字,或者不满足 IP 范围等。
可见,只要梳理清楚合法与非法的情况,直到如何动态生成新的递归判断,这道题就不难。
这道题输入很直白,直接给出来了,其实不是每道题的输入都这么容易想,我们看下一道全排列。
### 全排列
全排列是一道中等题,题目如下:
> 给定一个不含重复数字的数组 `nums` ,返回其 **所有可能的全排列** 。你可以 **按任意顺序** 返回答案。
与还原 IP 地址类似,我们也是消耗给的输入,比如 `123`,我们可以先消耗 `1`,余下 `23` 继续组合。但与 IP 复原不同的是,第一个数字可以是 `1` `2` `3` 中的任意一个,所以其实在生成当前项时有所不同:当前项可以从所有余项里挑选,然后再递归即可。
比如 `123` 的第一次可以挑选 `1``2``3`,对于 `1` 的情况,还剩 `23`,那么下次可以挑选 `2``3`,当只剩一项时,就不用挑了。
全排列的输入虽然不如还原 IP 地址的输入直白,但好歹是基于给出的字符串推导而出的,那么再复杂点的题目,输入可能会拆解为多个,这需要你灵活思考,比如括号生成题目。
### 括号生成
括号生成是一道中等题,题目如下:
> 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。
>
> 示例:
> 输入:`n = 3`
>
> 输出:["((()))","(()())","(())()","()(())","()()()"]
这道题基本思路与上一题很像,而且由于题目问的是所有可能性,而不是最优解,所以无法用动规,所以我们考虑回溯算法。
上一道 IP 题目的输入是已知字符串,而这道题的输入就要你动动脑经了。这道题的输入是字符串吗?显然不是,因为输入是括号数量,那么只有一个括号数量就够了吗?不够,因为题目要求有效括号,那什么是有效括号?闭合的才是,所以我们想到用左右括号数量表示这个数字,即输入是 `n`,那么转化为 `open=n, close=n`
有了输入,如何消耗输入呢?我们每一步都可以用一个左括号 `open` 或一个右括号 `close`,但第一个必须是 `open`,且当前已消耗 `close` 数量必须小于已消耗 `open` 数量时,才可以加上 `close`,因为一个 `close` 左边必须有个 `open` 形成合法闭合。
所以这道题就迎刃而解了。回顾来看,回溯的入参要能灵活思考,而这个思考取决于你的经验,比如遇到括号问题,下意识就直到拆解为左右括号。所以算法之间是相通的,适当的知识迁移可以事半功倍。
好了,在此我们先打住,其实不是所有题目都可以用回溯解决,但有些题目看上去只是回溯题目的变种,但其实不然。我们回到上一道全排列题,与之比较像的是 **下一个排列**,这道题看上去好像是基于全排列衍生的,但却无法用回溯算法解决,我们看看这道题。
### 下一个排列
下一个排列是一道中等题,题目如下:
> 实现获取 **下一个排列** 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
>
> 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
>
> 必须 **原地** 修改,只允许使用额外常数空间。
比如:
> 输入:nums = [1,2,3]
>
> 输出:[1,3,2]
> 输入:nums = [3,2,1]
>
> 输出:[1,2,3]
如果你在想,能否借鉴全排列的思想,在全排列过程中自然推导出下一个排列,那大概率是想不通的,因为从整体推导到局部的效率太低,这道题直接给出一个局部值,我们必须用相对 “局部的方法” 快速推导出下一个值,所以这道题无法用回溯算法解决。
 对于 `3,2,1` 的例子,由于已经是最大排列了,所以下个排列只能是初始化的 `1,2,3` 升序,这个是特例。除此之外,都有下一个更大排列,以 `1,2,3` 为例,更大的是 `1,3,2` 而不是 `2,1,3`
我们再观察长一点的例子,比如 `3,2,1,4,5,6`,可以发现,无论前面如何降序,只要最后几个是升序的,只要把最后两个扭转即可:`3,2,1,4,6,5`
如果是 `3,2,1,4,5,6,9,8,7` 呢?显然 `9,8,7` 任意相邻交换都会让数字变得更小,不符合要求,我们还是要交换 `5,6` .. 不 `6,9`,因为 `65x``596` 要大更多。到这里我们得到几个规律:
1. 尽可能交换后面的数。交换 `5,6` 会比交换 `6,9` 更大,因为 `6,9` 更靠后,位数更小。
2. 我们将 `3,2,1,4,5,6,9,8,7` 分为两段,分别是前段 `3,2,1,4,5,6` 和后段 `9,8,7`,我们要让前段尽可能大的数和后段尽可能小的数交换,同时还要保证,后段尽可能小的数比前段尽可能大的数还要 **大**
为了满足第二点,我们必须从后向前查找,如果是升序就跳过,直到找到一个数字 `j``j-1` 小,那么前段作为交换的就是第 `j` 项,后段要找一个最小的数与之交换,由于搜索的算法导致后段一定是降序的,因此从后向前找到第一个比 `j` 大的项交换即可。
最后我们发现,交换后也不一定是完美下一项,因为后段是降序的,而我们已经把前面一个尽可能最小的 “大” 位改大了,后面一定要升序才满足下一个排列,因此要把后段进行升序排列。
因为后段已经满足降序了,因此采用双指针交换法相互对调即可变成升序,这一步千万不要用快排,会导致整体时间复杂度提高 O(nlogn)。
最后由于只扫描了一次 + 反转后段一次,所以算法复杂度是 O(n)。
从这道题可以发现,不要轻视看似变种的题目,从全排列到下一个排列,可能要完全换一个思路,而不是对回溯进行优化。
我们继续回到回溯问题,回溯最经典的问题就是 N 皇后,也是难度最大的题目,与之类似的还有解决数独问题,不过都类似,我们这次还是以 N 皇后作为代表来理解。
### N 皇后问题
N 皇后问题是一道困难题,题目如下:
> n  皇后问题 研究的是如何将 `n`  个皇后放置在 `n×n` 的棋盘上,并且使皇后彼此之间不能相互攻击。
>
> 给你一个整数 `n` ,返回所有不同的  `n`  皇后问题 的解决方案。
>
> 每一种解法包含一个不同的  `n` 皇后问题 的棋子放置方案,该方案中 `'Q'``'.'` 分别代表了皇后和空位。
皇后的攻击范围非常广,包括横、纵、斜,所以当 `n<4` 时是无解的,而神奇的时,`n>=4` 时都有解,比如下面两个图:
<img width=400 src="https://z3.ax1x.com/2021/06/26/R8CtUS.png">
这道题显然具有 “强烈的” 后效性,因为皇后攻击范围是由其位置决定的,换而言之,一个皇后位置确定后,其他皇后的可能摆放位置会发生变化,因此只能用回溯算法。
那么如何识别合法与非法位置呢?核心就是根据横、纵、斜三种攻击方式,建立四个数组,分别存储哪些行、列、撇、捺位置是不能放置的,然后将所有合法位置都作为下一次递归的可能位置,直到皇后放完,或者无位置可放为止。
容易想到的就是四个数组,分别存储被占用的下标,这样的话,只是递归中条件判断分支复杂一些,其它其实并无难度。
这道题的空间复杂度进阶算法是,利用二进制方式,使用 **4 个数字** 代替四个下标数组,每个数组转化为二进制时,1 的位置代表被占用,0 的位置代表未占用,通过位运算,可以更快速、低成本的进行位置占用,与判断当前位置是否被占用。
这里只提一个例子,就可以感受到二进制魅力:
由于按照行看,一行只能放一个皇后,所以每次都从下一行看起,因此行限制就不用看了(至少下一行不可能和前面的行冲突),所以我们只要记录列、撇、捺三个位置即可。
不同之处在于,我们采用二进制的数字,只要三个数字即可表示列、撇、捺。二进制位中的 1 表示被占用,0 表示不被占用。
比如列、撇、捺分别是变量 `x,y,z`,对应二进制可能是:
- `0000001`
- `0010000`
- `0001100`
“非” 逻辑是任意为 1 就是 1,因此 “非” 逻辑可以将所有 1 合并,即 `x | y | z``0011101`
然后将这个结果取反,用非逻辑,即 `~(x | y | z)`,结果是 `1100010`,那这里所有的 `1` 就表示可放的位置,我们记这个变量为 `p`,通过 `p & -p` 不断拿最后一位 `1` 得到安放位置,即可调用递归了。
从这道题可以发现,N 皇后难度不在于回溯算法,而在于如何利用二进制写出高效的回溯算法。所以回溯算法考察的比较综合,因为算法本身很模式化,而且相对比较 “笨拙”,所以需要将更多重心放在优化效率上。
## 总结
回溯算法本质上是利用计算机高速计算能力,将所有可能都尝试一遍,唯一区别是相对暴力解法,可能在某个分支提前终止(枝剪),所以其实是一个较为笨重的算法,当题目确实具有后效性,且无法用贪心或者类似下一排列这种巧妙解法时,才应该采用。
最后我们要总结对比一下回溯与动态规划算法,其实动态规划算法的暴力递归过程就与回溯相当,只是动态规划可以利用缓存,存储之前的结果,避免重复子问题的重复计算,而回溯因为面临的问题具有后效性,不存在重复子问题,所以无法利用缓存加速,所以回溯算法高复杂度是无法避免的。
回溯算法被称为 “通用解题方法”,因为可以解决许多大规模计算问题,是利用计算机运算能力的很好实践。
> 讨论地址是:[精读《算法 - 回溯》· Issue #331 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/331)
**如果你想参与讨论,请 [点击这里](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,258 @@
二叉树是一种数据结构,并且拥有种类复杂的分支,本文作为入门篇,只介绍一些基本二叉树的题型,像二叉搜索树等等不在此篇介绍。
二叉树其实是链表的升级版,即链表同时拥有两个 Next 指针,就变成了二叉树。
二叉树可以根据一些特性,比如搜索二叉树,将查找的时间复杂度降低为 logn,而且堆这种数据结构,也是一种特殊的二叉树,可以以 O(1) 的时间复杂度查找最大值或者最小值。所以二叉树的变种很多,都可以很好的解决具体场景的问题。
## 精读
要入门二叉树,就必须理解二叉树的三种遍历策略,分别是:前序遍历、中序遍历、后序遍历,这些都属于深度优先遍历。
所谓前中后,就是访问节点值在什么时机,其余时机按先左后右访问子节点。比如前序遍历,就是先访问值,再访问左右;后续遍历就是先访问左右,再访问值;中序遍历就是左,值,右。
用递归方式遍历树非常简单:
```typescript
function visitTree(node: TreeNode) {
// 三选一:前序遍历
// console.log(node.val)
visitTree(node.left)
// 三选一:中序遍历
// console.log(node.val)
visitTree(node.right)
// 三选一:后序遍历
// console.log(node.val)
}
```
当然题目需要我们巧妙利用二叉树三种遍历的特性来解题,比如重建二叉树。
### 重建二叉树
重建二叉树是一道中等题,题目如下:
> 输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
>
> 例如
>
> 前序遍历 preorder = `[3,9,20,15,7]`
>
> 中序遍历 inorder = `[9,3,15,20,7]`
先给你二叉树前序与中序遍历结果,让你重建二叉树,这种逆向思维的题目就难了不少。
仔细观察遍历特性可以看出,我们也许能推测出一些关键节点的位置,再通过数组切割递归一下就能解题。
前序遍历第一个访问的一定是根节点,因此 `3` 一定是根节点,然后我们在中序遍历找到 `3`,这样 **左边就是所有左子树的中序遍历结果,右边就是所有右子树的中序遍历结果**,我们只要再找到 **左子树的前序遍历结果与右子树的前序遍历结果**,就可以递归了,终止条件是左或右子树只有一个值,那样就代表叶子节点。
那么怎么找左右子树的前序遍历呢?上面例子中,我们找到了 `3` 的左右子树的中序遍历结果,由于前序遍历优先访问左子树,因此我们数一下中序遍历中,`3` 左边的数量,只有一个 `9`,那么我们从前序遍历的 `3,9,20,15,7``3` 之后推一位,那么 `9` 就是左子树前序遍历结果,`9` 后面的 `20,15,7` 就是右子树的前序遍历结果。
最后只要递归一下就能解题了,我们将输入不断拆解为左右子树的的输入,直到达到终止条件。
解决此题的关键是,不仅要知道如何写前中后序遍历,还要知道前序遍历第一个节点是根节点,后序遍历最后一个节点是根节点,中序遍历以根节点为中心,左右分别是其左右子树,这几个重要延伸特征。
说完了反向,我们说正向,即递归一棵二叉树。
其实二叉树除了递归,还有一种常见的遍历方法是利用栈进行广度优先遍历,典型题目有从上到下打印二叉树。
### 从上到下打印二叉树
从上到下打印二叉树是一道简单题,题目如下:
> 从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
这道题要求从左到右顺序打印,完全遵循广度优先遍历,我们可以在二叉树递归时,先不要急着读取值,而是按照左、中、右,遇到左右子树节点,就推入栈的末尾,利用 `while` 语句不断循环,直到栈空为止。
利用展开时追加到栈尾,并不断循环处理栈元素的方式非常优雅,而且符合栈的特性。
当然如果题目要求倒序打印,你就可以以 右、中、左 的顺序进行处理。
接下来看看深度优先遍历,典型题目是二叉树的深度。
### 二叉树的深度
二叉树的深度是一道简单题,题目如下:
> 输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
由于二叉树有多种分支,在遍历前,我们并不知道哪条路线是最深的,所以必须利用递归尝试。
我们可以转换一下思路,用函数式语义方式来理解。假设我们有了这样一个函数 `deep` 来求二叉树深度,那么这个函数内容是什么呢?二叉树只可能存在左右子树,所以 `deep` 必然是左右子树的最大深度的最大值 +1(它自己)。
而求左右子树深度可以复用 `deep` 函数形成递归,我们只需要考虑边界情况,即访问节点不存在时,返回深度 `0` 即可,因此代码如下:
```typescript
function deep(node: TreeNode) {
if (!node) return 0
return Math.max(deep(node.left), deep(node.right)) + 1
}
```
从这可以看出,二叉树一般能用比较优雅的递归函数解决,如果你的解题思路不包含递归,往往就不是最优雅的解法。
类似优雅的题目还有,平衡二叉树。
### 平衡二叉树
平衡二叉树是一道简单题,题目如下:
> 输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过 1,那么它就是一棵平衡二叉树。
同理,我们设函数 `isBalance` 就是答案函数,那么一个平衡二叉树的特征,必然是其左右子树也是平衡的,所以可以写成:
```typescript
function isBalance(node: TreeNode) {
if (root == null) return true
return isBalance(node.left) && isBalance(node.right)
}
```
但是哪里不对,左右子树平衡还不够啊,万一左右子树之间深度相差超过 1 就坏了,所以还要求一下左右子树的深度,我们复用上题的函数 `deep`,整理一下如下:
```typescript
function isBalance(node: TreeNode) {
if (root == null) return true
return isBalance(root.left) && isBalance(root.right) &&
Math.abs(deep(root.left) - deep(root.right)) < 2
}
```
这道题提醒我们,不是所有递归都能完美写成仅自己调用自己的模式,不同题目要辅以其他函数,要敏锐的察觉到还缺少哪些条件。
还有一种递归,不是简单的函数自身递归自身,而是要构造出另一个函数进行递归,原因是递归参数不同。典型的题目有对称的二叉树。
### 对称的二叉树
对称的二叉树是一道简单题,题目如下:
> 请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
我们要注意,一颗二叉树的镜像比较特殊,比如最左节点与最右节点互为镜像,但它们的父节点并不相同,因此 `isSymmetric(tree)` 这样的参数是无法子递归的,我们必须拆解为左右子树作为参数,让它们进行相等判断,在传参时,将父级不同,但互为镜像的左右节点传入即可。
所以我们必须起一个新函数 `isSymmetricNew(left, right)`,将 `left.left``right.right` 对比,将 `left.right``right.left` 对比即可。
具体代码就不写了,然后注意一下边界情况即可。
这道题的重点是,由于镜像的关系,并不拥有相同的父节点,因此必须用一个新参数的函数进行递归。
那如果这道题反过来呢?要求构造一个二叉树镜像呢?
### 二叉树的镜像
二叉树的镜像是一道简单题,题目如下:
> 请完成一个函数,输入一个二叉树,该函数输出它的镜像。
判断镜像比较容易,但构造镜像就要想一想了:
```text
例如输入:
     4
   /   \
  2     7
 / \   / \
1   3 6   9
镜像输出:
     4
   /   \
  7     2
 / \   / \
9   6 3   1
```
观察发现,其实镜像可以理解为左右子树互换,同时 **其各子树的左右子树再递归互换**,这就构成了一个递归:
```typescript
function mirrorTree(node: TreeNode) {
if (node === null) return null
const left = mirrorTree(node.left)
const right = mirrorTree(node.right)
node.left = right
node.right = left
return node
}
```
我们要从下到上,因此先生成递归好的左右子树,再进行当前节点的互换,最后返回根节点即可。
接下来介绍一些有一定难度的经典题。
### 二叉树的最近公共祖先
二叉树的最近公共祖先是一道中等题,题目如下:
> 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
题目很简短,也很明确,就是寻找最近的公共祖先。显然,根节点是所有节点的公共祖先,但不一定是最近的。
我们还是用递归,先考虑特殊情况:如果任意节点等于当前节点,那么当前节点一定就是最近公共祖先,因为另一个节点一定在其子节点中。
然后,利用递归思想思考,假设我们利用 `lowestCommonAncestor` 函数分别找到左右子节点的最近公共祖先会怎样?
```typescript
function lowestCommonAncestor(node, a, b) {
const left = lowestCommonAncestor(node.left)
const right = lowestCommonAncestor(node.right)
}
```
如果左右节点都找不到,说明只可能当前节点是最近公共子节点:
```typescript
if (!left && !right) return node
```
如果左节点找不到,则右节点就是答案,否则相反:
```typescript
if (!left) return right
return left
```
这里巧妙利用了函数语义进行结果判断。
### 二叉树的右视图
二叉树的右视图是一道中等题,题目如下:
> 给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
想象一束光照,从二叉树右侧向左照射,自上而下读取即是答案。
其实这道题可以认为是一道融合题。右侧的光束可以认为是分层照射的,那么当我们用广度优先算法遍历时,对于每一层,都找到最后一个节点打印,并且按顺序打印就是最终答案。
有一道二叉树的题目,是根据树的深度,按照广度优先遍历打印成二维数组,记录树的深度其实也有巧妙办法,即在栈尾追加元素时,增加一个深度 key,那么访问时自然就可以读到深度值。
### 完全二叉树的节点个数
完全二叉树的节点个数是一道中等题,题目如下:
> 给你一棵 **完全二叉树** 的根节点 `root` ,求出该树的节点个数。
>
> **完全二叉树** 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 `h` 层,则该层包含 `1 ~ 2^h` 个节点。
用递归解决这道题的话,关键要分几种情况探讨完全二叉树。
由于最底层可能没有填满,但最底层一定有节点,而且是按照从左到右填的,那么递归遍历左节点就可以获取树的最大深度,通过最大深度我们可以快速计算出节点个树,前提是二叉树必须是满的。
但最底层节点可能不满,那怎么办呢?分情况即可,首先,如果一直按照 `node.right....right` 递归获得右侧节点深度,发现和最大深度相同,那么就是一个满二叉树,直接计算出结果即可。
我们再看 `node.right...left` 的深度如果等于最大深度,说明 `node.left` 也就是左子树是个满二叉树,可以通过数学公式 `2^n-1` 快速算出节点个树。
如果不等于最大深度呢?**则说明右子树深度减 1 是满二叉树**,也可以通过数学公式快速计算节点个数,再通过递归计算另一边即可。
## 总结
从题目中可以感受到,二叉树的解题魅力在于递归,二叉树问题中,我们可以同时追求优雅与答案。
> 讨论地址是:[精读《算法 - 二叉树》· Issue #331 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/331)
**如果你想参与讨论,请 [点击这里](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,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)