Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b06c714fa | ||
|
|
cf5cc265cb | ||
|
|
366c6c90c3 | ||
|
|
98cc6ace48 | ||
|
|
751d78be0a | ||
|
|
acfb7fd442 | ||
|
|
750e2cec32 | ||
|
|
d01463fc9e | ||
|
|
534e5a0f49 | ||
|
|
3b2b2adde4 | ||
|
|
e905d37dab | ||
|
|
14dbde8615 | ||
|
|
62afd07694 | ||
|
|
933862c0cd | ||
|
|
d4d87b029f | ||
|
|
ed6a7d10c2 | ||
|
|
6e36e81748 | ||
|
|
f3a50ce6f7 | ||
|
|
c8c9f7c7d9 | ||
|
|
7eeecc34d2 | ||
|
|
c8448215c7 | ||
|
|
050f00b3e5 | ||
|
|
b661e2329a | ||
|
|
8691358638 | ||
|
|
8116ed1f72 | ||
|
|
6ccc12acfe | ||
|
|
41a9d364a0 | ||
|
|
499a766252 | ||
|
|
af0f3dd04d | ||
|
|
3cd0bb8761 | ||
|
|
fa6d19f415 | ||
|
|
7fce362c17 | ||
|
|
fe93fce942 | ||
|
|
088df6eda6 | ||
|
|
62ed443234 | ||
|
|
49fd947437 | ||
|
|
d4eb4ee606 | ||
|
|
04c67e183d | ||
|
|
86ccf45f85 | ||
|
|
3f9febce36 | ||
|
|
140da78305 | ||
|
|
12c85b9418 | ||
|
|
4cee1951da | ||
|
|
548fde2f85 | ||
|
|
1ed6ac5c48 | ||
|
|
4b1fa0c7c6 | ||
|
|
d0609b0e77 | ||
|
|
0cc317e9f1 | ||
|
|
8dad1ef1af | ||
|
|
e81a26600f | ||
|
|
18a495ec39 | ||
|
|
713c88518f | ||
|
|
a7fa58f620 | ||
|
|
50e042c90d | ||
|
|
b2c02f7892 | ||
|
|
b91e09f170 | ||
|
|
68cd8d1966 | ||
|
|
fa28d1513c | ||
|
|
14f93ddd71 | ||
|
|
da81242d9b | ||
|
|
89c50fc032 | ||
|
|
c81446eff5 | ||
|
|
d5f96ff5a3 | ||
|
|
db3449ace1 | ||
|
|
1dddebbb1f | ||
|
|
77e2da3f2d | ||
|
|
019076e87f | ||
|
|
18e058af5b | ||
|
|
fe7b930b95 | ||
|
|
1a9be6aaf8 | ||
|
|
3110990b90 | ||
|
|
e5e2d49ae1 | ||
|
|
94381de60b | ||
|
|
70f20e6861 | ||
|
|
fab11f31d2 | ||
|
|
8af572d9f9 | ||
|
|
0c542af0ab | ||
|
|
4b3aae675d | ||
|
|
532d3bd6f0 | ||
|
|
cb20d65c2c | ||
|
|
07b6017451 | ||
|
|
2c5f8ca4f3 | ||
|
|
cd543b1fd4 | ||
|
|
88560b6626 | ||
|
|
1cf5e339c9 | ||
|
|
4ac67508f4 | ||
|
|
d2044c4636 | ||
|
|
59629228f9 | ||
|
|
eeb4272d49 | ||
|
|
e8ae8557d2 | ||
|
|
f0ab10ac09 | ||
|
|
b7b0409cd5 | ||
|
|
13963fbdd2 | ||
|
|
9b9d02b9de | ||
|
|
ec65f684bb | ||
|
|
022b1fdbbd | ||
|
|
ff121c7e1b | ||
|
|
a04380c1c1 | ||
|
|
3646b36db5 | ||
|
|
aaaa28953b | ||
|
|
9609101870 | ||
|
|
afb601a303 | ||
|
|
c72477ed7d | ||
|
|
dc09550e0c | ||
|
|
dc66e76dd1 | ||
|
|
cecef87d13 | ||
|
|
d0ca3a348f | ||
|
|
de4e309f39 | ||
|
|
ea56ac10ce | ||
|
|
90660c6d34 | ||
|
|
fcf04083a5 | ||
|
|
a1ea12a736 | ||
|
|
02a795bb8a | ||
|
|
6bffed8eb5 | ||
|
|
ddd5ceb74a | ||
|
|
3c6cee2f49 | ||
|
|
fc2f0c6f68 | ||
|
|
0dcf208315 | ||
|
|
7730834e83 | ||
|
|
91875ab232 | ||
|
|
d81e6d7813 | ||
|
|
3e299d85e9 | ||
|
|
455e1693f4 | ||
|
|
2a17e62107 | ||
|
|
0a090cf0a7 | ||
|
|
c13f0af435 | ||
|
|
8ee7995ddc | ||
|
|
0e8c749e68 | ||
|
|
ee0450ffb2 |
@@ -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("");
|
||||
});
|
||||
|
||||
Generated
+2158
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -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 ./"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
最新精读:<a href="./前沿技术/196.精读《前端职业规划 - 2021 年》.md">196.精读《前端职业规划 - 2021 年》</a>
|
||||
最新精读:<a href="./前沿技术/230.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%20Markdown%20%E7%9A%84%E6%80%9D%E8%80%83%E3%80%8B.md">230.精读《对 Markdown 的思考》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
@@ -19,214 +19,251 @@
|
||||
|
||||
### 前沿技术
|
||||
|
||||
- <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 don’t》.md">21.精读《Web fonts: when you need them, when you don’t》</a>
|
||||
- <a href="./前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md">22.精读《V8 引擎特性带来的的 JS 性能变化》</a>
|
||||
- <a href="./前沿技术/23.精读《API 设计原则》.md">23.精读《API 设计原则》</a>
|
||||
- <a href="./前沿技术/24.精读《现代 JavaScript 概览》.md">24.精读《现代 JavaScript 概览》</a>
|
||||
- <a href="./前沿技术/25.精读《null >= 0?》.md">25.精读《null >= 0?》</a>
|
||||
- <a href="./前沿技术/26.精读《加密媒体扩展》.md">26.精读《加密媒体扩展》</a>
|
||||
- <a href="./前沿技术/27.精读《css-in-js 杀鸡用牛刀》.md">27.精读《css-in-js 杀鸡用牛刀》</a>
|
||||
- <a href="./前沿技术/28.精读《2017 前端性能优化备忘录》.md">28.精读《2017 前端性能优化备忘录》</a>
|
||||
- <a href="./前沿技术/29.精读《JS 中的内存管理》.md">29.精读《JS 中的内存管理》</a>
|
||||
- <a href="./前沿技术/30.精读《Javascript 事件循环与异步》.md">30.精读《Javascript 事件循环与异步》</a>
|
||||
- <a href="./前沿技术/31.精读《我不再使用高阶组件》.md">31.精读《我不再使用高阶组件》</a>
|
||||
- <a href="./前沿技术/32.精读《React Router4.0 进阶概念》.md">32.精读《React Router4.0 进阶概念》</a>
|
||||
- <a href="./前沿技术/33.精读《30 行 js 代码创建神经网络》.md">33.精读《30 行 js 代码创建神经网络》</a>
|
||||
- <a href="./前沿技术/34.精读《React 代码整洁之道》.md">34.精读《React 代码整洁之道》</a>
|
||||
- <a href="./前沿技术/35.精读《dob - 框架实现》.md">35.精读《dob - 框架实现》</a>
|
||||
- <a href="./前沿技术/36.精读《When You “Git” in Trouble- a Version Control Story》.md">36.精读《When You “Git” in Trouble- a Version Control Story》</a>
|
||||
- <a href="./前沿技术/37.精读《how we position and what we compare》.md">37.精读《how we position and what we compare》</a>
|
||||
- <a href="./前沿技术/38.精读《dob - 框架使用》.md">38.精读《dob - 框架使用》</a>
|
||||
- <a href="./前沿技术/39.精读《全链路体验浏览器挖矿》.md">39.精读《全链路体验浏览器挖矿》</a>
|
||||
- <a href="./前沿技术/40.精读《初探 Reason 与 GraphQL》.md">40.精读《初探 Reason 与 GraphQL》</a>
|
||||
- <a href="./前沿技术/41.精读《Ant Design 3.0 背后的故事》.md">41.精读《Ant Design 3.0 背后的故事》</a>
|
||||
- <a href="./前沿技术/42.精读《前端数据流哲学》.md">42.精读《前端数据流哲学》</a>
|
||||
- <a href="./前沿技术/43.精读《增强现实与可视化》.md">43.精读《增强现实与可视化》</a>
|
||||
- <a href="./前沿技术/44.精读《Rekit Studio》.md">44.精读《Rekit Studio》</a>
|
||||
- <a href="./前沿技术/45.精读《React's new Context API》.md">45.精读《React's new Context API》</a>
|
||||
- <a href="./前沿技术/46.精读《react-rxjs》.md">46.精读《react-rxjs》</a>
|
||||
- <a href="./前沿技术/47.精读《webpack4.0 升级指南》.md">47.精读《webpack4.0 升级指南》</a>
|
||||
- <a href="./前沿技术/49.精读《Compilers are the New Frameworks》.md">49.精读《Compilers are the New Frameworks》</a>
|
||||
- <a href="./前沿技术/50.精读《快速上手构建 ARKit 应用》.md">50.精读《快速上手构建 ARKit 应用》</a>
|
||||
- <a href="./前沿技术/51.精读《Elements of Web Dev》.md">51.精读《Elements of Web Dev》</a>
|
||||
- <a href="./前沿技术/52.精读《图解 ES 模块》.md">52.精读《图解 ES 模块》</a>
|
||||
- <a href="./前沿技术/53.精读《插件化思维》.md">53.精读《插件化思维》</a>
|
||||
- <a href="./前沿技术/54.精读《在浏览器运行 serverRender》.md">54.精读《在浏览器运行 serverRender》</a>
|
||||
- <a href="./前沿技术/55.精读《async await 是把双刃剑》.md">55.精读《async await 是把双刃剑》</a>
|
||||
- <a href="./前沿技术/56.精读《重新思考 Redux》.md">56.精读《重新思考 Redux》</a>
|
||||
- <a href="./前沿技术/57.精读《现代 js 框架存在的根本原因》.md">57.精读《现代 js 框架存在的根本原因》</a>
|
||||
- <a href="./前沿技术/58.精读《Typescript2.0 - 2.9》.md">58.精读《Typescript2.0 - 2.9》</a>
|
||||
- <a href="./前沿技术/59.精读《如何利用 Nodejs 监听文件夹》.md">59.精读《如何利用 Nodejs 监听文件夹》</a>
|
||||
- <a href="./前沿技术/60.精读《如何在 nodejs 使用环境变量》.md">60.精读《如何在 nodejs 使用环境变量》</a>
|
||||
- <a href="./前沿技术/61.精读《React 八种条件渲染》.md">61.精读《React 八种条件渲染》</a>
|
||||
- <a href="./前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md">62.精读《JS 引擎基础之 Shapes and Inline Caches》</a>
|
||||
- <a href="./前沿技术/63.精读《React 的多态性》.md">63.精读《React 的多态性》</a>
|
||||
- <a href="./前沿技术/68.精读《衡量用户体验》.md">68.精读《衡量用户体验》</a>
|
||||
- <a href="./前沿技术/69.精读《SQL vs Flux》.md">69.精读《SQL vs Flux》</a>
|
||||
- <a href="./前沿技术/72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》.md">72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》</a>
|
||||
- <a href="./前沿技术/74.精读《12 个评估 JS 库你需要关心的事》.md">74.精读《12 个评估 JS 库你需要关心的事》</a>
|
||||
- <a href="./前沿技术/76.精读《谈谈 Web Workers》.md">76.精读《谈谈 Web Workers》</a>
|
||||
- <a href="./前沿技术/77.精读《用 Reduce 实现 Promise 串行执行》.md">77.精读《用 Reduce 实现 Promise 串行执行》</a>
|
||||
- <a href="./前沿技术/79.精读《React Hooks》.md">79.精读《React Hooks》</a>
|
||||
- <a href="./前沿技术/80.精读《怎么用 React Hooks 造轮子》.md">80.精读《怎么用 React Hooks 造轮子》</a>
|
||||
- <a href="./前沿技术/81.精读《使用 CSS 属性选择器》.md">81.精读《使用 CSS 属性选择器》</a>
|
||||
- <a href="./前沿技术/83.精读《React16 新特性》.md">83.精读《React16 新特性》</a>
|
||||
- <a href="./前沿技术/84.精读《Typescript 3.2 新特性》.md">84.精读《Typescript 3.2 新特性》</a>
|
||||
- <a href="./前沿技术/86.精读《国际化布局 - Logical Properties》.md">86.精读《国际化布局 - Logical Properties》</a>
|
||||
- <a href="./前沿技术/87.精读《setState 做了什么》.md">87.精读《setState 做了什么》</a>
|
||||
- <a href="./前沿技术/88.精读《Caches API》.md">88.精读《Caches API》</a>
|
||||
- <a href="./前沿技术/89.精读《如何编译前端项目与组件》.md">89.精读《如何编译前端项目与组件》</a>
|
||||
- <a href="./前沿技术/91.精读《正则 ES2018》.md">91.精读《正则 ES2018》</a>
|
||||
- <a href="./前沿技术/94.精读《Serverless 给前端带来了什么》.md">94.精读《Serverless 给前端带来了什么》</a>
|
||||
- <a href="./前沿技术/95.精读《Function VS Class 组件》.md">95.精读《Function VS Class 组件》</a>
|
||||
- <a href="./前沿技术/96.精读《useEffect 完全指南》.md">96.精读《useEffect 完全指南》</a>
|
||||
- <a href="./前沿技术/97.精读《编写有弹性的组件》.md">97.精读《编写有弹性的组件》</a>
|
||||
- <a href="./前沿技术/99.精读《Scheduling in React》.md">99.精读《Scheduling in React》</a>
|
||||
- <a href="./前沿技术/100.精读《V8 引擎 Lazy Parsing》.md">100.精读《V8 引擎 Lazy Parsing》</a>
|
||||
- <a href="./前沿技术/101.精读《持续集成 vs 持续交付 vs 持续部署》.md">101.精读《持续集成 vs 持续交付 vs 持续部署》</a>
|
||||
- <a href="./前沿技术/102.精读《Monorepo 的优势》.md">102.精读《Monorepo 的优势》</a>
|
||||
- <a href="./前沿技术/104.精读《Function Component 入门》.md">104.精读《Function Component 入门》</a>
|
||||
- <a href="./前沿技术/105.精读《What's new in javascript》.md">105.精读《What's new in javascript》</a>
|
||||
- <a href="./前沿技术/107.精读《Optional chaining》.md">107.精读《Optional chaining》</a>
|
||||
- <a href="./前沿技术/109.精读《Vue3.0 Function API》.md">109.精读《Vue3.0 Function API》</a>
|
||||
- <a href="./前沿技术/111.精读《前端未来展望》.md">111.精读《前端未来展望》</a>
|
||||
- <a href="./前沿技术/112.精读《源码学习》.md">112.精读《源码学习》</a>
|
||||
- <a href="./前沿技术/113.精读《Nodejs V12》.md">113.精读《Nodejs V12》</a>
|
||||
- <a href="./前沿技术/117.精读《Tableau 探索式模型》.md">117.精读《Tableau 探索式模型》</a>
|
||||
- <a href="./前沿技术/118.精读《使用 css 变量生成颜色主题》.md">118.精读《使用 css 变量生成颜色主题》</a>
|
||||
- <a href="./前沿技术/119.精读《前端深水区》.md">119.精读《前端深水区》</a>
|
||||
- <a href="./前沿技术/120.精读《React Hooks 最佳实践》.md">120.精读《React Hooks 最佳实践》</a>
|
||||
- <a href="./前沿技术/121.精读《前端与 BI》.md">121.精读《前端与 BI》</a>
|
||||
- <a href="./前沿技术/123.精读《用 Babel 创造自定义 JS 语法》.md">123.精读《用 Babel 创造自定义 JS 语法》</a>
|
||||
- <a href="./前沿技术/124.精读《用 css grid 重新思考布局》.md">124.精读《用 css grid 重新思考布局》</a>
|
||||
- <a href="./前沿技术/125.精读《深度学习 - 函数式之美》.md">125.精读《深度学习 - 函数式之美》</a>
|
||||
- <a href="./前沿技术/126.精读《Nuxtjs》.md">126.精读《Nuxtjs》</a>
|
||||
- <a href="./前沿技术/127.精读《React Conf 2019 - Day1》.md">127.精读《React Conf 2019 - Day1》</a>
|
||||
- <a href="./前沿技术/129.精读《React Conf 2019 - Day2》.md">129.精读《React Conf 2019 - Day2》</a>
|
||||
- <a href="./前沿技术/132.精读《正交的 React 组件》.md">132.精读《正交的 React 组件》</a>
|
||||
- <a href="./前沿技术/133.精读《寻找框架设计的平衡点》.md">133.精读《寻找框架设计的平衡点》</a>
|
||||
- <a href="./前沿技术/134.精读《我在阿里数据中台大前端》.md">134.精读《我在阿里数据中台大前端》</a>
|
||||
- <a href="./前沿技术/138.精读《精通 console.log》.md">138.精读《精通 console.log》</a>
|
||||
- <a href="./前沿技术/139.精读《手写 JSON Parser》.md">139.精读《手写 JSON Parser》</a>
|
||||
- <a href="./前沿技术/140.精读《结合 React 使用原生 Drag Drop API》.md">140.精读《结合 React 使用原生 Drag Drop API》</a>
|
||||
- <a href="./前沿技术/141.精读《useRef 与 createRef 的区别》.md">141.精读《useRef 与 createRef 的区别》</a>
|
||||
- <a href="./前沿技术/142.精读《如何做好 CodeReview》.md">142.精读《如何做好 CodeReview》</a>
|
||||
- <a href="./前沿技术/143.精读《Suspense 改变开发方式》.md">143.精读《Suspense 改变开发方式》</a>
|
||||
- <a href="./前沿技术/144.精读《Webpack5 新特性 - 模块联邦》.md">144.精读《Webpack5 新特性 - 模块联邦》</a>
|
||||
- <a href="./前沿技术/145.精读《React Router v6》.md">145.精读《React Router v6》</a>
|
||||
- <a href="./前沿技术/146.精读《React Hooks 数据流》.md">146.精读《React Hooks 数据流》</a>
|
||||
- <a href="./前沿技术/147. 精读《@types react 值得注意的 TS 技巧》.md">147. 精读《@types react 值得注意的 TS 技巧》</a>
|
||||
- <a href="./前沿技术/148. 精读《React Error Boundaries》.md">148. 精读《React Error Boundaries》</a>
|
||||
- <a href="./前沿技术/149. 精读《React 性能调试》.md">149. 精读《React 性能调试》</a>
|
||||
- <a href="./前沿技术/150. 精读《Deno 1.0 你需要了解的》.md">150. 精读《Deno 1.0 你需要了解的》</a>
|
||||
- <a href="./前沿技术/152. 精读《recoil》.md">152. 精读《recoil》</a>
|
||||
- <a href="./前沿技术/153. 精读《snowpack》.md">153. 精读《snowpack》</a>
|
||||
- <a href="./前沿技术/154. 精读《用 React 做按需渲染》.md">154. 精读《用 React 做按需渲染》</a>
|
||||
- <a href="./前沿技术/157. 精读《如何比较 Object 对象》.md">157. 精读《如何比较 Object 对象》</a>
|
||||
- <a href="./前沿技术/158. 精读《Typescript 4》.md">158. 精读《Typescript 4》</a>
|
||||
- <a href="./前沿技术/159. 精读《对低代码搭建的理解》.md">159. 精读《对低代码搭建的理解》</a>
|
||||
- <a href="./前沿技术/160. 精读《函数缓存》.md">160. 精读《函数缓存》</a>
|
||||
- <a href="./前沿技术/161.精读《可视化搭建思考 - 富文本搭建》.md">161.精读《可视化搭建思考 - 富文本搭建》</a>
|
||||
- <a href="./前沿技术/162.精读《Tasks, microtasks, queues and schedules》.md">162.精读《Tasks, microtasks, queues and schedules》</a>
|
||||
- <a href="./前沿技术/163.精读《Spring 概念》.md">163.精读《Spring 概念》</a>
|
||||
- <a href="./前沿技术/164.精读《数据搭建引擎 bi-designer API-设计器》.md">164.精读《数据搭建引擎 bi-designer API-设计器》</a>
|
||||
- <a href="./前沿技术/165.精读《数据搭建引擎 bi-designer API-组件》.md">165.精读《数据搭建引擎 bi-designer API-组件》</a>
|
||||
- <a href="./前沿技术/166.精读《BI 搭建 - 筛选条件》.md">166.精读《BI 搭建 - 筛选条件》</a>
|
||||
- <a href="./前沿技术/190.精读《DOM diff 原理详解》.md">190.精读《DOM diff 原理详解》</a>
|
||||
- <a href="./前沿技术/191.精读《高性能表格》.md">191.精读《高性能表格》</a>
|
||||
- <a href="./前沿技术/192.精读《DOM diff 最长上升子序列》.md">192.精读《DOM diff 最长上升子序列》</a>
|
||||
- <a href="./前沿技术/193.精读《React Server Component》.md">193.精读《React Server Component》</a>
|
||||
- <a href="./前沿技术/194.精读《算法基础数据结构》.md">194.精读《算法基础数据结构》</a>
|
||||
- <a href="./前沿技术/195.精读《新一代前端构建工具对比》.md">195.精读《新一代前端构建工具对比》</a>
|
||||
- <a href="./前沿技术/196.精读《前端职业规划 - 2021 年》.md">196.精读《前端职业规划 - 2021 年》</a>
|
||||
- <a href="./前沿技术/1.%E7%B2%BE%E8%AF%BB%E3%80%8Ajs%20%E6%A8%A1%E5%9D%97%E5%8C%96%E5%8F%91%E5%B1%95%E3%80%8B.md">1.精读《js 模块化发展》</a>
|
||||
- <a href="./前沿技术/2.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%A8%A1%E6%80%81%E6%A1%86%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">2.精读《模态框的最佳实践》</a>
|
||||
- <a href="./前沿技术/3.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E5%90%8E%E7%AB%AF%E6%B8%B2%E6%9F%93%E4%B9%8B%E4%BA%89%E3%80%8B.md">3.精读《前后端渲染之争》</a>
|
||||
- <a href="./前沿技术/4.%E7%B2%BE%E8%AF%BB%E3%80%8AAsyncAwait%20%E4%BC%98%E8%B6%8A%E4%B9%8B%E5%A4%84%E3%80%8B.md">4.精读《AsyncAwait 优越之处》</a>
|
||||
- <a href="./前沿技术/5.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B0%91%E5%B7%A5%E5%8F%94%E5%8D%95%E9%A1%B5%E6%95%B0%E6%8D%AE%E6%B5%81%E6%96%B9%E6%A1%88%E3%80%8B.md">5.精读《民工叔单页数据流方案》</a>
|
||||
- <a href="./前沿技术/6.%E7%B2%BE%E8%AF%BB%E3%80%8AJavaScript%20%E9%94%99%E8%AF%AF%E5%A0%86%E6%A0%88%E5%A4%84%E7%90%86%E3%80%8B.md">6.精读《JavaScript 错误堆栈处理》</a>
|
||||
- <a href="./前沿技术/7.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AF%B7%E5%81%9C%E6%AD%A2%20css-in-js%20%E7%9A%84%E8%A1%8C%E4%B8%BA%E3%80%8B.md">7.精读《请停止 css-in-js 的行为》</a>
|
||||
- <a href="./前沿技术/8.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A5%E5%9D%91%20React%20%E5%89%8D%E6%B2%A1%E6%9C%89%E4%BA%BA%E4%BC%9A%E5%91%8A%E8%AF%89%E4%BD%A0%E7%9A%84%E4%BA%8B%E3%80%8B.md">8.精读《入坑 React 前没有人会告诉你的事》</a>
|
||||
- <a href="./前沿技术/9.%E7%B2%BE%E8%AF%BB%E3%80%8AImmutable%20%E7%BB%93%E6%9E%84%E5%85%B1%E4%BA%AB%E3%80%8B.md">9.精读《Immutable 结构共享》</a>
|
||||
- <a href="./前沿技术/10.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20Components%20%E7%9A%84%E5%9B%B0%E5%A2%83%E3%80%8B.md">10.精读《Web Components 的困境》</a>
|
||||
- <a href="./前沿技术/11.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7%E3%80%8B.md">11.精读《前端调试技巧》</a>
|
||||
- <a href="./前沿技术/12.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">12.精读《React 高阶组件》</a>
|
||||
- <a href="./前沿技术/13.%E7%B2%BE%E8%AF%BB%E3%80%8AThis%20%E5%B8%A6%E6%9D%A5%E7%9A%84%E5%9B%B0%E6%83%91%E3%80%8B.md">13.精读《This 带来的困惑》</a>
|
||||
- <a href="./前沿技术/14.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B9%8B%20DCI%E3%80%8B.md">14.精读《架构设计之 DCI》</a>
|
||||
- <a href="./前沿技术/15.%E7%B2%BE%E8%AF%BB%E3%80%8ATC39%20%E4%B8%8E%20ECMAScript%20%E6%8F%90%E6%A1%88%E3%80%8B.md">15.精读《TC39 与 ECMAScript 提案》</a>
|
||||
- <a href="./前沿技术/16.%E7%B2%BE%E8%AF%BB%E3%80%8ACSS%20Animations%20vs%20Web%20Animations%20API%E3%80%8B.md">16.精读《CSS Animations vs Web Animations API》</a>
|
||||
- <a href="./前沿技术/17.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%AE%89%E5%85%A8%E5%9C%B0%E4%BD%BF%E7%94%A8%20React%20context%E3%80%8B.md">17.精读《如何安全地使用 React context》</a>
|
||||
- <a href="./前沿技术/18.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E5%AE%8C%E7%BE%8E%E7%9A%84%E6%97%A5%E6%9C%9F%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">18.精读《设计完美的日期选择器》</a>
|
||||
- <a href="./前沿技术/19.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9C%80%E4%BD%B3%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98%E3%80%8B%E5%8F%8A%E9%9D%A2%E8%AF%95%E5%AE%98%E6%8A%80%E5%B7%A7.md">19.精读《最佳前端面试题》及面试官技巧</a>
|
||||
- <a href="./前沿技术/20.%E7%B2%BE%E8%AF%BB%E3%80%8ANestjs%E3%80%8B%E6%96%87%E6%A1%A3.md">20.精读《Nestjs》文档</a>
|
||||
- <a href="./前沿技术/21.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20fonts%3A%20when%20you%20need%20them%2C%20when%20you%20don%E2%80%99t%E3%80%8B.md">21.精读《Web fonts: when you need them, when you don’t》</a>
|
||||
- <a href="./前沿技术/22.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%E7%89%B9%E6%80%A7%E5%B8%A6%E6%9D%A5%E7%9A%84%E7%9A%84%20JS%20%E6%80%A7%E8%83%BD%E5%8F%98%E5%8C%96%E3%80%8B.md">22.精读《V8 引擎特性带来的的 JS 性能变化》</a>
|
||||
- <a href="./前沿技术/23.%E7%B2%BE%E8%AF%BB%E3%80%8AAPI%20%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%E3%80%8B.md">23.精读《API 设计原则》</a>
|
||||
- <a href="./前沿技术/24.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20JavaScript%20%E6%A6%82%E8%A7%88%E3%80%8B.md">24.精读《现代 JavaScript 概览》</a>
|
||||
- <a href="./前沿技术/25.%E7%B2%BE%E8%AF%BB%E3%80%8Anull%20%3E%3D%200%3F%E3%80%8B.md">25.精读《null >= 0?》</a>
|
||||
- <a href="./前沿技术/26.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8A%A0%E5%AF%86%E5%AA%92%E4%BD%93%E6%89%A9%E5%B1%95%E3%80%8B.md">26.精读《加密媒体扩展》</a>
|
||||
- <a href="./前沿技术/27.%E7%B2%BE%E8%AF%BB%E3%80%8Acss-in-js%20%E6%9D%80%E9%B8%A1%E7%94%A8%E7%89%9B%E5%88%80%E3%80%8B.md">27.精读《css-in-js 杀鸡用牛刀》</a>
|
||||
- <a href="./前沿技术/28.%E7%B2%BE%E8%AF%BB%E3%80%8A2017%20%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%A4%87%E5%BF%98%E5%BD%95%E3%80%8B.md">28.精读《2017 前端性能优化备忘录》</a>
|
||||
- <a href="./前沿技术/29.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E4%B8%AD%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E3%80%8B.md">29.精读《JS 中的内存管理》</a>
|
||||
- <a href="./前沿技术/30.%E7%B2%BE%E8%AF%BB%E3%80%8AJavascript%20%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E4%B8%8E%E5%BC%82%E6%AD%A5%E3%80%8B.md">30.精读《Javascript 事件循环与异步》</a>
|
||||
- <a href="./前沿技术/31.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E4%B8%8D%E5%86%8D%E4%BD%BF%E7%94%A8%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">31.精读《我不再使用高阶组件》</a>
|
||||
- <a href="./前沿技术/32.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router4.0%20%E8%BF%9B%E9%98%B6%E6%A6%82%E5%BF%B5%E3%80%8B.md">32.精读《React Router4.0 进阶概念》</a>
|
||||
- <a href="./前沿技术/33.%E7%B2%BE%E8%AF%BB%E3%80%8A30%20%E8%A1%8C%20js%20%E4%BB%A3%E7%A0%81%E5%88%9B%E5%BB%BA%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E3%80%8B.md">33.精读《30 行 js 代码创建神经网络》</a>
|
||||
- <a href="./前沿技术/34.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93%E3%80%8B.md">34.精读《React 代码整洁之道》</a>
|
||||
- <a href="./前沿技术/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md">35.精读《dob - 框架实现》</a>
|
||||
- <a href="./前沿技术/36.%E7%B2%BE%E8%AF%BB%E3%80%8AWhen%20You%20%E2%80%9CGit%E2%80%9D%20in%20Trouble-%20a%20Version%20Control%20Story%E3%80%8B.md">36.精读《When You “Git” in Trouble- a Version Control Story》</a>
|
||||
- <a href="./前沿技术/37.%E7%B2%BE%E8%AF%BB%E3%80%8Ahow%20we%20position%20and%20what%20we%20compare%E3%80%8B.md">37.精读《how we position and what we compare》</a>
|
||||
- <a href="./前沿技术/38.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E4%BD%BF%E7%94%A8%E3%80%8B.md">38.精读《dob - 框架使用》</a>
|
||||
- <a href="./前沿技术/39.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A8%E9%93%BE%E8%B7%AF%E4%BD%93%E9%AA%8C%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8C%96%E7%9F%BF%E3%80%8B.md">39.精读《全链路体验浏览器挖矿》</a>
|
||||
- <a href="./前沿技术/40.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%9D%E6%8E%A2%20Reason%20%E4%B8%8E%20GraphQL%E3%80%8B.md">40.精读《初探 Reason 与 GraphQL》</a>
|
||||
- <a href="./前沿技术/41.%E7%B2%BE%E8%AF%BB%E3%80%8AAnt%20Design%203.0%20%E8%83%8C%E5%90%8E%E7%9A%84%E6%95%85%E4%BA%8B%E3%80%8B.md">41.精读《Ant Design 3.0 背后的故事》</a>
|
||||
- <a href="./前沿技术/42.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81%E5%93%B2%E5%AD%A6%E3%80%8B.md">42.精读《前端数据流哲学》</a>
|
||||
- <a href="./前沿技术/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md">43.精读《增强现实与可视化》</a>
|
||||
- <a href="./前沿技术/44.%E7%B2%BE%E8%AF%BB%E3%80%8ARekit%20Studio%E3%80%8B.md">44.精读《Rekit Studio》</a>
|
||||
- <a href="./前沿技术/45.%E7%B2%BE%E8%AF%BB%E3%80%8AReact's%20new%20Context%20API%E3%80%8B.md">45.精读《React's new Context API》</a>
|
||||
- <a href="./前沿技术/46.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-rxjs%E3%80%8B.md">46.精读《react-rxjs》</a>
|
||||
- <a href="./前沿技术/47.%E7%B2%BE%E8%AF%BB%E3%80%8Awebpack4.0%20%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97%E3%80%8B.md">47.精读《webpack4.0 升级指南》</a>
|
||||
- <a href="./前沿技术/49.%E7%B2%BE%E8%AF%BB%E3%80%8ACompilers%20are%20the%20New%20Frameworks%E3%80%8B.md">49.精读《Compilers are the New Frameworks》</a>
|
||||
- <a href="./前沿技术/50.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E6%9E%84%E5%BB%BA%20ARKit%20%E5%BA%94%E7%94%A8%E3%80%8B.md">50.精读《快速上手构建 ARKit 应用》</a>
|
||||
- <a href="./前沿技术/51.%E7%B2%BE%E8%AF%BB%E3%80%8AElements%20of%20Web%20Dev%E3%80%8B.md">51.精读《Elements of Web Dev》</a>
|
||||
- <a href="./前沿技术/52.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BE%E8%A7%A3%20ES%20%E6%A8%A1%E5%9D%97%E3%80%8B.md">52.精读《图解 ES 模块》</a>
|
||||
- <a href="./前沿技术/53.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md">53.精读《插件化思维》</a>
|
||||
- <a href="./前沿技术/54.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E8%BF%90%E8%A1%8C%20serverRender%E3%80%8B.md">54.精读《在浏览器运行 serverRender》</a>
|
||||
- <a href="./前沿技术/55.%E7%B2%BE%E8%AF%BB%E3%80%8Aasync%20await%20%E6%98%AF%E6%8A%8A%E5%8F%8C%E5%88%83%E5%89%91%E3%80%8B.md">55.精读《async await 是把双刃剑》</a>
|
||||
- <a href="./前沿技术/56.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%20Redux%E3%80%8B.md">56.精读《重新思考 Redux》</a>
|
||||
- <a href="./前沿技术/57.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20js%20%E6%A1%86%E6%9E%B6%E5%AD%98%E5%9C%A8%E7%9A%84%E6%A0%B9%E6%9C%AC%E5%8E%9F%E5%9B%A0%E3%80%8B.md">57.精读《现代 js 框架存在的根本原因》</a>
|
||||
- <a href="./前沿技术/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md">58.精读《Typescript2.0 - 2.9》</a>
|
||||
- <a href="./前沿技术/59.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20Nodejs%20%E7%9B%91%E5%90%AC%E6%96%87%E4%BB%B6%E5%A4%B9%E3%80%8B.md">59.精读《如何利用 Nodejs 监听文件夹》</a>
|
||||
- <a href="./前沿技术/60.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%9C%A8%20nodejs%20%E4%BD%BF%E7%94%A8%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E3%80%8B.md">60.精读《如何在 nodejs 使用环境变量》</a>
|
||||
- <a href="./前沿技术/61.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E5%85%AB%E7%A7%8D%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93%E3%80%8B.md">61.精读《React 八种条件渲染》</a>
|
||||
- <a href="./前沿技术/62.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E5%BC%95%E6%93%8E%E5%9F%BA%E7%A1%80%E4%B9%8B%20Shapes%20and%20Inline%20Caches%E3%80%8B.md">62.精读《JS 引擎基础之 Shapes and Inline Caches》</a>
|
||||
- <a href="./前沿技术/63.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E7%9A%84%E5%A4%9A%E6%80%81%E6%80%A7%E3%80%8B.md">63.精读《React 的多态性》</a>
|
||||
- <a href="./前沿技术/68.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%A1%A1%E9%87%8F%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E3%80%8B.md">68.精读《衡量用户体验》</a>
|
||||
- <a href="./前沿技术/69.%E7%B2%BE%E8%AF%BB%E3%80%8ASQL%20vs%20Flux%E3%80%8B.md">69.精读《SQL vs Flux》</a>
|
||||
- <a href="./前沿技术/72.%E7%B2%BE%E8%AF%BB%E3%80%8AREST%2C%20GraphQL%2C%20Webhooks%2C%20%26%20gRPC%20%E5%A6%82%E4%BD%95%E9%80%89%E5%9E%8B%E3%80%8B.md">72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》</a>
|
||||
- <a href="./前沿技术/74.%E7%B2%BE%E8%AF%BB%E3%80%8A12%20%E4%B8%AA%E8%AF%84%E4%BC%B0%20JS%20%E5%BA%93%E4%BD%A0%E9%9C%80%E8%A6%81%E5%85%B3%E5%BF%83%E7%9A%84%E4%BA%8B%E3%80%8B.md">74.精读《12 个评估 JS 库你需要关心的事》</a>
|
||||
- <a href="./前沿技术/76.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%B0%88%E8%B0%88%20Web%20Workers%E3%80%8B.md">76.精读《谈谈 Web Workers》</a>
|
||||
- <a href="./前沿技术/77.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Reduce%20%E5%AE%9E%E7%8E%B0%20Promise%20%E4%B8%B2%E8%A1%8C%E6%89%A7%E8%A1%8C%E3%80%8B.md">77.精读《用 Reduce 实现 Promise 串行执行》</a>
|
||||
- <a href="./前沿技术/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md">79.精读《React Hooks》</a>
|
||||
- <a href="./前沿技术/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md">80.精读《怎么用 React Hooks 造轮子》</a>
|
||||
- <a href="./前沿技术/81.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20CSS%20%E5%B1%9E%E6%80%A7%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">81.精读《使用 CSS 属性选择器》</a>
|
||||
- <a href="./前沿技术/83.%E7%B2%BE%E8%AF%BB%E3%80%8AReact16%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">83.精读《React16 新特性》</a>
|
||||
- <a href="./前沿技术/84.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">84.精读《Typescript 3.2 新特性》</a>
|
||||
- <a href="./前沿技术/86.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BD%E9%99%85%E5%8C%96%E5%B8%83%E5%B1%80%20-%20Logical%20Properties%E3%80%8B.md">86.精读《国际化布局 - Logical Properties》</a>
|
||||
- <a href="./前沿技术/87.%E7%B2%BE%E8%AF%BB%E3%80%8AsetState%20%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">87.精读《setState 做了什么》</a>
|
||||
- <a href="./前沿技术/88.%E7%B2%BE%E8%AF%BB%E3%80%8ACaches%20API%E3%80%8B.md">88.精读《Caches API》</a>
|
||||
- <a href="./前沿技术/89.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E7%BC%96%E8%AF%91%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE%E4%B8%8E%E7%BB%84%E4%BB%B6%E3%80%8B.md">89.精读《如何编译前端项目与组件》</a>
|
||||
- <a href="./前沿技术/91.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E5%88%99%20ES2018%E3%80%8B.md">91.精读《正则 ES2018》</a>
|
||||
- <a href="./前沿技术/94.%E7%B2%BE%E8%AF%BB%E3%80%8AServerless%20%E7%BB%99%E5%89%8D%E7%AB%AF%E5%B8%A6%E6%9D%A5%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">94.精读《Serverless 给前端带来了什么》</a>
|
||||
- <a href="./前沿技术/95.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20VS%20Class%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">95.精读《Function VS Class 组件》</a>
|
||||
- <a href="./前沿技术/96.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md">96.精读《useEffect 完全指南》</a>
|
||||
- <a href="./前沿技术/97.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BC%96%E5%86%99%E6%9C%89%E5%BC%B9%E6%80%A7%E7%9A%84%E7%BB%84%E4%BB%B6%E3%80%8B.md">97.精读《编写有弹性的组件》</a>
|
||||
- <a href="./前沿技术/99.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md">99.精读《Scheduling in React》</a>
|
||||
- <a href="./前沿技术/100.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%20Lazy%20Parsing%E3%80%8B.md">100.精读《V8 引擎 Lazy Parsing》</a>
|
||||
- <a href="./前沿技术/101.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90%20vs%20%E6%8C%81%E7%BB%AD%E4%BA%A4%E4%BB%98%20vs%20%E6%8C%81%E7%BB%AD%E9%83%A8%E7%BD%B2%E3%80%8B.md">101.精读《持续集成 vs 持续交付 vs 持续部署》</a>
|
||||
- <a href="./前沿技术/102.%E7%B2%BE%E8%AF%BB%E3%80%8AMonorepo%20%E7%9A%84%E4%BC%98%E5%8A%BF%E3%80%8B.md">102.精读《Monorepo 的优势》</a>
|
||||
- <a href="./前沿技术/104.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20Component%20%E5%85%A5%E9%97%A8%E3%80%8B.md">104.精读《Function Component 入门》</a>
|
||||
- <a href="./前沿技术/105.%E7%B2%BE%E8%AF%BB%E3%80%8AWhat's%20new%20in%20javascript%E3%80%8B.md">105.精读《What's new in javascript》</a>
|
||||
- <a href="./前沿技术/107.%E7%B2%BE%E8%AF%BB%E3%80%8AOptional%20chaining%E3%80%8B.md">107.精读《Optional chaining》</a>
|
||||
- <a href="./前沿技术/109.%E7%B2%BE%E8%AF%BB%E3%80%8AVue3.0%20Function%20API%E3%80%8B.md">109.精读《Vue3.0 Function API》</a>
|
||||
- <a href="./前沿技术/111.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%E3%80%8B.md">111.精读《前端未来展望》</a>
|
||||
- <a href="./前沿技术/112.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E3%80%8B.md">112.精读《源码学习》</a>
|
||||
- <a href="./前沿技术/113.%E7%B2%BE%E8%AF%BB%E3%80%8ANodejs%20V12%E3%80%8B.md">113.精读《Nodejs V12》</a>
|
||||
- <a href="./前沿技术/117.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E6%8E%A2%E7%B4%A2%E5%BC%8F%E6%A8%A1%E5%9E%8B%E3%80%8B.md">117.精读《Tableau 探索式模型》</a>
|
||||
- <a href="./前沿技术/118.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20css%20%E5%8F%98%E9%87%8F%E7%94%9F%E6%88%90%E9%A2%9C%E8%89%B2%E4%B8%BB%E9%A2%98%E3%80%8B.md">118.精读《使用 css 变量生成颜色主题》</a>
|
||||
- <a href="./前沿技术/119.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%B7%B1%E6%B0%B4%E5%8C%BA%E3%80%8B.md">119.精读《前端深水区》</a>
|
||||
- <a href="./前沿技术/120.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">120.精读《React Hooks 最佳实践》</a>
|
||||
- <a href="./前沿技术/121.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E4%B8%8E%20BI%E3%80%8B.md">121.精读《前端与 BI》</a>
|
||||
- <a href="./前沿技术/123.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Babel%20%E5%88%9B%E9%80%A0%E8%87%AA%E5%AE%9A%E4%B9%89%20JS%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">123.精读《用 Babel 创造自定义 JS 语法》</a>
|
||||
- <a href="./前沿技术/124.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20css%20grid%20%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%E5%B8%83%E5%B1%80%E3%80%8B.md">124.精读《用 css grid 重新思考布局》</a>
|
||||
- <a href="./前沿技术/125.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%20-%20%E5%87%BD%E6%95%B0%E5%BC%8F%E4%B9%8B%E7%BE%8E%E3%80%8B.md">125.精读《深度学习 - 函数式之美》</a>
|
||||
- <a href="./前沿技术/126.%E7%B2%BE%E8%AF%BB%E3%80%8ANuxtjs%E3%80%8B.md">126.精读《Nuxtjs》</a>
|
||||
- <a href="./前沿技术/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md">127.精读《React Conf 2019 - Day1》</a>
|
||||
- <a href="./前沿技术/129.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day2%E3%80%8B.md">129.精读《React Conf 2019 - Day2》</a>
|
||||
- <a href="./前沿技术/132.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E4%BA%A4%E7%9A%84%20React%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">132.精读《正交的 React 组件》</a>
|
||||
- <a href="./前沿技术/133.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%BB%E6%89%BE%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%B9%B3%E8%A1%A1%E7%82%B9%E3%80%8B.md">133.精读《寻找框架设计的平衡点》</a>
|
||||
- <a href="./前沿技术/134.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E5%9C%A8%E9%98%BF%E9%87%8C%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%8F%B0%E5%A4%A7%E5%89%8D%E7%AB%AF%E3%80%8B.md">134.精读《我在阿里数据中台大前端》</a>
|
||||
- <a href="./前沿技术/138.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%B2%BE%E9%80%9A%20console.log%E3%80%8B.md">138.精读《精通 console.log》</a>
|
||||
- <a href="./前沿技术/139.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20JSON%20Parser%E3%80%8B.md">139.精读《手写 JSON Parser》</a>
|
||||
- <a href="./前沿技术/140.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BB%93%E5%90%88%20React%20%E4%BD%BF%E7%94%A8%E5%8E%9F%E7%94%9F%20Drag%20Drop%20API%E3%80%8B.md">140.精读《结合 React 使用原生 Drag Drop API》</a>
|
||||
- <a href="./前沿技术/141.%E7%B2%BE%E8%AF%BB%E3%80%8AuseRef%20%E4%B8%8E%20createRef%20%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">141.精读《useRef 与 createRef 的区别》</a>
|
||||
- <a href="./前沿技术/142.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%81%9A%E5%A5%BD%20CodeReview%E3%80%8B.md">142.精读《如何做好 CodeReview》</a>
|
||||
- <a href="./前沿技术/143.%E7%B2%BE%E8%AF%BB%E3%80%8ASuspense%20%E6%94%B9%E5%8F%98%E5%BC%80%E5%8F%91%E6%96%B9%E5%BC%8F%E3%80%8B.md">143.精读《Suspense 改变开发方式》</a>
|
||||
- <a href="./前沿技术/144.%E7%B2%BE%E8%AF%BB%E3%80%8AWebpack5%20%E6%96%B0%E7%89%B9%E6%80%A7%20-%20%E6%A8%A1%E5%9D%97%E8%81%94%E9%82%A6%E3%80%8B.md">144.精读《Webpack5 新特性 - 模块联邦》</a>
|
||||
- <a href="./前沿技术/145.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router%20v6%E3%80%8B.md">145.精读《React Router v6》</a>
|
||||
- <a href="./前沿技术/146.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E3%80%8B.md">146.精读《React Hooks 数据流》</a>
|
||||
- <a href="./前沿技术/147.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40types%20react%20%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%20TS%20%E6%8A%80%E5%B7%A7%E3%80%8B.md">147. 精读《@types react 值得注意的 TS 技巧》</a>
|
||||
- <a href="./前沿技术/148.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Error%20Boundaries%E3%80%8B.md">148. 精读《React Error Boundaries》</a>
|
||||
- <a href="./前沿技术/149.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E6%80%A7%E8%83%BD%E8%B0%83%E8%AF%95%E3%80%8B.md">149. 精读《React 性能调试》</a>
|
||||
- <a href="./前沿技术/150.%20%E7%B2%BE%E8%AF%BB%E3%80%8ADeno%201.0%20%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E3%80%8B.md">150. 精读《Deno 1.0 你需要了解的》</a>
|
||||
- <a href="./前沿技术/152.%20%E7%B2%BE%E8%AF%BB%E3%80%8Arecoil%E3%80%8B.md">152. 精读《recoil》</a>
|
||||
- <a href="./前沿技术/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md">153. 精读《snowpack》</a>
|
||||
- <a href="./前沿技术/154.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20React%20%E5%81%9A%E6%8C%89%E9%9C%80%E6%B8%B2%E6%9F%93%E3%80%8B.md">154. 精读《用 React 做按需渲染》</a>
|
||||
- <a href="./前沿技术/157.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E6%AF%94%E8%BE%83%20Object%20%E5%AF%B9%E8%B1%A1%E3%80%8B.md">157. 精读《如何比较 Object 对象》</a>
|
||||
- <a href="./前沿技术/158.%20%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204%E3%80%8B.md">158. 精读《Typescript 4》</a>
|
||||
- <a href="./前沿技术/159.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E4%BD%8E%E4%BB%A3%E7%A0%81%E6%90%AD%E5%BB%BA%E7%9A%84%E7%90%86%E8%A7%A3%E3%80%8B.md">159. 精读《对低代码搭建的理解》</a>
|
||||
- <a href="./前沿技术/160.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%87%BD%E6%95%B0%E7%BC%93%E5%AD%98%E3%80%8B.md">160. 精读《函数缓存》</a>
|
||||
- <a href="./前沿技术/161.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E6%80%9D%E8%80%83%20-%20%E5%AF%8C%E6%96%87%E6%9C%AC%E6%90%AD%E5%BB%BA%E3%80%8B.md">161.精读《可视化搭建思考 - 富文本搭建》</a>
|
||||
- <a href="./前沿技术/162.%E7%B2%BE%E8%AF%BB%E3%80%8ATasks%2C%20microtasks%2C%20queues%20and%20schedules%E3%80%8B.md">162.精读《Tasks, microtasks, queues and schedules》</a>
|
||||
- <a href="./前沿技术/163.%E7%B2%BE%E8%AF%BB%E3%80%8ASpring%20%E6%A6%82%E5%BF%B5%E3%80%8B.md">163.精读《Spring 概念》</a>
|
||||
- <a href="./前沿技术/164.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E8%AE%BE%E8%AE%A1%E5%99%A8%E3%80%8B.md">164.精读《数据搭建引擎 bi-designer API-设计器》</a>
|
||||
- <a href="./前沿技术/165.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E7%BB%84%E4%BB%B6%E3%80%8B.md">165.精读《数据搭建引擎 bi-designer API-组件》</a>
|
||||
- <a href="./前沿技术/166.%E7%B2%BE%E8%AF%BB%E3%80%8ABI%20%E6%90%AD%E5%BB%BA%20-%20%E7%AD%9B%E9%80%89%E6%9D%A1%E4%BB%B6%E3%80%8B.md">166.精读《BI 搭建 - 筛选条件》</a>
|
||||
- <a href="./前沿技术/190.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3%E3%80%8B.md">190.精读《DOM diff 原理详解》</a>
|
||||
- <a href="./前沿技术/191.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BD%E8%A1%A8%E6%A0%BC%E3%80%8B.md">191.精读《高性能表格》</a>
|
||||
- <a href="./前沿技术/192.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E6%9C%80%E9%95%BF%E4%B8%8A%E5%8D%87%E5%AD%90%E5%BA%8F%E5%88%97%E3%80%8B.md">192.精读《DOM diff 最长上升子序列》</a>
|
||||
- <a href="./前沿技术/193.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Server%20Component%E3%80%8B.md">193.精读《React Server Component》</a>
|
||||
- <a href="./前沿技术/194.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E3%80%8B.md">194.精读《算法基础数据结构》</a>
|
||||
- <a href="./前沿技术/195.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%96%B0%E4%B8%80%E4%BB%A3%E5%89%8D%E7%AB%AF%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7%E5%AF%B9%E6%AF%94%E3%80%8B.md">195.精读《新一代前端构建工具对比》</a>
|
||||
- <a href="./前沿技术/196.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%81%8C%E4%B8%9A%E8%A7%84%E5%88%92%20-%202021%20%E5%B9%B4%E3%80%8B.md">196.精读《前端职业规划 - 2021 年》</a>
|
||||
- <a href="./前沿技术/197.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%8E%E4%BB%A3%E7%A0%81%E9%80%BB%E8%BE%91%E7%BC%96%E6%8E%92%E3%80%8B.md">197.精读《低代码逻辑编排》</a>
|
||||
- <a href="./前沿技术/202.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%2018%E3%80%8B.md">202.精读《React 18》</a>
|
||||
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
|
||||
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
|
||||
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
|
||||
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
|
||||
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
- <a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
|
||||
- <a href="./前沿技术/210.%E7%B2%BE%E8%AF%BB%E3%80%8Aclass%20static%20block%E3%80%8B.md">210.精读《class static block》</a>
|
||||
- <a href="./前沿技术/211.%E7%B2%BE%E8%AF%BB%E3%80%8AMicrosoft%20Power%20Fx%E3%80%8B.md">211.精读《Microsoft Power Fx》</a>
|
||||
- <a href="./前沿技术/212.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E7%BB%B4%E6%8A%A4%E6%80%A7%E6%80%9D%E8%80%83%E3%80%8B.md">212.精读《可维护性思考》</a>
|
||||
- <a href="./前沿技术/213.%E7%B2%BE%E8%AF%BB%E3%80%8APrisma%20%E7%9A%84%E4%BD%BF%E7%94%A8%E3%80%8B.md">213.精读《Prisma 的使用》</a>
|
||||
- <a href="./前沿技术/214.%E7%B2%BE%E8%AF%BB%E3%80%8Aweb%20streams%E3%80%8B.md">214.精读《web streams》</a>
|
||||
- <a href="./前沿技术/215.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E3%80%8B.md">215.精读《什么是 LOD 表达式》</a>
|
||||
- <a href="./前沿技术/216.%E7%B2%BE%E8%AF%BB%E3%80%8A15%20%E5%A4%A7%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%20-%20%E4%B8%8A%E3%80%8B.md">216.精读《15 大 LOD 表达式 - 上》</a>
|
||||
- <a href="./前沿技术/217.%E7%B2%BE%E8%AF%BB%E3%80%8A15%20%E5%A4%A7%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%20-%20%E4%B8%8B%E3%80%8B.md">217.精读《15 大 LOD 表达式 - 下》</a>
|
||||
- <a href="./前沿技术/218.%E7%B2%BE%E8%AF%BB%E3%80%8ARust%20%E6%98%AF%20JS%20%E5%9F%BA%E5%BB%BA%E7%9A%84%E6%9C%AA%E6%9D%A5%E3%80%8B.md">218.精读《Rust 是 JS 基建的未来》</a>
|
||||
- <a href="./前沿技术/219.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%80%E3%80%8B.md">219.精读《深入了解现代浏览器一》</a>
|
||||
- <a href="./前沿技术/220.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BA%8C%E3%80%8B.md">220.精读《深入了解现代浏览器二》</a>
|
||||
- <a href="./前沿技术/221.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%89%E3%80%8B.md">221.精读《深入了解现代浏览器三》</a>
|
||||
- <a href="./前沿技术/222.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E5%9B%9B%E3%80%8B.md">222.精读《深入了解现代浏览器四》</a>
|
||||
- <a href="./前沿技术/223.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20%E6%8F%90%E6%A1%88%E3%80%8B.md">223.精读《Records & Tuples 提案》</a>
|
||||
- <a href="./前沿技术/224.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20for%20React%E3%80%8B.md">224.精读《Records & Tuples for React》</a>
|
||||
- <a href="./前沿技术/225.%E7%B2%BE%E8%AF%BB%E3%80%8AExcel%20JS%20API%E3%80%8B.md">225.精读《Excel JS API》</a>
|
||||
- <a href="./前沿技术/226.%E7%B2%BE%E8%AF%BB%E3%80%8A2021%20%E5%89%8D%E7%AB%AF%E6%96%B0%E7%A7%80%E5%9B%9E%E9%A1%BE%E3%80%8B.md">226.精读《2021 前端新秀回顾》</a>
|
||||
- <a href="./前沿技术/228.%E7%B2%BE%E8%AF%BB%E3%80%8Apipe%20operator%20for%20JavaScript%E3%80%8B.md">228.精读《pipe operator for JavaScript》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
- <a href="./设计模式/167.精读《设计模式 - Abstract Factory 抽象工厂》.md">167.精读《设计模式 - Abstract Factory 抽象工厂》</a>
|
||||
- <a href="./设计模式/168.精读《设计模式 - Builder 生成器》.md">168.精读《设计模式 - Builder 生成器》</a>
|
||||
- <a href="./设计模式/169.精读《设计模式 - Factory Method 工厂方法》.md">169.精读《设计模式 - Factory Method 工厂方法》</a>
|
||||
- <a href="./设计模式/170.精读《设计模式 - Prototype 原型模式》.md">170.精读《设计模式 - Prototype 原型模式》</a>
|
||||
- <a href="./设计模式/171.精读《设计模式 - Singleton 单例模式》.md">171.精读《设计模式 - Singleton 单例模式》</a>
|
||||
- <a href="./设计模式/172.精读《设计模式 - Adapter 适配器模式》.md">172.精读《设计模式 - Adapter 适配器模式》</a>
|
||||
- <a href="./设计模式/173.精读《设计模式 - Bridge 桥接模式》.md">173.精读《设计模式 - Bridge 桥接模式》</a>
|
||||
- <a href="./设计模式/174.精读《设计模式 - Composite 组合模式》.md">174.精读《设计模式 - Composite 组合模式》</a>
|
||||
- <a href="./设计模式/175.精读《设计模式 - Decorator 装饰器模式》.md">175.精读《设计模式 - Decorator 装饰器模式》</a>
|
||||
- <a href="./设计模式/176.精读《设计模式 - Facade 外观模式》.md">176.精读《设计模式 - Facade 外观模式》</a>
|
||||
- <a href="./设计模式/177.精读《设计模式 - Flyweight 享元模式》.md">177.精读《设计模式 - Flyweight 享元模式》</a>
|
||||
- <a href="./设计模式/178.精读《设计模式 - Proxy 代理模式》.md">178.精读《设计模式 - Proxy 代理模式》</a>
|
||||
- <a href="./设计模式/179.精读《设计模式 - Chain of Responsibility 职责链模式》.md">179.精读《设计模式 - Chain of Responsibility 职责链模式》</a>
|
||||
- <a href="./设计模式/180.精读《设计模式 - Command 命令模式》.md">180.精读《设计模式 - Command 命令模式》</a>
|
||||
- <a href="./设计模式/181.精读《设计模式 - Interpreter 解释器模式》.md">181.精读《设计模式 - Interpreter 解释器模式》</a>
|
||||
- <a href="./设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md">182.精读《设计模式 - Iterator 迭代器模式》</a>
|
||||
- <a href="./设计模式/183.精读《设计模式 - Mediator 中介者模式》.md">183.精读《设计模式 - Mediator 中介者模式》</a>
|
||||
- <a href="./设计模式/184.精读《设计模式 - Memoto 备忘录模式》.md">184.精读《设计模式 - Memoto 备忘录模式》</a>
|
||||
- <a href="./设计模式/185.精读《设计模式 - Observer 观察者模式》.md">185.精读《设计模式 - Observer 观察者模式》</a>
|
||||
- <a href="./设计模式/186.精读《设计模式 - State 状态模式》.md">186.精读《设计模式 - State 状态模式》</a>
|
||||
- <a href="./设计模式/187.精读《设计模式 - Strategy 策略模式》.md">187.精读《设计模式 - Strategy 策略模式》</a>
|
||||
- <a href="./设计模式/188.精读《设计模式 - Template Method 模版模式》.md">188.精读《设计模式 - Template Method 模版模式》</a>
|
||||
- <a href="./设计模式/189.精读《设计模式 - Visitor 访问者模式》.md">189.精读《设计模式 - Visitor 访问者模式》</a>
|
||||
- <a href="./设计模式/167.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Abstract%20Factory%20%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E3%80%8B.md">167.精读《设计模式 - Abstract Factory 抽象工厂》</a>
|
||||
- <a href="./设计模式/168.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Builder%20%E7%94%9F%E6%88%90%E5%99%A8%E3%80%8B.md">168.精读《设计模式 - Builder 生成器》</a>
|
||||
- <a href="./设计模式/169.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Factory%20Method%20%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E3%80%8B.md">169.精读《设计模式 - Factory Method 工厂方法》</a>
|
||||
- <a href="./设计模式/170.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Prototype%20%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">170.精读《设计模式 - Prototype 原型模式》</a>
|
||||
- <a href="./设计模式/171.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Singleton%20%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">171.精读《设计模式 - Singleton 单例模式》</a>
|
||||
- <a href="./设计模式/172.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Adapter%20%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">172.精读《设计模式 - Adapter 适配器模式》</a>
|
||||
- <a href="./设计模式/173.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Bridge%20%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">173.精读《设计模式 - Bridge 桥接模式》</a>
|
||||
- <a href="./设计模式/174.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Composite%20%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">174.精读《设计模式 - Composite 组合模式》</a>
|
||||
- <a href="./设计模式/175.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Decorator%20%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">175.精读《设计模式 - Decorator 装饰器模式》</a>
|
||||
- <a href="./设计模式/176.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Facade%20%E5%A4%96%E8%A7%82%E6%A8%A1%E5%BC%8F%E3%80%8B.md">176.精读《设计模式 - Facade 外观模式》</a>
|
||||
- <a href="./设计模式/177.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Flyweight%20%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F%E3%80%8B.md">177.精读《设计模式 - Flyweight 享元模式》</a>
|
||||
- <a href="./设计模式/178.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Proxy%20%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E3%80%8B.md">178.精读《设计模式 - Proxy 代理模式》</a>
|
||||
- <a href="./设计模式/179.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Chain%20of%20Responsibility%20%E8%81%8C%E8%B4%A3%E9%93%BE%E6%A8%A1%E5%BC%8F%E3%80%8B.md">179.精读《设计模式 - Chain of Responsibility 职责链模式》</a>
|
||||
- <a href="./设计模式/180.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Command%20%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F%E3%80%8B.md">180.精读《设计模式 - Command 命令模式》</a>
|
||||
- <a href="./设计模式/181.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Interpreter%20%E8%A7%A3%E9%87%8A%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">181.精读《设计模式 - Interpreter 解释器模式》</a>
|
||||
- <a href="./设计模式/182.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Iterator%20%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">182.精读《设计模式 - Iterator 迭代器模式》</a>
|
||||
- <a href="./设计模式/183.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Mediator%20%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">183.精读《设计模式 - Mediator 中介者模式》</a>
|
||||
- <a href="./设计模式/184.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Memoto%20%E5%A4%87%E5%BF%98%E5%BD%95%E6%A8%A1%E5%BC%8F%E3%80%8B.md">184.精读《设计模式 - Memoto 备忘录模式》</a>
|
||||
- <a href="./设计模式/185.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Observer%20%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">185.精读《设计模式 - Observer 观察者模式》</a>
|
||||
- <a href="./设计模式/186.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20State%20%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E3%80%8B.md">186.精读《设计模式 - State 状态模式》</a>
|
||||
- <a href="./设计模式/187.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Strategy%20%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">187.精读《设计模式 - Strategy 策略模式》</a>
|
||||
- <a href="./设计模式/188.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Template%20Method%20%E6%A8%A1%E7%89%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">188.精读《设计模式 - Template Method 模版模式》</a>
|
||||
- <a href="./设计模式/189.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Visitor%20%E8%AE%BF%E9%97%AE%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">189.精读《设计模式 - Visitor 访问者模式》</a>
|
||||
|
||||
### 编译原理
|
||||
|
||||
- <a href="./编译原理/64.精读《手写 SQL 编译器 - 词法分析》.md">64.精读《手写 SQL 编译器 - 词法分析》</a>
|
||||
- <a href="./编译原理/65.精读《手写 SQL 编译器 - 文法介绍》.md">65.精读《手写 SQL 编译器 - 文法介绍》</a>
|
||||
- <a href="./编译原理/66.精读《手写 SQL 编译器 - 语法分析》.md">66.精读《手写 SQL 编译器 - 语法分析》</a>
|
||||
- <a href="./编译原理/67.精读《手写 SQL 编译器 - 回溯》.md">67.精读《手写 SQL 编译器 - 回溯》</a>
|
||||
- <a href="./编译原理/70.精读《手写 SQL 编译器 - 语法树》.md">70.精读《手写 SQL 编译器 - 语法树》</a>
|
||||
- <a href="./编译原理/71.精读《手写 SQL 编译器 - 错误提示》.md">71.精读《手写 SQL 编译器 - 错误提示》</a>
|
||||
- <a href="./编译原理/78.精读《手写 SQL 编译器 - 性能优化之缓存》.md">78.精读《手写 SQL 编译器 - 性能优化之缓存》</a>
|
||||
- <a href="./编译原理/85.精读《手写 SQL 编译器 - 智能提示》.md">85.精读《手写 SQL 编译器 - 智能提示》</a>
|
||||
- <a href="./编译原理/64.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">64.精读《手写 SQL 编译器 - 词法分析》</a>
|
||||
- <a href="./编译原理/65.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%96%87%E6%B3%95%E4%BB%8B%E7%BB%8D%E3%80%8B.md">65.精读《手写 SQL 编译器 - 文法介绍》</a>
|
||||
- <a href="./编译原理/66.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">66.精读《手写 SQL 编译器 - 语法分析》</a>
|
||||
- <a href="./编译原理/67.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md">67.精读《手写 SQL 编译器 - 回溯》</a>
|
||||
- <a href="./编译原理/70.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E6%A0%91%E3%80%8B.md">70.精读《手写 SQL 编译器 - 语法树》</a>
|
||||
- <a href="./编译原理/71.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E3%80%8B.md">71.精读《手写 SQL 编译器 - 错误提示》</a>
|
||||
- <a href="./编译原理/78.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BC%93%E5%AD%98%E3%80%8B.md">78.精读《手写 SQL 编译器 - 性能优化之缓存》</a>
|
||||
- <a href="./编译原理/85.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md">85.精读《手写 SQL 编译器 - 智能提示》</a>
|
||||
|
||||
### 源码解读
|
||||
|
||||
- <a href="./源码解读/48.精读《Immer.js》源码.md">48.精读《Immer.js》源码</a>
|
||||
- <a href="./源码解读/73.精读《sqorn 源码》.md">73.精读《sqorn 源码》</a>
|
||||
- <a href="./源码解读/75.精读《Epitath 源码 - renderProps 新用法》.md">75.精读《Epitath 源码 - renderProps 新用法》</a>
|
||||
- <a href="./源码解读/82.精读《Htm - Hyperscript 源码》.md">82.精读《Htm - Hyperscript 源码》</a>
|
||||
- <a href="./源码解读/92.精读《React PowerPlug 源码》.md">92.精读《React PowerPlug 源码》</a>
|
||||
- <a href="./源码解读/93.精读《syntax-parser 源码》.md">93.精读《syntax-parser 源码》</a>
|
||||
- <a href="./源码解读/98.精读《react-easy-state 源码》.md">98.精读《react-easy-state 源码》</a>
|
||||
- <a href="./源码解读/110.精读《Inject Instance 源码》.md">110.精读《Inject Instance 源码》</a>
|
||||
- <a href="./源码解读/122.精读《robot 源码 - 有限状态机》.md">122.精读《robot 源码 - 有限状态机》</a>
|
||||
- <a href="./源码解读/128.精读《Hooks 取数 - swr 源码》.md">128.精读《Hooks 取数 - swr 源码》</a>
|
||||
- <a href="./源码解读/130.精读《unstated 与 unstated-next 源码》.md">130.精读《unstated 与 unstated-next 源码》</a>
|
||||
- <a href="./源码解读/151. 精读《@umijs use-request》源码.md">151. 精读《@umijs use-request》源码</a>
|
||||
- <a href="./源码解读/155. 精读《use-what-changed 源码》.md">155. 精读《use-what-changed 源码》</a>
|
||||
- <a href="./源码解读/156. 精读《react-intersection-observer 源码》.md">156. 精读《react-intersection-observer 源码》</a>
|
||||
- <a href="./源码解读/48.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md">48.精读《Immer.js》源码</a>
|
||||
- <a href="./源码解读/73.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md">73.精读《sqorn 源码》</a>
|
||||
- <a href="./源码解读/75.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md">75.精读《Epitath 源码 - renderProps 新用法》</a>
|
||||
- <a href="./源码解读/82.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md">82.精读《Htm - Hyperscript 源码》</a>
|
||||
- <a href="./源码解读/92.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md">92.精读《React PowerPlug 源码》</a>
|
||||
- <a href="./源码解读/93.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md">93.精读《syntax-parser 源码》</a>
|
||||
- <a href="./源码解读/98.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md">98.精读《react-easy-state 源码》</a>
|
||||
- <a href="./源码解读/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md">110.精读《Inject Instance 源码》</a>
|
||||
- <a href="./源码解读/122.%E7%B2%BE%E8%AF%BB%E3%80%8Arobot%20%E6%BA%90%E7%A0%81%20-%20%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA%E3%80%8B.md">122.精读《robot 源码 - 有限状态机》</a>
|
||||
- <a href="./源码解读/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md">128.精读《Hooks 取数 - swr 源码》</a>
|
||||
- <a href="./源码解读/130.%E7%B2%BE%E8%AF%BB%E3%80%8Aunstated%20%E4%B8%8E%20unstated-next%20%E6%BA%90%E7%A0%81%E3%80%8B.md">130.精读《unstated 与 unstated-next 源码》</a>
|
||||
- <a href="./源码解读/151.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40umijs%20use-request%E3%80%8B%E6%BA%90%E7%A0%81.md">151. 精读《@umijs use-request》源码</a>
|
||||
- <a href="./源码解读/155.%20%E7%B2%BE%E8%AF%BB%E3%80%8Ause-what-changed%20%E6%BA%90%E7%A0%81%E3%80%8B.md">155. 精读《use-what-changed 源码》</a>
|
||||
- <a href="./源码解读/156.%20%E7%B2%BE%E8%AF%BB%E3%80%8Areact-intersection-observer%20%E6%BA%90%E7%A0%81%E3%80%8B.md">156. 精读《react-intersection-observer 源码》</a>
|
||||
- <a href="./源码解读/227.%20%E7%B2%BE%E8%AF%BB%E3%80%8Azustand%20%E6%BA%90%E7%A0%81%E3%80%8B.md">227. 精读《zustand 源码》</a>
|
||||
- <a href="./前沿技术/229.%E7%B2%BE%E8%AF%BB%E3%80%8Avue-lit%20%E6%BA%90%E7%A0%81%E3%80%8B.md">229.精读《vue-lit 源码》</a>
|
||||
- <a href="./前沿技术/230.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%20Markdown%20%E7%9A%84%E6%80%9D%E8%80%83%E3%80%8B.md">230.精读《对 Markdown 的思考》</a>
|
||||
|
||||
### 商业思考
|
||||
|
||||
- <a href="./商业思考/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>
|
||||
|
||||
## 关注前端精读微信公众号
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
|
||||
对于 js 模块化,最近出现的 `<script type="module">` 方式,虽然还没有得到浏览器原生支持,但也是我比较看好的未来趋势,这样就连 webpack 的拆包都不需要了,直接把源代码传到服务器,配合 http2.0 完美抛开预编译的枷锁。
|
||||
|
||||
上述三中方案都不依赖预编译,分别实现了 html、css、js 模块化,相信这就是未来。
|
||||
上述三种方案都不依赖预编译,分别实现了 html、css、js 模块化,相信这就是未来。
|
||||
|
||||
### 模块化标准推进速度仍然缓慢
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
## 概述
|
||||
|
||||
原文对于深水区的想法,讲的很清楚,还是建议读者去读一下原文。
|
||||
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
|
||||
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
|
||||
|
||||
- 深水区需要哪些技能
|
||||

|
||||
|
||||
@@ -180,7 +180,7 @@ const dragProps = {
|
||||
```jsx
|
||||
const dropProps = {
|
||||
onDragOver: ev => {
|
||||
// 做一些样式处理,提示用户此时松手会将元素防止在何处
|
||||
// 做一些样式处理,提示用户此时松手会将元素放置在何处
|
||||
},
|
||||
onDrop: ev => {
|
||||
ev.stopPropagation()
|
||||
|
||||
@@ -27,20 +27,12 @@ BI 平台是阿里数据中台团队非常重要的平台级产品,要保证
|
||||
3. 如果切换到 active 后 props 没有变化,也不应该触发重渲染。
|
||||
4. 从 active 切换到 inActive 后不应触发渲染,且立即阻塞后续重渲染。
|
||||
|
||||
目前 Function Component 做不到这一点,我们仍需借助 Class Component 的 `shouldComponentUpdate` 做到这一点,因为 Class Component 阻塞渲染时,会将最新 props 存储下来,而 Function Component 完全没有内部状态,目前还无法胜任这项工作。
|
||||
|
||||
我们可以写一个 `RenderWhenActive` 组件轻松实现此功能:
|
||||
|
||||
```jsx
|
||||
class RenderWhenActive extends React.Component {
|
||||
public shouldComponentUpdate(nextProps) {
|
||||
return nextProps.active;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
const RenderWhenActive = React.memo(({ children }) => children, (prevProps, nextProps) => (
|
||||
!nextProps.active
|
||||
))
|
||||
```
|
||||
|
||||
### 获取组件 active 状态
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
- [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 后再讲一遍。
|
||||
第一篇是通过可视化帮你理解 DOM 事件的文章,UI 很有意思,但 DOM 事件作为前端基础,精读实在不适合拿过来炒冷饭,这个知识点讲一遍就行了,没必要做成 UI 后再讲一遍。
|
||||
|
||||
第二篇是讲一项技术可以让 Node 运行在浏览器的,这确实是一个新技术,但现阶段我们没必要为这项技术找场景,只要知道有这个东西就行了,没必要仔细阅读。第三篇是对 React 的完整教程,非常体系化,但没有新东西,适合前端新人读,所以也不需要看。
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
其实最核心的业务模型天然在后端,这是因为前端只是一个用户与业务系统交互的窗口,没有前端,用户也可以和接口直接交互,只是这么做成本很大,所以为了降低用户上手难度,或者带来更好的用户体验,才需要不断升级 UI 界面,所以 UI 界面和后端往往是多对一的关系,移动端、小程序、网页对应的接口都是一套,目的就是为了方便任何场景用户都能轻松触达业务,所以作为前端,首先要对前端存在的原因有正确的认识。
|
||||
|
||||
注意这里说的是业务模型,没有提到体验深度,如果讲究体验深度,自然只有前端能做到。然而前端本质还是景上添花的部分,因为在任何行业耕耘久了,如果仅仅只考虑前端,那么目标永远是体验度量、研发提效的事情,很少触及到业务层,以至于前端在业务价值的体现不直接,比较难解释体验度量、研发提效与最终业务增长之间的关系。
|
||||
注意这里说的是业务模型,没有提到体验深度,如果讲究体验深度,自然只有前端能做到。然而前端本质还是锦上添花的部分,因为在任何行业耕耘久了,如果仅仅只考虑前端,那么目标永远是体验度量、研发提效的事情,很少触及到业务层,以至于前端在业务价值的体现不直接,比较难解释体验度量、研发提效与最终业务增长之间的关系。
|
||||
|
||||
所以对于有一定工作经验的前端同学,想要更进一步,一定要在业务领域深耕。
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
来自 Wikipedia 的定义:模态框是一个定位于应用视窗定层的元素。它创造了一种模式让自身保持在一个最外层的子视察下显示,并让主视窗失效。用户必须在回到主视窗前在它上面做交互动作。
|
||||
来自 Wikipedia 的定义:模态框是一个定位于应用视窗顶层的元素。它创造了一种模式让自身保持在一个最外层的子视察下显示,并让主视窗失效。用户必须在回到主视窗前在它上面做交互动作。
|
||||
|
||||
**模态框用处**
|
||||
|
||||
@@ -103,7 +103,7 @@ const TdElement = data.map(item => {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码初始化执行了 N 个模态框初始化代码,显然不合适。对于 table 操作列中触发的模态框,所有行都对应一个模态框,通过父级中一个状态变量来控制展示的内容:
|
||||
上面代码初始化执行了 N 个模态框初始化代码,显然不合适。对于 table 操作列中触发的模态框,所有行都复用同一个模态框,通过父级中一个状态变量来控制展示的内容:
|
||||
|
||||
```js
|
||||
class Table extends Component {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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。
|
||||
- <SuspenseList>。
|
||||
|
||||
后两个文档还未放出,所以本文只介绍第一个 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))
|
||||
@@ -0,0 +1,101 @@
|
||||
> PS: 所有没给原文链接的精读都是原创,本篇也是原创。
|
||||
|
||||
前端精读之前写了 23 篇设计模式总结文,再加上 6 种设计原则,开闭、单一职责、依赖倒置、接口分离、迪米特法则、里氏替换原则,基本上对代码的可维护性有了全面深刻的理解。
|
||||
|
||||
但你我在工作中都会不断遇到烂代码,快要无法维护的大型项目,想一想,仅凭设计模式就能解决这些问题吗?为什么不断膨胀的大型项目总是变得越来越难以维护,而复杂度更高的真实世界,但没有人觉得快要崩塌了呢?
|
||||
|
||||
设计模式考虑的是代码之间的关系,设计原则考虑的是模块以及项目间的关系,那是否存在更上层的思考,解决大型项目越来越难维护的问题?
|
||||
|
||||
## 精读
|
||||
|
||||
先考虑一下,为什么真实世界没有可维护性问题?
|
||||
|
||||
### 真实世界为什么没有可维护问题
|
||||
|
||||
这个问题看起来有点傻,因为从来没有人会发出这样的抱怨 “我们的产品、科技、概念太多了,多到我觉得无法在这个世界活下去了”。但是在代码世界,程序员经常会抱怨,项目的概念太多、设计过于复杂,以至于他无法继续再维护下去了,是时候寻找下一份工作了。
|
||||
|
||||
一种显而易见的解释是,生活中,我们都是小角色,活在自己的天空下并不需要触及那么多概念,而程序员在项目中基本扮演了上帝的角色,必须为每一个细节操心。
|
||||
|
||||
但这并不完全解释得通。我们以为自己接触的东西不多,但实际上日常生活的知识太多了,就拿家电来说,每个人都会同时接触几十种家电,大到空调冰箱洗衣机,小到手机牙刷充电器,即便这些产品被大量标准化,但每个产品用起来都有大量细节的区别,但没有一个人觉得学习使用一个新剃须刀是一种负担,也并不觉得一款设计得不好的牙刷,会对整个牙刷行业造成怎样负面的冲击。
|
||||
|
||||
这背后的原因是:拷贝。正因为我们用的每一件东西都是拷贝,所以即使用坏了也不会对其它相同物品产生任何影响。但代码世界则不同,因为代码调用关系的存在,复用的越优雅,破坏力也就越大。一栋大楼断了几块钢筋尚可支撑,但换在代码世界,只要断了一块钢筋,就意味着这栋大楼所有钢筋都断了。这就是程序员最痛恨的问题之一,就是为什么改了一处看似人畜无害的代码,却导致一场故障。
|
||||
|
||||
从这个角度来说,代码世界是无法吸取真实世界经验的。而且代码世界的这种副作用,在商业上是有巨大正向价值的,即软件的边际成本几乎为零,这是实体产品做不到的,因此软件需要付出可维护性代价,似乎是这种极低边际成本的代价。
|
||||
|
||||
虽然通过借鉴真实世界的经验,使自己维护成本变成零是不可能的,但真实世界对软件世界确实有可借鉴之处,下面我们就来探讨几个有意思的点。
|
||||
|
||||
### 真实世界不断屏蔽复杂度
|
||||
|
||||
不知道你会不会有过这样的思考:面试官总是问原理,就是担心我只会用框架,而缺乏基础。但基础是什么呢?懂得 js,java 算是基础吗?也可以说不算,因为这些语言背后的编译原理好像才是基础,编译原理背后还有操作系统,操作系统运行在硬件上,而硬件的原理呢?从 CPU 设计到背后的硅是如何制作的,等等,这样下去,似乎永远也无法掌握原理。
|
||||
|
||||
但当我们从软件推导到硬件时,可以很自然的发现,没有人觉得掌握硅胶的制作过程是一件必须的事,我们可以一直使用硅胶制作的产品,但却可以不用了解硅胶制作的原理。
|
||||
|
||||
真实世界总是不断屏蔽复杂度,作为消费者时,我们面对的商品总是经过精心包装,简单易用的,只有我们工作时,才需要对某个专业领域的原理有所了解。
|
||||
|
||||
这个道理可以迁移到代码世界,即对于一个庞大而复杂的项目,不能指望每位开发者都了解全部原理后才能工作,我们需要在大多数时候把开发者当作消费者来看待,提供精美而稳定的接口。要做到这一点,需要一个类似下图的架构设计:
|
||||
|
||||
<img width=400 src="https://z3.ax1x.com/2021/10/08/5PTFBR.png">
|
||||
|
||||
从图中可以看出,即便是业务层代码,我也不需要关心过于底层的实现,底层的代码就像脚下被压实了的土地,只需要在上面走就行了。
|
||||
|
||||
然而最让人崩溃的是下面的设计:
|
||||
|
||||
<img width=400 src="https://z3.ax1x.com/2021/10/08/5P7Fxg.png">
|
||||
|
||||
为了解决一个问题,需要面对无穷无尽的上下文,这就是维护成本高的最主要原因。
|
||||
|
||||
### 为什么觉得维护成本高
|
||||
|
||||
作为开发者,已经习惯了评价代码维护成本高还是低,今天我们换个视角,想一想为什么你会觉得维护成本高?
|
||||
|
||||
对维护成本的感受不完全是客观的,我画了一个四象限图:
|
||||
|
||||
<img width=300 src="https://z3.ax1x.com/2021/10/09/5iGjZ6.png">
|
||||
|
||||
左边是和人相关部分,包括你对代码的理解能力,以及对项目的熟悉度。
|
||||
|
||||
理解能力越强,越不容易觉得维护成本高;对项目越熟悉,哪怕是屎山代码,也会觉得重构后可维护性并不会提高,因为自己对项目会变得不熟悉。
|
||||
|
||||
右边是和项目相关部分,包括业务本身的复杂度,以及这背后的技术抽象实现的质量。
|
||||
|
||||
业务本身越复杂,维护成本就会越高,因为信息量不可避免的增大了,我们永远不能只盯着 Hello World 的 Demo 研究框架;代码质量体现了技术对业务的抽象,抽象的好,复杂度曲线就会比较贴合业务真实复杂度,抽象的不好,Hello World Demo 也能够新人进来喝一壶。
|
||||
|
||||
在这四个关键词中,业务复杂度是几乎无法改变的,对项目熟悉也需要一个过程,所以重点应该放在理解能力与代码质量两部分。
|
||||
|
||||
无论是个人理解能力,还是代码质量,目标都是帮助我们快速理解项目,也就是说,只要能快速理解技术项目在做什么,我如何快速融入,就会觉得可维护性高,反之则觉得不好维护。
|
||||
|
||||
所以一个简单的项目,或者一个分层合理,文档清晰的大型项目都会让人觉得可维护性好。在这一点上,需要向真实世界学习的经验就是,即便在软件世界,也并不是了解所有原理,所有犄角旮旯的逻辑才表明技高一筹,带着这种思想工作只会让大家陷入无尽的内卷和理解焦虑。我们要给大家思想减负,不需要理解的模块、代码设计,就不要轻易展示出来,将每个模块开发所需了解的最小知识设定好,最大程度减少开发者的理解负担。
|
||||
|
||||
当然要补充一句,这并不意味着局限开发者的成长和学习空间,其它知识随时敞开大门,只是理解它们并不是日常开发所必要的,这些知识形成文档可以用完即弃,不用成为长期记忆。说到这,就引出了真实世界第二个有趣的地方,就是说明书。
|
||||
|
||||
### 真实世界的说明书
|
||||
|
||||
我回头想想也挺不可思议的,无论快递买来任何需要组装的东西,按照说明书的指引最终都可以组装好,而且装好之后就可以把说明书扔了,完全没有认知负担。
|
||||
|
||||
与其说快递包裹的说明书太完善了,不如说说明不完善,不好用的商品根本卖不出去。我们早已习惯极度易用的商品,及其详尽的说明书了,这是商业社会持续发展,长期博弈后的结果,而且会稳定持续下去。试想一下,如果我们参与维护的项目也有精巧的设计,完善的文档,那维护就不是什么问题,按照文档说的一步步来就行了。
|
||||
|
||||
那为什么大部分情况,我们接手的项目就像一个没有说明书的乐高呢?这应该是商品与代码的本质区别了,即商品质量好不好,是由买家用钞票投票的,做得好用,说明书完善的商品才能存活下来,但这背后的技术实现是看不到的,也没有人可以投票,即便技术人员吐槽代码无法维护,但如果项目取得了商业上的成功,也只会越做越大,技术债越滚越多。
|
||||
|
||||
技术项目的买家是程序员,但程序员没有拒签的办法,导致无论项目质量如何都要接受,没有市场机制的作用,就导致了烂代码随处可见。
|
||||
|
||||
要解决这个问题,首先要意识到这个问题,即技术项目质量本质上是无人长期、持续关心的,你可能会说,技术 Leader 会关心呀?但这和业务驱动相比实在是太弱了。产品有用户侧钞票的投票,无论管理者换多少人,还是会从源头持续提供动力,但项目质量总是要反复强调,间歇性整治,并且不同的 Leader 关心程度也不同,因为这背后没有源动力,除非项目质量影响到用户那头的现金供给了,但这种情况发生时,说明项目早已烂透了。
|
||||
|
||||
正是因为技术质量缺乏源动力,或者说源动力传导链路太长,我们才要人为的不断加强重视,重视文档、重视使用体验、重视是否符合设计模式。只有长期主义者才能坚持做代码质量治理,因为坚信总有一天,代码质量会影响到业务发展。
|
||||
|
||||
## 总结
|
||||
|
||||
这次从真实世界借鉴了一些经验到软件世界,我们从借鉴真实世界的屏蔽复杂度,谈到了为什么真实世界的说明书这么好用,但技术项目文档却总是缺胳膊少腿的问题。
|
||||
|
||||
我们总结出的经验是,设计原则与设计模式固然可以提升可维护性,但归根结底还是动力的问题,提升代码质量本身就是一件缺乏动力去做的事,或者长期被认为是重要不紧急的事,往往很难找出理由现在就去做,但没有人觉得不应该做。
|
||||
|
||||
所以想要提升可维护性,找到为什么现在,立刻,马上就要做技术优化的原因,并立即开始优化才是最重要的。
|
||||
|
||||
> 讨论地址是:[精读《可维护性思考》· Issue #359 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/359)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,569 @@
|
||||
ORM(Object relational mappers) 的含义是,将数据模型与 Object 建立强力的映射关系,这样我们对数据的增删改查可以转换为操作 Object(对象)。
|
||||
|
||||
Prisma 是一个现代 Nodejs ORM 库,根据 [Prisma 官方文档](https://www.prisma.io/docs/concepts/overview/what-is-prisma) 可以了解这个库是如何设计与使用的。
|
||||
|
||||
## 概述
|
||||
|
||||
Prisma 提供了大量工具,包括 Prisma Schema、Prisma Client、Prisma Migrate、Prisma CLI、Prisma Studio 等,其中最核心的两个是 Prisma Schema 与 Prisma Client,分别是描述应用数据模型与 Node 操作 API。
|
||||
|
||||
与一般 ORM 完全由 Class 描述数据模型不同,Primsa 采用了一个全新语法 Primsa Schema 描述数据模型,再执行 `prisma generate` 产生一个配置文件存储在 `node_modules/.prisma/client` 中,Node 代码里就可以使用 Prisma Client 对数据增删改查了。
|
||||
|
||||
### Prisma Schema
|
||||
|
||||
Primsa Schema 是在最大程度贴近数据库结构描述的基础上,对关联关系进行了进一步抽象,并且背后维护了与数据模型的对应关系,下图很好的说明了这一点:
|
||||
|
||||
<img width=400 src="https://z3.ax1x.com/2021/10/17/5YwZoF.png">
|
||||
|
||||
可以看到,几乎与数据库的定义一模一样,唯一多出来的 `posts` 与 `author` 其实是弥补了数据库表关联外键中不直观的部分,将这些外键转化为实体对象,让操作时感受不到外键或者多表的存在,在具体操作时再转化为 join 操作。下面是对应的 Prisma Schema:
|
||||
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String? @map("post_content")
|
||||
published Boolean @default(false)
|
||||
author User? @relation(fields: [authorId], references: [id])
|
||||
authorId Int?
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
posts Post[]
|
||||
}
|
||||
```
|
||||
|
||||
`datasource db` 申明了链接数据库信息;`generator client` 申明了使用 Prisma Client 进行客户端操作,也就是说 Prisma Client 其实是可以替换实现的;`model` 是最核心的模型定义。
|
||||
|
||||
在模型定义中,可以通过 `@map` 修改字段名映射、`@@map` 修改表名映射,默认情况下,字段名与 key 名相同:
|
||||
|
||||
```prisma
|
||||
model Comment {
|
||||
title @map("comment_title")
|
||||
|
||||
@@map("comments")
|
||||
}
|
||||
```
|
||||
|
||||
字段由下面四种描述组成:
|
||||
|
||||
- 字段名。
|
||||
- 字段类型。
|
||||
- 可选的类型修饰。
|
||||
- 可选的属性描述。
|
||||
|
||||
```prisma
|
||||
model Tag {
|
||||
name String? @id
|
||||
}
|
||||
```
|
||||
|
||||
在这个描述里,包含字段名 `name`、字段类型 `String`、类型修饰 `?`、属性描述 `@id`。
|
||||
|
||||
#### 字段类型
|
||||
|
||||
字段类型可以是 model,比如关联类型字段场景:
|
||||
|
||||
```prisma
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
// Other fields
|
||||
comments Comment[] // A post can have many comments
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id Int
|
||||
// Other fields
|
||||
Post Post? @relation(fields: [postId], references: [id]) // A comment can have one post
|
||||
postId Int?
|
||||
}
|
||||
```
|
||||
|
||||
关联场景有 1v1, nv1, 1vn, nvn 四种情况,字段类型可以为定义的 model 名称,并使用属性描述 `@relation` 定义关联关系,比如上面的例子,描述了 `Commenct` 与 `Post` 存在 nv1 关系,并且 `Comment.postId` 与 `Post.id` 关联。
|
||||
|
||||
字段类型还可以是底层数据类型,通过 `@db.` 描述,比如:
|
||||
|
||||
```prisma
|
||||
model Post {
|
||||
id @db.TinyInt(1)
|
||||
}
|
||||
```
|
||||
|
||||
对于 Prisma 不支持的类型,还可以使用 `Unsupported` 修饰:
|
||||
|
||||
```prisma
|
||||
model Post {
|
||||
someField Unsupported("polygon")?
|
||||
}
|
||||
```
|
||||
|
||||
这种类型的字段无法通过 ORM API 查询,但可以通过 `queryRaw` 方式查询。`queryRaw` 是一种 ORM 对原始 SQL 模式的支持,在 Prisma Client 会提到。
|
||||
|
||||
#### 类型修饰
|
||||
|
||||
类型修饰有 `?` `[]` 两种语法,比如:
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
name String?
|
||||
posts Post[]
|
||||
}
|
||||
```
|
||||
|
||||
分别表示可选与数组。
|
||||
|
||||
#### 属性描述
|
||||
|
||||
属性描述有如下几种语法:
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
isAdmin Boolean @default(false)
|
||||
email String @unique
|
||||
|
||||
@@unique([firstName, lastName])
|
||||
}
|
||||
```
|
||||
|
||||
`@id` 对应数据库的 PRIMARY KEY。
|
||||
|
||||
`@default` 设置字段默认值,可以联合函数使用,比如 `@default(autoincrement())`,可用函数包括 `autoincrement()`、`dbgenerated()`、`cuid()`、`uuid()`、`now()`,还可以通过 `dbgenerated` 直接调用数据库底层的函数,比如 `dbgenerated("gen_random_uuid()")`。
|
||||
|
||||
`@unique` 设置字段值唯一。
|
||||
|
||||
`@relation` 设置关联,上面已经提到过了。
|
||||
|
||||
`@map` 设置映射,上面也提到过了。
|
||||
|
||||
`@updatedAt` 修饰字段用来存储上次更新时间,一般是数据库自带的能力。
|
||||
|
||||
`@ignore` 对 Prisma 标记无效的字段。
|
||||
|
||||
所有属性描述都可以组合使用,并且还存在需对 model 级别的描述,一般用两个 `@` 描述,包括 `@@id`、`@@unique`、`@@index`、`@@map`、`@@ignore`。
|
||||
|
||||
#### ManyToMany
|
||||
|
||||
Prisma 在多对多关联关系的描述上也下了功夫,支持隐式关联描述:
|
||||
|
||||
```prisma
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
categories Category[]
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
posts Post[]
|
||||
}
|
||||
```
|
||||
|
||||
看上去很自然,但其实背后隐藏了不少实现。数据库多对多关系一般通过第三张表实现,第三张表会存储两张表之间外键对应关系,所以如果要显式定义其实是这样的:
|
||||
|
||||
```prisma
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
categories CategoriesOnPosts[]
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
posts CategoriesOnPosts[]
|
||||
}
|
||||
|
||||
model CategoriesOnPosts {
|
||||
post Post @relation(fields: [postId], references: [id])
|
||||
postId Int // relation scalar field (used in the `@relation` attribute above)
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
categoryId Int // relation scalar field (used in the `@relation` attribute above)
|
||||
assignedAt DateTime @default(now())
|
||||
assignedBy String
|
||||
|
||||
@@id([postId, categoryId])
|
||||
}
|
||||
```
|
||||
|
||||
背后生成如下 SQL:
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Category" (
|
||||
id SERIAL PRIMARY KEY
|
||||
);
|
||||
CREATE TABLE "Post" (
|
||||
id SERIAL PRIMARY KEY
|
||||
);
|
||||
-- Relation table + indexes -------------------------------------------------------
|
||||
CREATE TABLE "CategoryToPost" (
|
||||
"categoryId" integer NOT NULL,
|
||||
"postId" integer NOT NULL,
|
||||
"assignedBy" text NOT NULL
|
||||
"assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("categoryId") REFERENCES "Category"(id),
|
||||
FOREIGN KEY ("postId") REFERENCES "Post"(id)
|
||||
);
|
||||
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);
|
||||
```
|
||||
|
||||
### Prisma Client
|
||||
|
||||
描述好 Prisma Model 后,执行 `prisma generate`,再利用 `npm install @prisma/client` 安装好 Node 包后,就可以在代码里操作 ORM 了:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
```
|
||||
|
||||
#### CRUD
|
||||
|
||||
使用 `create` 创建一条记录:
|
||||
|
||||
```typescript
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: 'elsa@prisma.io',
|
||||
name: 'Elsa Prisma',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `createMany` 创建多条记录:
|
||||
|
||||
```typescript
|
||||
const createMany = await prisma.user.createMany({
|
||||
data: [
|
||||
{ name: 'Bob', email: 'bob@prisma.io' },
|
||||
{ name: 'Bobo', email: 'bob@prisma.io' }, // Duplicate unique key!
|
||||
{ name: 'Yewande', email: 'yewande@prisma.io' },
|
||||
{ name: 'Angelique', email: 'angelique@prisma.io' },
|
||||
],
|
||||
skipDuplicates: true, // Skip 'Bobo'
|
||||
})
|
||||
```
|
||||
|
||||
使用 `findUnique` 查找单条记录:
|
||||
|
||||
```typescript
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: 'elsa@prisma.io',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
对于联合索引的情况:
|
||||
|
||||
```prisma
|
||||
model TimePeriod {
|
||||
year Int
|
||||
quarter Int
|
||||
total Decimal
|
||||
|
||||
@@id([year, quarter])
|
||||
}
|
||||
```
|
||||
|
||||
需要再嵌套一层由 `_` 拼接的 key:
|
||||
|
||||
```typescript
|
||||
const timePeriod = await prisma.timePeriod.findUnique({
|
||||
where: {
|
||||
year_quarter: {
|
||||
quarter: 4,
|
||||
year: 2020,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `findMany` 查询多条记录:
|
||||
|
||||
```typescript
|
||||
const users = await prisma.user.findMany()
|
||||
```
|
||||
|
||||
可以使用 SQL 中各种条件语句,语法如下:
|
||||
|
||||
```typescript
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
role: 'ADMIN',
|
||||
},
|
||||
include: {
|
||||
posts: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `update` 更新记录:
|
||||
|
||||
```typescript
|
||||
const updateUser = await prisma.user.update({
|
||||
where: {
|
||||
email: 'viola@prisma.io',
|
||||
},
|
||||
data: {
|
||||
name: 'Viola the Magnificent',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `updateMany` 更新多条记录:
|
||||
|
||||
```typescript
|
||||
const updateUsers = await prisma.user.updateMany({
|
||||
where: {
|
||||
email: {
|
||||
contains: 'prisma.io',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role: 'ADMIN',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `delete` 删除记录:
|
||||
|
||||
```typescript
|
||||
const deleteUser = await prisma.user.delete({
|
||||
where: {
|
||||
email: 'bert@prisma.io',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `deleteMany` 删除多条记录:
|
||||
|
||||
```typescript
|
||||
const deleteUsers = await prisma.user.deleteMany({
|
||||
where: {
|
||||
email: {
|
||||
contains: 'prisma.io',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
使用 `include` 表示关联查询是否生效,比如:
|
||||
|
||||
```typescript
|
||||
const getUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: 19,
|
||||
},
|
||||
include: {
|
||||
posts: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
这样就会在查询 `user` 表时,顺带查询所有关联的 `post` 表。关联查询也支持嵌套:
|
||||
|
||||
```typescript
|
||||
const user = await prisma.user.findMany({
|
||||
include: {
|
||||
posts: {
|
||||
include: {
|
||||
categories: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
筛选条件支持 `equals`、`not`、`in`、`notIn`、`lt`、`lte`、`gt`、`gte`、`contains`、`search`、`mode`、`startsWith`、`endsWith`、`AND`、`OR`、`NOT`,一般用法如下:
|
||||
|
||||
```typescript
|
||||
const result = await prisma.user.findMany({
|
||||
where: {
|
||||
name: {
|
||||
equals: 'Eleanor',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
这个语句代替 sql 的 `where name="Eleanor"`,即通过对象嵌套的方式表达语义。
|
||||
|
||||
Prisma 也可以直接写原生 SQL:
|
||||
|
||||
```typescript
|
||||
const email = 'emelie@prisma.io'
|
||||
const result = await prisma.$queryRaw(
|
||||
Prisma.sql`SELECT * FROM User WHERE email = ${email}`
|
||||
)
|
||||
```
|
||||
|
||||
### 中间件
|
||||
|
||||
Prisma 支持中间件的方式在执行过程中进行拓展,看下面的例子:
|
||||
|
||||
```typescript
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// Middleware 1
|
||||
prisma.$use(async (params, next) => {
|
||||
console.log(params.args.data.title)
|
||||
console.log('1')
|
||||
const result = await next(params)
|
||||
console.log('6')
|
||||
return result
|
||||
})
|
||||
|
||||
// Middleware 2
|
||||
prisma.$use(async (params, next) => {
|
||||
console.log('2')
|
||||
const result = await next(params)
|
||||
console.log('5')
|
||||
return result
|
||||
})
|
||||
|
||||
// Middleware 3
|
||||
prisma.$use(async (params, next) => {
|
||||
console.log('3')
|
||||
const result = await next(params)
|
||||
console.log('4')
|
||||
return result
|
||||
})
|
||||
|
||||
const create = await prisma.post.create({
|
||||
data: {
|
||||
title: 'Welcome to Prisma Day 2020',
|
||||
},
|
||||
})
|
||||
|
||||
const create2 = await prisma.post.create({
|
||||
data: {
|
||||
title: 'How to Prisma!',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
输出如下:
|
||||
|
||||
```text
|
||||
Welcome to Prisma Day 2020
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
How to Prisma!
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
```
|
||||
|
||||
可以看到,中间件执行顺序是洋葱模型,并且每个操作都会触发。我们可以利用中间件拓展业务逻辑或者进行操作时间的打点记录。
|
||||
|
||||
## 精读
|
||||
|
||||
### ORM 的两种设计模式
|
||||
|
||||
ORM 有 Active Record 与 Data Mapper 两种设计模式,其中 Active Record 使对象背后完全对应 sql 查询,现在已经不怎么流行了,而 Data Mapper 模式中的对象并不知道数据库的存在,即中间多了一层映射,甚至背后不需要对应数据库,所以可以做一些很轻量的调试功能。
|
||||
|
||||
Prisma 采用了 Data Mapper 模式。
|
||||
|
||||
### ORM 容易引发性能问题
|
||||
|
||||
当数据量大,或者性能、资源敏感的情况下,我们需要对 SQL 进行优化,甚至我们需要对特定的 Mysql 的特定版本的某些内核错误,对 SQL 进行某些看似无意义的申明调优(比如在 where 之前再进行相同条件的 IN 范围限定),有的时候能取得惊人的性能提升。
|
||||
|
||||
而 ORM 是建立在一个较为理想化理论基础上的,即数据模型可以很好的转化为对象操作,然而对象操作由于屏蔽了细节,我们无法对 SQL 进行针对性调优。
|
||||
|
||||
另外,得益于对象操作的便利性,我们很容易通过 obj.obj. 的方式访问某些属性,但这背后生成的却是一系列未经优化(或者部分自动优化)的复杂 join sql,我们在写这些 sql 时会提前考虑性能因素,但通过对象调用时却因为成本低,或觉得 ORM 有 magic 优化等想法,写出很多实际上不合理的 sql。
|
||||
|
||||
### Prisma Schema 的好处
|
||||
|
||||
其实从语法上,Prisma Schema 与 Typeorm 基于 Class + 装饰器的拓展几乎可以等价转换,但 Prisma Schema 在实际使用中有一个很不错的优势,即减少样板代码以及稳定数据库模型。
|
||||
|
||||
减少样板代码比较好理解,因为 Prisma Schema 并不会出现在代码中,而稳定模型是指,只要不执行 `prisma generate`,数据模型就不会变化,而且 Prisma Schema 也独立于 Node 存在,甚至可以不放在项目源码中,相比之下,修改起来会更加慎重,而完全用 Node 定义的模型因为本身是代码的一部分,可能会突然被修改,而且也没有执行数据库结构同步的操作。
|
||||
|
||||
如果项目采用 Prisma,则模型变更后,可以执行 `prisma db pull` 更新数据库结构,再执行 `prisma generate` 更新客户端 API,这个流程比较清晰。
|
||||
|
||||
## 总结
|
||||
|
||||
Prisma Schema 是 Prisma 的一大特色,因为这部分描述独立于代码,带来了如下几个好处:
|
||||
|
||||
1. 定义比 Node Class 更简洁。
|
||||
2. 不生成冗余的代码结构。
|
||||
3. Prisma Client 更加轻量,且查询返回的都是 Pure Object。
|
||||
|
||||
至于 Prisma Client 的 API 设计其实并没有特别突出之处,无论与 [sequelize](https://sequelize.org/master/) 还是 [typeorm](https://typeorm.io/#/) 的 API 设计相比,都没有太大的优化,只是风格不同。
|
||||
|
||||
不过对于记录的创建,我更喜欢 Prisma 的 API:
|
||||
|
||||
```typescript
|
||||
// typeorm - save API
|
||||
const userRepository = getManager().getRepository(User)
|
||||
const newUser = new User()
|
||||
newUser.name = 'Alice'
|
||||
userRepository.save(newUser)
|
||||
|
||||
// typeorm - insert API
|
||||
const userRepository = getManager().getRepository(User)
|
||||
userRepository.insert({
|
||||
name: 'Alice',
|
||||
})
|
||||
|
||||
// sequelize
|
||||
const user = User.build({
|
||||
name: 'Alice',
|
||||
})
|
||||
await user.save()
|
||||
|
||||
// Mongoose
|
||||
const user = await User.create({
|
||||
name: 'Alice',
|
||||
email: 'alice@prisma.io',
|
||||
})
|
||||
|
||||
// prisma
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
name: 'Alice',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
首先存在 `prisma` 这个顶层变量,使用起来会非常方便,另外从 API 拓展上来说,虽然 Mongoose 设计得更简洁,但添加一些条件时拓展性会不足,导致结构不太稳定,不利于统一记忆。
|
||||
|
||||
Prisma Client 的 API 统一采用下面这种结构:
|
||||
|
||||
```typescript
|
||||
await prisma.modelName.operateName({
|
||||
// 数据,比如 create、update 时会用到
|
||||
data: /** ... */,
|
||||
// 条件,大部分情况都可以用到
|
||||
where: /** ... */,
|
||||
// 其它特殊参数,或者 operater 特有的参数
|
||||
})
|
||||
```
|
||||
|
||||
所以总的来说,Prisma 虽然没有对 ORM 做出革命性改变,但在微创新与 API 优化上都做得足够棒,github 更新也比较活跃,如果你决定使用 ORM 开发项目,还是比较推荐 Prisma 的。
|
||||
|
||||
在实际使用中,为了规避 ORM 产生笨拙 sql 导致的性能问题,可以利用 Prisma Middleware 监控查询性能,并对性能较差的地方采用 `prisma.$queryRaw` 原生 sql 查询。
|
||||
|
||||
> 讨论地址是:[精读《Prisma 的使用》· Issue #362 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/362)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,300 @@
|
||||
Node stream 比较难理解,也比较难用,但 “流” 是个很重要而且会越来越常见的概念(`fetch` 返回值就是流),所以我们有必要认真学习 stream。
|
||||
|
||||
好在继 node stream 之后,又推出了比较好用,好理解的 web streams API,我们结合 [Web Streams Everywhere (and Fetch for Node.js)](https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/)、[2016 - the year of web streams](https://jakearchibald.com/2016/streams-ftw/)、[ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)、[WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) 这几篇文章学一下。
|
||||
|
||||
> node stream 与 web stream 可以相互转换:`.fromWeb()` 将 web stream 转换为 node stream;`.toWeb()` 将 node stream 转换为 web stream。
|
||||
|
||||
## 精读
|
||||
|
||||
stream(流)是什么?
|
||||
|
||||
stream 是一种抽象 API。我们可以和 promise 做一下类比,如果说 promise 是异步标准 API,则 stream 希望成为 I/O 的标准 API。
|
||||
|
||||
什么是 I/O?就是输入输出,即信息的读取与写入,比如看视频、加载图片、浏览网页、编码解码器等等都属于 I/O 场景,所以并不一定非要大数据量才算 I/O,比如读取一个磁盘文件算 I/O,同样读取 `"hello world"` 字符串也可以算 I/O。
|
||||
|
||||
stream 就是当下对 I/O 的标准抽象。
|
||||
|
||||
为了更好理解 stream 的 API 设计,以及让你理解的更深刻,我们先自己想一想一个标准 I/O API 应该如何设计?
|
||||
|
||||
### I/O 场景应该如何抽象 API?
|
||||
|
||||
`read()`、`write()` 是我们第一个想到的 API,继续补充的话还有 `open()`、`close()` 等等。
|
||||
|
||||
这些 API 确实可以称得上 I/O 场景标准 API,而且也足够简单。但这些 API 有一个不足,就是缺乏对大数据量下读写的优化考虑。什么是大数据量的读写?比如读一个几 GB 的视频文件,在 2G 慢网络环境下访问网页,这些情况下,如果我们只有 `read`、`write` API,那么可能一个读取命令需要 2 个小时才能返回,而一个写入命令需要 3 个小时执行时间,同时对用户来说,不论是看视频还是看网页,都无法接受这么长的白屏时间。
|
||||
|
||||
但为什么我们看视频和看网页的时候没有等待这么久?因为看网页时,并不是等待所有资源都加载完毕才能浏览与交互的,许多资源都是在首屏渲染后再异步加载的,视频更是如此,我们不会加载完 30GB 的电影后再开始播放,而是先下载 300kb 片头后就可以开始播放了。
|
||||
|
||||
无论是视频还是网页,为了快速响应内容,资源都是 **在操作过程中持续加载的**,如果我们设计一个支持这种模式的 API,无论资源大还是小都可以覆盖,自然比 `read`、`wirte` 设计更合理。
|
||||
|
||||
这种持续加载资源的行为就是 stream(流)。
|
||||
|
||||
### 什么是 stream
|
||||
|
||||
stream 可以认为在形容资源持续流动的状态,我们需要把 I/O 场景看作一个持续的场景,就像把一条河的河水导流到另一条河。
|
||||
|
||||
做一个类比,我们在发送 http 请求、浏览网页、看视频时,可以看作一个南水北调的过程,把 A 河的水持续调到 B 河。
|
||||
|
||||
在发送 http 请求时,A 河就是后端服务器,B 河就是客户端;浏览网页时,A 河就是别人的网站,B 河就是你的手机;看视频时,A 河是网络上的视频资源(当然也可能是本地的),B 河是你的视频播放器。
|
||||
|
||||
所以流是一个持续的过程,而且可能有多个节点,不仅网络请求是流,资源加载到本地硬盘后,读取到内存,视频解码也是流,所以这个南水北调过程中还有许多中途蓄水池节点。
|
||||
|
||||
将这些事情都考虑到一起,最后形成了 web stream API。
|
||||
|
||||
一共有三种流,分别是:writable streams、readable streams、transform streams,它们的关系如下:
|
||||
|
||||
<img width=400 src="https://z3.ax1x.com/2021/10/25/55kQtP.png">
|
||||
|
||||
- readable streams 代表 A 河流,是数据的源头,因为是数据源头,所以只可读不可写。
|
||||
- writable streams 代表 B 河流,是数据的目的地,因为要持续蓄水,所以是只可写不可读。
|
||||
- transform streams 是中间对数据进行变换的节点,比如 A 与 B 河中间有一个大坝,这个大坝可以通过蓄水的方式控制水运输的速度,还可以安装滤网净化水源,所以它一头是 writable streams 输入 A 河流的水,另一头提供 readable streams 供 B 河流读取。
|
||||
|
||||
乍一看很复杂的概念,但映射到河水引流就非常自然了,stream 的设计非常贴近生活概念。
|
||||
|
||||
要理解 stream,需要思考下面三个问题:
|
||||
|
||||
1. readable streams 从哪来?
|
||||
2. 是否要使用 transform streams 进行中间件加工?
|
||||
3. 消费的 writable streams 逻辑是什么?
|
||||
|
||||
还是再解释一下,为什么相比 `read()`、`write()`,stream 要多这三个思考:stream 既然将 I/O 抽象为流的概念,也就是具有持续性,那么读取的资源就必须是一个 readable 流,所以我们要构造一个 readable streams(未来可能越来越多函数返回值就是流,也就是在流的环境下工作,就不用考虑如何构造流了)。对流的读取是一个持续的过程,所以不是调用一个函数一次性读取那么简单,因此 writable streams 也有一定 API 语法。正是因为对资源进行了抽象,所以无论是读取还是消费,都被包装了一层 stream API,而普通的 `read` 函数读取的资源都是其本身,所以才没有这些额外思维负担。
|
||||
|
||||
好在 web streams API 设计都比较简单易用,而且作为一种标准规范,更加有掌握的必要,下面分别说明:
|
||||
|
||||
### readable streams
|
||||
|
||||
读取流不可写,所以只有初始化时才能设置值:
|
||||
|
||||
```typescript
|
||||
const readableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('h')
|
||||
controller.enqueue('e')
|
||||
controller.enqueue('l')
|
||||
controller.enqueue('l')
|
||||
controller.enqueue('o')
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`controller.enqueue()` 可以填入任意值,相当于是将值加入队列,`controller.close()` 关闭后,就无法继续 `enqueue` 了,并且这里的关闭时机,会在 writable streams 的 `close` 回调响应。
|
||||
|
||||
上面只是 mock 的例子,实际场景中,读取流往往是一些调用函数返回的对象,最常见的就是 `fetch` 函数:
|
||||
|
||||
```typescript
|
||||
async function fetchStream() {
|
||||
const response = await fetch('https://example.com')
|
||||
const stream = response.body;
|
||||
}
|
||||
```
|
||||
|
||||
可见,`fetch` 函数返回的 `response.body` 就是一个 readable stream。
|
||||
|
||||
我们可以通过以下方式直接消费读取流:
|
||||
|
||||
```typescript
|
||||
readableStream.getReader().read().then({ value, done } => {})
|
||||
```
|
||||
|
||||
也可以 `readableStream.pipeThrough(transformStream)` 到一个转换流,也可以 `readableStream.pipeTo(writableStream)` 到一个写入流。
|
||||
|
||||
不管是手动 mock 还是函数返回,我们都能猜到,**读取流不一定一开始就充满数据**,比如 `response.body` 就可能因为读的比较早而需要等待,就像接入的水管水流较慢,而源头水池的水很多一样。我们也可以手动模拟读取较慢的情况:
|
||||
|
||||
```typescript
|
||||
const readableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('h')
|
||||
controller.enqueue('e')
|
||||
|
||||
setTimeout(() => {
|
||||
controller.enqueue('l')
|
||||
controller.enqueue('l')
|
||||
controller.enqueue('o')
|
||||
controller.close()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
上面例子中,如果我们一开始就用写入流对接,必然要等待 1s 才能得到完整的 `'hello'` 数据,但如果 1s 后再对接写入流,那么瞬间就能读取整个 `'hello'`。另外,写入流可能处理的速度也会慢,如果写入流处理每个单词的时间都是 1s,那么写入流无论何时执行,都比读取流更慢。
|
||||
|
||||
所以可以体会到,流的设计就是为了让整个数据处理过程最大程度的高效,无论读取流数据 ready 的多迟、开始对接写入流的时间有多晚、写入流处理的多慢,整个链路都是尽可能最高效的:
|
||||
|
||||
- 如果 readableStream ready 的迟,我们可以晚一点对接,让 readableStream 准备好再开始快速消费。
|
||||
- 如果 writableStream 处理的慢,也只是这一处消费的慢,对接的 “水管” readableStream 可能早就 ready 了,此时换一个高效消费的 writableStream 就能提升整体效率。
|
||||
|
||||
### writable streams
|
||||
|
||||
写入流不可读,可以通过如下方式创建:
|
||||
|
||||
```typescript
|
||||
const writableStream = new WritableStream({
|
||||
write(chunk) {
|
||||
return new Promise(resolve => {
|
||||
// 消费的地方,可以执行插入 dom 等等操作
|
||||
console.log(chunk)
|
||||
|
||||
resolve()
|
||||
});
|
||||
},
|
||||
close() {
|
||||
// 可读流 controller.close() 时,这里被调用
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
写入流不用关心读取流是什么,所以只要关心数据写入就行了,实现写入回调 `write`。
|
||||
|
||||
`write` 回调需要返回一个 Promise,所以如果我们消费 `chunk` 的速度比较慢,写入流执行速度就会变慢,我们可以理解为 A 河流引水到 B 河流,就算 A 河流的河道很宽,一下就把河水全部灌入了,但 B 河流的河道很窄,无法处理那么大的水流量,所以受限于 B 河流河道宽度,整体水流速度还是比较慢的(当然这里不可能发生洪灾)。
|
||||
|
||||
那么 writableStream 如何触发写入呢?可以通过 `write()` 函数直接写入:
|
||||
|
||||
```typescript
|
||||
writableStream.getWriter().write('h')
|
||||
```
|
||||
|
||||
也可以通过 `pipeTo()` 直接对接 readableStream,就像本来是手动滴水,现在直接对接一个水管,这样我们只管处理写入就行了:
|
||||
|
||||
```typescript
|
||||
readableStream.pipeTo(writableStream)
|
||||
```
|
||||
|
||||
当然通过最原始的 API 也可以拼装出 `pipeTo` 的效果,为了理解的更深刻,我们用原始方法模拟一个 `pipeTo`:
|
||||
|
||||
```typescript
|
||||
const reader = readableStream.getReader()
|
||||
const writer = writableStream.getWriter()
|
||||
|
||||
function tryRead() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
return
|
||||
}
|
||||
|
||||
writer.ready().then(() => writer.write(value))
|
||||
|
||||
tryRead()
|
||||
})
|
||||
}
|
||||
|
||||
tryRead()
|
||||
```
|
||||
|
||||
### transform streams
|
||||
|
||||
转换流内部是一个写入流 + 读取流,创建转换流的方式如下:
|
||||
|
||||
```typescript
|
||||
const decoder = new TextDecoder()
|
||||
const decodeStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(decoder.decode(chunk, {stream: true}))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`chunk` 是 writableStream 拿到的包,`controller.enqueue` 是 readableStream 的入列方法,所以它其实底层实现就是两个流的叠加,API 上简化为 `transform` 了,可以一边写入读到的数据,一边转化为读取流,供后面的写入流消费。
|
||||
|
||||
当然有很多原生的转换流可以用,比如 `TextDecoderStream`:
|
||||
|
||||
```typescript
|
||||
const textDecoderStream = TextDecoderStream()
|
||||
```
|
||||
|
||||
### readable to writable streams
|
||||
|
||||
下面是一个包含了编码转码的完整例子:
|
||||
|
||||
```typescript
|
||||
// 创建读取流
|
||||
const readableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
const textEncoder = new TextEncoder()
|
||||
const chunks = textEncoder.encode('hello', { stream: true })
|
||||
chunks.forEach(chunk => controller.enqueue(chunk))
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
|
||||
// 创建写入流
|
||||
const writableStream = new WritableStream({
|
||||
write(chunk) {
|
||||
const textDecoder = new TextDecoder()
|
||||
return new Promise(resolve => {
|
||||
const buffer = new ArrayBuffer(2);
|
||||
const view = new Uint16Array(buffer);
|
||||
view[0] = chunk;
|
||||
const decoded = textDecoder.decode(view, { stream: true });
|
||||
console.log('decoded', decoded)
|
||||
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 1000)
|
||||
});
|
||||
},
|
||||
close() {
|
||||
console.log('writable stream close')
|
||||
},
|
||||
})
|
||||
|
||||
readableStream.pipeTo(writableStream)
|
||||
```
|
||||
|
||||
首先 readableStream 利用 `TextEncoder` 以极快的速度瞬间将 `hello` 这 5 个字母加入队列,并执行 `controller.close()`,意味着这个 readableStream 瞬间就完成了初始化,并且后面无法修改,只能读取了。
|
||||
|
||||
我们在 writableStream 的 `write` 方法中,利用 `TextDecoder` 对 `chunk` 进行解码,一次解码一个字母,并打印到控制台,然后过了 1s 才 `resolve`,所以写入流会每隔 1s 打印一个字母:
|
||||
|
||||
```shell
|
||||
h
|
||||
# 1s later
|
||||
e
|
||||
# 1s later
|
||||
l
|
||||
# 1s later
|
||||
l
|
||||
# 1s later
|
||||
o
|
||||
writable stream close
|
||||
```
|
||||
|
||||
这个例子转码解码处理的还不够优雅,我们不需要将转码与解码写在流函数里,而是写在转换流中,比如:
|
||||
|
||||
```typescript
|
||||
readableStream
|
||||
.pipeThrough(new TextEncoderStream())
|
||||
.pipeThrough(customStream)
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeTo(writableStream)
|
||||
```
|
||||
|
||||
这样 readableStream 与 writableStream 都不需要处理编码与解码,但流在中间被转化为了 Uint8Array,方便被其它转换流处理,最后经过解码转换流转换为文字后,再 `pipeTo` 给写入流,这样写入流拿到的就是文字了。
|
||||
|
||||
但也并不总是这样,比如我们要传输一个视频流,可能 readableStream 原始值就已经是 Uint8Array,所以具体要不要对接转换流看情况。
|
||||
|
||||
## 总结
|
||||
|
||||
streams 是对 I/O 抽象的标准处理 API,其支持持续小片段数据处理的特性并不是偶然,而是对 I/O 场景进行抽象后的必然。
|
||||
|
||||
我们通过水流的例子类比了 streams 的概念,当 I/O 发生时,源头的流转换是有固定速度的 x M/s,目标客户端比如视频的转换也是有固定速度的 y M/s,网络请求也有速度并且是个持续的过程,所以 `fetch` 天然也是一个流,速度时 z M/s,我们最终看到视频的速度就是 `min(x, y, z)`,当然如果服务器提前将 readableStream 提供好,那么 x 的速度就可以忽略,此时看到视频的速度是 `min(y, z)`。
|
||||
|
||||
不仅视频如此,打开文件、打开网页等等都是如此,浏览器处理 html 也是一个流的过程:
|
||||
|
||||
```typescript
|
||||
new Response(stream, {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
})
|
||||
```
|
||||
|
||||
如果这个 readableStream 的 `controller.enqueue` 过程被刻意处理的比较慢,网页甚至可以一个字一个字的逐步呈现:[Serving a string, slowly Demo](https://jakearchibald.github.io/isserviceworkerready/demos/simple-stream/)。
|
||||
|
||||
尽管流的场景如此普遍,但也没有必要将所有代码都改成流式处理,因为代码在内存中执行速度很快,变量的赋值是没必要使用流处理的,但如果这个变量的值来自于一个打开的文件,或者网络请求,那么使用流进行处理是最高效的。
|
||||
|
||||
> 讨论地址是:[精读《web streams》· Issue #363 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/363)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,147 @@
|
||||
LOD 表达式在数据分析领域很常用,其全称为 Level Of Detail,即详细级别。
|
||||
|
||||
## 精读
|
||||
|
||||
什么是详细级别,为什么需要 LOD?你一定会有这个问题,我们来一步步解答。
|
||||
|
||||
### 什么是详细级别
|
||||
|
||||
可以尝试这么发问:你这个数据有多详细?
|
||||
|
||||
得到的回答可能是:
|
||||
|
||||
1. 数据是汇总的,抱歉看不到细节,不过如果您正好要看总销量的话,这儿都给您汇总好了。。
|
||||
2. 详细?这直接就是原始表数据,30 亿条,这够详细了吧?如果觉得还不够详细,那只好把业务过程再拆分一下重新埋点了。
|
||||
|
||||
详细程度越高,数据量越大,详细程度越低,数据就越少,就越是汇总的数据。
|
||||
|
||||
人很难在详细程度很高的 30 亿条记录里看到有价值的信息,所以数据分析的过程也可以看作是 **对数据汇总计算的过程,这背后数据详细程度在逐渐降低**。
|
||||
|
||||
### BI 工具的详细级别
|
||||
|
||||
如果没有 LOD 表达式,一个 BI 查询的详细程度是完全固定的:
|
||||
|
||||
- 如果表格拖入度量,没有维度,那就是最高详细级别,因为最终只会汇总出一条记录。
|
||||
- 如果折线图拖入维度,那结果就是根据这个维度内分别聚合度量,数据更详细了,详细粒度为当前维度,比如日期。
|
||||
|
||||
如果我们要更详细的数据,就需要在维度上拖入更多字段,直到达到最详细的明细表级别的粒度。然而同一个查询不可能包含不同详细粒度,因为详细粒度由维度组合决定,不可改变,比如下面表格的例子:
|
||||
|
||||
```text
|
||||
行:国家 省 城市
|
||||
列:GDP
|
||||
```
|
||||
|
||||
这个例子中,详细级别限定在了城市这一级汇总,城市下更细粒度的数据就看不到了,每一条数据都是城市粒度的,我们不可能让查询结果里出现按照国家汇总的 GDP,或者看到更详细粒度的每月 GDP 信息,更不可能让城市粒度的 GDP 与国家粒度 GDP 在一起做计算,算出城市 GDP 在国家中占比。
|
||||
|
||||
但是,类似上面例子的需求是很多的,而且很常见,BI 工具必须想出一种解法,因此诞生了 LOD:**LOD 就是一种表达式,允许我们在一个查询中描述不同的详细粒度**。
|
||||
|
||||
### 从表达式计算来看详细级别
|
||||
|
||||
表达式计算必须限定在同样的详细粒度,这是铁律,为什么呢?
|
||||
|
||||
试想一下下面两张不同详细粒度的表:
|
||||
|
||||
`总销售额`:
|
||||
|
||||
```text
|
||||
10000
|
||||
```
|
||||
|
||||
`各城市销售额`:
|
||||
|
||||
```text
|
||||
北京 3000
|
||||
上海 7000
|
||||
```
|
||||
|
||||
如果我们想在各城市销售额中,计算贡献占比,那么就要写出 `[各城市销售额] / [总销售额]` 的计算公式,但显然这是不可能的,因为前者有两条数据,后者只有一条数据,根本无法计算。
|
||||
|
||||
我们能做的一定是数据行数相同,那么无论是 IF ELSE、CASE WHEN,还是加减乘除都可以按照行粒度进行了。
|
||||
|
||||
LOD 给了我们跨详细粒度计算的能力,其本质还是将数据详细粒度统一,但我们可以让某列数据来自于一个完全不同详细级别的计算:
|
||||
|
||||
```text
|
||||
城市 销售额 总销售额
|
||||
北京 3000 10000
|
||||
上海 7000 10000
|
||||
```
|
||||
|
||||
如图表,LOD 可以把数据加工成这样,即虽然总销售额与城市详细粒度不同,但还是添加到了每一行的末尾,这样就可以进行计算了。
|
||||
|
||||
**因此 LOD 可以按照任意详细级别进行计算,将最终产出 “贴合” 到当前查询的详细级别中。**
|
||||
|
||||
LOD 表达式分为三种能力,分别是 FIXED、INCLUDE、EXCLUDE。
|
||||
|
||||
### FIXED
|
||||
|
||||
```text
|
||||
{ fixed [省份] : sum([GDP]) }
|
||||
```
|
||||
|
||||
按照城市这个固定详细粒度,计算每个省份的 DGP,最后合并到当前详细粒度里。
|
||||
|
||||
假如现在的查询粒度是省份、城市,那么 LOD 字段的添加逻辑如下图所示:
|
||||
|
||||

|
||||
|
||||
可见,本质是两个不同 sql 查询后 join 的结果,内部的 `sum` 表示在 FIXED 表达式内的聚合方式,外部的 `sum` 表示,如果 FIXED 详细级别比当前视图详细级别低,应该如何聚合。在这个例子中,FIXED 详细级别较高,所以 `sum` 不起作用,换成 `avg` 效果也相同,因为合并详细级别是,是一对多关系,只有合并时多对一关系才需要聚合。
|
||||
|
||||
最外层聚合方式一般在 INCLUDE 表达式中发挥作用。
|
||||
|
||||
### EXCLUDE
|
||||
|
||||
```text
|
||||
{ exclude [城市] : sum([GDP]) }
|
||||
```
|
||||
|
||||
在当前查询粒度中,排除城市这个粒度后计算 GDP,最后合并到当前详细粒度中。
|
||||
|
||||
假如现在的查询粒度是省份、城市、季节,那么 LOD 字段的添加逻辑如下图所示:
|
||||
|
||||

|
||||
|
||||
如图所示,EXCLUDE 在当前视图详细级别的基础上,排除一些维度,所得到的详细级别一定会更高。
|
||||
|
||||
### INCLUDE
|
||||
|
||||
```text
|
||||
{ include [城乡] : avg([GDP]) }
|
||||
```
|
||||
|
||||
在当前查询粒度中,额外加上城乡这个粒度后计算 GDP,最后合并到当前详细粒度中。
|
||||
|
||||
这类的例子比较难理解,且在 `sum` 情况下一般无实际意义,因为计算结果不会有差异,必须在类似 `avg` 场景下才有意义,我们还是结合下图来看:
|
||||
|
||||

|
||||
|
||||
这就是 avg 算不准的问题,即不同详细级别计算的平均值是不同的,但 sum、count 等不会随着详细级别变化而影响计算结果,所以当涉及到 avg 计算时,可以通过 INCLUDE 表达式指定计算的详细级别,以保证数据口径准确性。
|
||||
|
||||
### LOD 字段怎么用
|
||||
|
||||
除了上面的例子中,直接查出来展示给用户外,LOD 字段更常用的是作为中间计算过程,比如计算省份 GDP 占在国内占比。因为 LOD 已经将不同详细粒度计算结果合并到了当前的详细粒度里,所以如下的计算表达式:
|
||||
|
||||
```text
|
||||
sum([GDP]) / sum({ fixed [国家] : sum([GDP]) })
|
||||
```
|
||||
|
||||
看似是跨详细粒度计算,其实没有,实际计算时还是一行一行来算的,后面的 **LOD 表达式只是在逻辑上按照指定的详细粒度计算,但最终会保持与当前视图详细粒度一致**,因此可以参与计算。
|
||||
|
||||
我们后面会继续解读 tableau 整理的 Top 15 LOD 表达式业务场景,更深入的理解 LOD 表达式。
|
||||
|
||||
## 总结
|
||||
|
||||
LOD 表达式让你轻松创建 “脱离” 当前视图详细级别的计算字段。
|
||||
|
||||
或许你会疑惑,为什么不主动改变当前视图详细级别来实现同样的效果?比如新增或减少一个维度。
|
||||
|
||||
原因是,LOD 往往用于跨详细级别的计算,比如算部分相对总体的占比,计算当条记录是否为用户首单等等,更多的场景会在下次精读中解读。
|
||||
|
||||
> 讨论地址是:[精读《什么是 LOD 表达式》· Issue #365 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/365)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,127 @@
|
||||
通过上一篇 [精读《什么是 LOD 表达式》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/215.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E3%80%8B.md) 的学习,你已经理解了什么是 LOD 表达式。为了巩固理解,结合场景复习是最有效的手段,所以这次我们结合 [Top 15 LOD Expressions](https://www.tableau.com/about/blog/LOD-expressions) 这篇文章学习 LOD 表达式的 15 大应用场景,因篇幅限制,本文介绍 1~8 场景。
|
||||
|
||||
## 1. 客户下单频次
|
||||
|
||||
**各下单次数的顾客数量是多少?**
|
||||
|
||||
柱状图的 Y 轴显然是 `count([customerID])`,因为要统计 **当前维度下的客户总数**。
|
||||
|
||||
> 这里插一句,对于柱状图的 Y 轴,在 sql 里就是对 X 轴 `group by` 后的聚合,因此 Y 轴就是对 X 轴各项的汇总。
|
||||
|
||||
柱状图的 X 轴要表达的是以何种粒度拆解,比如我们是看各城市数据,还是看各省数据。在这个场景下也不例外,我们要看 **各下单次数下的数据**,那么如何把下单次数转化为维度呢?
|
||||
|
||||
我们需要用 FIX 表达式制作一个维度字段,表示各顾客下单次数。很显然数据库是没有这个维度的,而且这个维度需要按照客户 ID group by 后,按照订单 ID count 聚合才能得到,因此可以利用 FIX 表达式:`{ fixed [customerID] : count([orderId]) }` 描述。
|
||||
|
||||

|
||||
|
||||
## 2. 阵列分析
|
||||
|
||||
当我们看年客户销售量时,即便是逐年增长的,我们也会有一个疑问:**每年销量中,首单在各年份的顾客分别贡献了多少?**
|
||||
|
||||
因为关系到老客忠诚度和新客拓展速度,新客与老客差距过大都不好,那我们如何让 2021 年的柱状图按照 2019、2020、2021 年首单的顾客分层呢?这就是阵列分析。
|
||||
|
||||
我们要画一个柱状图,X、Y 轴分别是 `[Year]`、`sum([Sales])`。
|
||||
|
||||
为了让柱状图分层,我们需要一个表示颜色图例的维度字段,比如我们拖入已有的性别维度,每根柱子就会被划分为男、女两块。但问题是,我们制作并不存在的 “首单年份维度”?
|
||||
|
||||
答案是利用 FIX 表达式:`{ fixed [customerID] : min([orderDate]) }`。
|
||||
|
||||

|
||||
|
||||
## 3. 日利润指标
|
||||
|
||||
分析 **每年各月份的盈利、亏损天数分布**。如下图:
|
||||
|
||||

|
||||
|
||||
列是年到月的下钻,比较好实现,只要拖入字段 `[year]` 并下钻到月粒度,移除季度粒度即可。
|
||||
|
||||
行是 “高收益”、“正收益”、“亏损” 的透视图,值是在当前月份中天数。
|
||||
|
||||
那么如何计算高收益、亏损状态呢?因为最终粒度是天,所以我们要按天计,首先就要得到每天的利润总和,这些中间过程可以利用 LOD 的字段来完成,即创建一个 **日利润字段(profitPerDay)**:`{ fixed [orderDate] : sum([profit]) }`。
|
||||
|
||||
由于我们对利润总量不敏感,只希望拆分为三个阶段,所以利用 IF THEN 生成一个新字段 **日利润指标(dailyProfitKPI)**:`IF [profitPerDay] > 2000 THEN "Highly Profitable" ELSEIF [profitPerDay] <= 0 THEN "unprofitable" ELSE "profitable" END`。
|
||||
|
||||
所以创建的 `[dailyProfitKPI]` 指标是个维度,即如果当前行所在的天利润汇总如果大于 2000,值就是 "Highly Profitable"。所以在行上拖入 `count(distinct [orderDate])`,把 `[dailyProfitKPI]` 拖入行的颜色透视即可。
|
||||
|
||||
## 4. 占总体百分比
|
||||
|
||||
LOD 表达式的一大特色就是计算跨详细级别的占比,比如我们要看 **欧洲各国的销量在全世界占比**:
|
||||
|
||||

|
||||
|
||||
显然这个图里所有国家之和不是 100%,因为欧洲加起来也才不到百分之二十,然而在当前详细级别下,是拿不到全球总销售量的,所以我们可以利用 FIX 表达式来实现:`sum([sales]) / max({ sum([sales]) })`。
|
||||
|
||||
这里解释两点:
|
||||
|
||||
1. 之所以用 `max` 是因为 LOD 表达式只是一个字段,并没有聚合方式,运算必须在相同详细级别下进行,由于总销量只有一条数据,所以我们用 `max` 或者 `min` 甚至 `sum` 都行,结果都是一样的。
|
||||
2. 如果不加维度限制,就可以省略 “fix” 申明,所以 `{ sum([sales]) }` 实际上就是 FIX 表达式,它表示 `{ fixed : sum([sales]) }`。
|
||||
|
||||
## 5. 新客增长趋势
|
||||
|
||||
看着年客户增长趋势图,你有没有想过,这个趋势图肯定永远是向上的?也就是说,看着趋势图朝上走,不一定说明业务做得好。
|
||||
|
||||
如果公司每年都比去年发展的好,每年的新增新客数应该要比去年多,所以 **每年新客增长趋势图** 才比较有意义,如果你看到这个趋势图的趋势朝上,说明每年的新客都比去年多,说明公司摆脱了惯性,每年都获得了新的增长。
|
||||
|
||||
所以我们要加一个筛选条件。新增一个维度字段,当这一单客户是今年新客时为 true,否则为 false,这样我们筛选时,只看这个字段为 true 的结果就行了。
|
||||
|
||||
那么这个字段怎么来呢?思路是,获取客户首单年份,如果首单年份与当前下单年份相同,值为 true,否则为 false。
|
||||
|
||||
我们利用 LOD 创建首单年份字段 `[firstOrderDate]`:`{ fixed [customerId] : min([orderDate]) }`,然后创建筛选字段 `[newOrExist]`: `IFF([firstOrderDate] = [orderDate], 'true', 'false')`。
|
||||
|
||||
## 6. 销量对比分析
|
||||
|
||||
入下图条形图所示,右侧是每项根据选择的分类的对比数据:
|
||||
|
||||

|
||||
|
||||
对比值计算方式是,用 **当前的销量减去当前选中分类的销量**。相信你可以猜到,但前分类的销量与当前视图详细级别无关,只与用户选择的 Category 有关。
|
||||
|
||||
如果我们已经有一个度量字段 - 选中分类销量 `selectedSales`,应该再排除当前 category 维度的干扰,所以可用 EXCLUDE 表达式描述 `selectedCategorySales`: `{ exclude [category] : sum([selectedSales]) }`。
|
||||
|
||||
接下来是创建 `selectedSales` 字段。背景知识是 `[parameters].[category]` 可以获得当前选中的维度值,那我们可以写个 IF 表达式,在维度等于选中维度时聚合销量,不就是选中销量吗?所以公式是:`IF [category] = [parameters].[category] THEN sales ELSE 0 END`。
|
||||
|
||||
最后对比差异,只要创建一个 `[diff]` 字段,表达式为 `sum(sales) - sum(selectedCategorySales)` 即可。
|
||||
|
||||
## 7. 平均最高交易额
|
||||
|
||||
如下图所示,当前的详细级别是国家,但我们却要展示每个国家平均最高交易额:
|
||||
|
||||

|
||||
|
||||
显然,要求平均最高交易额,首先要计算每个销售代表的最高交易额,由于这个详细级别比国家低,我们可以利用 INCLUDE 表达式计算销售代表最高交易额 `largestSalesByRep`: `{ include [salesRep] : max([sales]) }`,并对这个度量字段求平均即可。
|
||||
|
||||
从这个例子可以看出,如果我们在一个较高的详细级别,比如国家,此时的 `sum([sales])` 是根据国家详细级别汇总的,而忽略了销售代表这个详细级别。但如果要展示每个国家的平均最高交易额,就必须在销售代表这个详细级别求 `max([sales])`,由于是各国家的,所以我们不用 `{ fixed [salesRep] }`,而是 `{ include [salesRep] }`,这样最终计算的详细级别是:`[country],[salesRep]`,这样才能算出销售在每个国家的最高交易额(因为也许某些销售同时在不同国家销售)。
|
||||
|
||||
## 8. 实际与目标
|
||||
|
||||
在第六个例子 - 销量对比分析中,我们可以看到销量绝对值的对比,这次,我们需要计算实际销售额与目标的差距百分比:
|
||||
|
||||

|
||||
|
||||
如上图所示,左上角展示了实际与目标的差值;右上角展示了每个地区产品目标完成率;下半部分展示了每个产品实际销量柱状图,并用黑色横线标记出目标值。
|
||||
|
||||
左上角非常简单,`[diffActualTraget]`: `[profit] - [targetProfit]`,只要将当前利润与目标利润相减即可。
|
||||
|
||||
右上角需要分为几步拆解。我们的最终目标是计算每个地区产品目标完成率,显然公式是 当前完成产品数/总产品数。总产品数比较简单,在已有地区维度拆解下,计算下产品总数就行了,即 `count(distinct [product])`;难点是当前完成产品数,这里我们又要用到 INCLUDE,为什么呢?因为地区粒度比产品粒度高,我们看地区汇总的时候,就不知道各产品的完成情况了,所以必须 INCLUDE product 维度计算利润目标差,公式是 `[diffProductActualTraget]` :`{ include [product] : sum(diffActualTraget) }`,然后当这个值大于 0 就认为完成了目标,我们可以再创建一个字段,即完成目标数,如果达成目标就是 1,否则是 0,这样便于求 “当前完成产品数”:`aboveTargetProductCount`: `IFF([diffProductActualTraget] > 0, 1, 0)`,那么当前完成产品数就是 `sum([diffProductActualTraget])`,所以产品目标完成率就是 `sum([diffProductActualTraget]) / count(distinct [product])`,将这个字段拖入指标,按照百分比格式化,就得到结果了。
|
||||
|
||||
## 总结
|
||||
|
||||
通过上面的例子,我们可以总结出实际业务场景中几条使用心法:
|
||||
|
||||
1. 首先对计算公式进行拆解,判断拆解后的字段是否数据集里都有,如果都有的话就结束了,说明是个简单需求。
|
||||
2. 如果数据集里没有,而且发现数据详细级别与当前不符(比如要得到每个国家销量,但当前维度是城市),就要用 FIXED 表达式固定详细级别。
|
||||
3. 如果不是明确的按照某个详细级别计算,就不要使用 FIXED,因为不太灵活。
|
||||
4. 当计算时要跳过某个指定详细级别,但又要保留视图里的详细级别时,使用 EXCLUDE 表达式。
|
||||
5. 如果计算涉及到比视图低的详细级别,比如计算平均或者最大最小时,使用 INCLUDE 表达式。
|
||||
6. 使用 FIXED 表达式创建的字段也可以进行二次计算,合理拆解多个计算字段并组合,会让逻辑更加清晰,易于理解。
|
||||
|
||||
> 讨论地址是:[精读《15 大 LOD 表达式 - 上》· Issue #369 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/369)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,183 @@
|
||||
接着上一篇 [精读《15 大 LOD 表达式 - 上》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/216.%E7%B2%BE%E8%AF%BB%E3%80%8A15%20%E5%A4%A7%20LOD%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%20-%20%E4%B8%8A%E3%80%8B.md) ,这次继续总结 [Top 15 LOD Expressions](https://www.tableau.com/about/blog/LOD-expressions) 这篇文章的 9~15 场景。
|
||||
|
||||
## 9. 某时间段内最后一天的值
|
||||
|
||||
如何实现股票平均每日收盘价与当月最后一天收盘价的对比趋势图?
|
||||
|
||||

|
||||
|
||||
如图所示,要对比的并非是某个时间段,而是当月最后一天的收盘价,因此必须要借助 LOD 表达式。
|
||||
|
||||
设想原表如下:
|
||||
|
||||
| Date | Ticker | Adj Close |
|
||||
| ---- | ---- | ---- |
|
||||
| 29/08/2013 | SYMC | $1 |
|
||||
| 28/08/2013 | SYMC | $2 |
|
||||
| 27/08/2013 | SYMC | $3 |
|
||||
|
||||
我们按照月进行聚合作为横轴,求 `avg([Adj Close])` 作为纵轴即可。但计算对比我们需要一个 Max Date 字段如下:
|
||||
|
||||
| Date | Ticker | Adj Close | Max, Date |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 29/08/2013 | SYMC | $1 | 29/08/2013 |
|
||||
| 28/08/2013 | SYMC | $2 | 29/08/2013 |
|
||||
| 27/08/2013 | SYMC | $3 | 29/08/2013 |
|
||||
|
||||
如果我们使用 `max(Date)` 表达式,在聚合后结果是可以看到 Max Date 的:
|
||||
|
||||
| Month of Date | Ticker | Avg, Adj Close | Max, Date
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 08/2013 | SYMC | $2 | 29/08/2013 |
|
||||
|
||||
原因是,`max(Date)` 是一个聚合表达式,只能在 group by 聚合 sql 下生效。但如果我们要计算最后一天的收盘价,就要执行 `sum([Close value on last day]`,表达式如下:
|
||||
|
||||
`[Close value on last day] = if [Max Date] = [Date] then [Adj Close] else 0 end`。
|
||||
|
||||
但问题是,这个表达式计算的明细级别是以天为粒度的,我们 `max(Date)` 在天粒度下是算不出来的:
|
||||
|
||||
| Date | Ticker | Adj Close | Max, Date |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 29/08/2013 | SYMC | $1 | |
|
||||
| 28/08/2013 | SYMC | $2 | |
|
||||
| 27/08/2013 | SYMC | $3 | |
|
||||
|
||||
原因就是上面说过的,聚合表达式不能在非聚合的明细级别中出现。因此我们利用 `{ include : max([Date]) }` 表达式就能轻松实现下面的效果了:
|
||||
|
||||
| Date | Ticker | Adj Close | { include : max([Date]) } |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 29/08/2013 | SYMC | $1 | 29/08/2013 |
|
||||
| 28/08/2013 | SYMC | $2 | 29/08/2013 |
|
||||
| 27/08/2013 | SYMC | $3 | 29/08/2013 |
|
||||
|
||||
`{ include : max([Date]) }` 表达式没有给定 include 参数,意味着永远以当前视图的明细级别计算,因此这个字段下推到明细表做计算时,也可以出现在明细表的每一行。接着按照上面的思路组装表达式即可。
|
||||
|
||||
拓展一下,如果横轴我们按年进行聚合,那么对比值就是每年最后一天的收盘价。原因是 `{ include : max([Date]) }` 会以当前年这个粒度计算 `max([Date])`,自然是当年的最后一天,然后下推到明细表,整整一年 365 行数据中,`[Close value on last day]` 大概是这样:
|
||||
|
||||
| Date | Ticker | Adj Close | [Close value on last day] |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 31/12/2013 | SYMC | $1 | $1 |
|
||||
| 30/12/2013 | SYMC | $2 | $1 |
|
||||
| ... | ... | ... | ... |
|
||||
| 03/01/2013 | SYMC | $7 | $1 |
|
||||
| 02/01/2013 | SYMC | $8 | $1 |
|
||||
| 01/01/2013 | SYMC | $9 | $1 |
|
||||
|
||||
接着对比值按照 `sum([Close value on last day])` 聚合即可。
|
||||
|
||||
## 10. 复购阵列
|
||||
|
||||
如下图所示,希望查看客户第一次购买到第二次购买间隔季度的复购阵列:
|
||||
|
||||

|
||||
|
||||
关键在于如何求第一次与第二次购买的季度时间差。首先可以通过 `[1st purchase] = { fixed [customer id] : min([order date]) }` 计算每位客户首次购买时间。
|
||||
|
||||
如何计算第二次购买时间?这里有个小技巧。首先利用 `[repeat purchase] = iif([order date] > [1st purchase], [order date], null)` 得到一个新列,首次购买的那一行值为 null,我们可以利用 `min` 函数计算时忽略 null 的特性,得到第二次购买时间:`[2nd purchase] = { fixed [customer id] : min([repeat purchase]) }`。
|
||||
|
||||
最后利用 `datediff` 函数得到间隔的季度数:`[quarters repeat to purchase] = datediff('quarter', [1st prechase], [2nd purchase])`。
|
||||
|
||||
## 11. 范围平均值差异百分比
|
||||
|
||||
如下图所示,我们希望将趋势图的每个点,与选定区域(图中两个虚线范围内)的均值做一个差异百分比,并生成一个新的折线图放在上方。
|
||||
|
||||

|
||||
|
||||
重点是上面折线图 y 轴字段,差异百分比如何表示。首先我们要生成一个只包含指定区间的收盘值:
|
||||
|
||||
`[Close value in reference period] = IF [Date] >= [Start reference date] AND [Date] <= [End reference date] THEN [Adj close] END`,这段表达式只在日期在制定区间内时,才返回 `[Adj close]`,也就是只包含这个区间内的值。
|
||||
|
||||
第二步,计算制定区间的平均值,这个用 FIX 表达式即可:`[Average daily close value between ref date] = { fixed [Ticker] : AVG([Close value in reference period]) }`。
|
||||
|
||||
第三步,计算百分比差异:`[percent different from ref period] = ([Adj close] - [Average daily close value between ref date]) / [Average daily close value between ref date]`。
|
||||
|
||||
最后就是用 `[percent different from ref period]` 这个字段绘制上面的图形了。
|
||||
|
||||
## 12. 相对周期过滤
|
||||
|
||||
如果我们想对比两个周期数据差异,可能会遇到数据不全导致的错误。比如今年 3 月份数据只产出到 6 号,但却和去年 3 月整月的数据进行对比,显然是不合理的。我们可以利用 LOD 表达式解决这个问题:
|
||||
|
||||

|
||||
|
||||
相对周期过滤的重点是,不能直接用日期进行对比,因为今年数据总是比去年大。比如因为今年最新数据到 11.11 号,那么去年 11.11 号之后的数据都要被过滤掉。
|
||||
|
||||
首先找到最新数据是哪一天,利用不包含条件的 FIX 表达式即可:`[max date] = { max([date]) }`。
|
||||
|
||||
然后利用 datepart 函数计算当前日期是今年的第几天:
|
||||
|
||||
`[day of year of max date] = datepart('dayofyear', [max date])`,`[day of year of order date] = datepart('dayofyear', [order date])`。
|
||||
|
||||
所以 `[day of year of max date]` 就是一个卡点,任何超过今年这么多天的数据都要过滤掉。因此我们创建一个过滤条件:`[period filter] = [day of year of order date] <= [day of year of max date]`。
|
||||
|
||||
把 `[period filter]` 字段作为筛选条件即可。
|
||||
|
||||
## 13. 用户登陆频率
|
||||
|
||||
如何绘制一个用户每个月登陆频率?
|
||||
|
||||

|
||||
|
||||
要计算这个指标,得用用户总活跃时间除以总登陆次数。
|
||||
|
||||
首先计算总活跃时间:利用 FIX 表达式计算用户最早、最晚的登陆时间:
|
||||
|
||||
- `[first login] = { fixed [user id] : min([log in date]) }`
|
||||
- `[last login] = { fixed [user id] : max([log in date]) }`
|
||||
|
||||
计算其中月份 diff,就是用户活跃月数:
|
||||
|
||||
`[total months user is active] = datediff("month", [first login], [last login])`
|
||||
|
||||
总登录次数比较简单,也是固定用户 ID 后,对登陆日期计数即可:
|
||||
|
||||
`[numbers of logins per user] = { fixed [user id] : count([login date]) }`
|
||||
|
||||
最后,我们用两者相除,得到用户登陆频率:
|
||||
|
||||
`[login frequency] = [total months user is active] / [numbers of logins per user]`
|
||||
|
||||
制作图表就很简单了,把 `[login frequency]` 移到横轴,count distinct 用户 ID 作为纵轴即可。
|
||||
|
||||
## 14. 比例笔刷
|
||||
|
||||
这个是 LOD 最常见的场景,比如求各品类销量占此品类总销量的贡献占比?
|
||||
|
||||

|
||||
|
||||
`sum(sales) / sum({ fixed [category] : sum(sales) })` 即可。
|
||||
|
||||
当前详细级别是 category + country,我们固定品类,就可以得到各品类在所有国家的累积销量。
|
||||
|
||||
## 15. 按客户群划分的年度购买频率
|
||||
|
||||
如何证明老客户忠诚度更高?
|
||||
|
||||
我们可以如下图,按照客户群(2011 年、2012 年客户)作为图例,观察他们每年购买频次分布。
|
||||
|
||||

|
||||
|
||||
如上图所示,我们发现顾客注册时间越早,各购买频次的比例都更高,所以证明了老顾客忠诚度更高这一结论。注意这里看的是至少购买 N 次,所以每条线相比才具有说服力。如果是购买 N 次,则可能老顾客购买 1 次较少,购买 10 次较多,难以直接对比。
|
||||
|
||||
首先我们生成图例字段,即按最早照购买年份划分顾客群:`[Cohort] = { fixed [customer id] : min(Year([order date])) }`
|
||||
|
||||
然后就和我们第一个例子类似,计算每个订单数量下,有多少顾客。唯一的区别是,我们不仅按照顾客 ID group,还要进一步对最早购买日期做拆分,即:`{ fixed [customer id], [Cohort] : count([order id]) }`。
|
||||
|
||||
上面的字段作为 X 轴,Y 轴和第一个例子类似:`count(customer id)`,但我们想查看的是至少购买 N 次,也就是这个购买次数是累计值,即至少购买 9 次 = 购买 9 次 + 购买 10 次 + ... 购买 MAX 次。所以是一种 DESC 的 `windowsum`,整体表达式应该类似 `[Running Total] = WINDOW_SUM(count(customer id)), 0, LAST())`。
|
||||
|
||||
最后,因为实际 Y 轴计算的是占比,所以用刚才计算的至少购买 N 次指标除以各 Cohort 下总购买次数,即 `[Running Total] / sum({ fixed [Cohort] : count([customer id]) })`。
|
||||
|
||||
## 总结
|
||||
|
||||
上面的几个例子,都是基于 fixed、include、exclude 这几个基本 LOD 用法的叠加。但从实际例子来看,我们会发现真正的难点不在与 LOD 表达式的语法,而在于我们如何精确理解需求,拆解成合理的计算步骤,并在需要运行 LOD 的计算步骤正确的使用。
|
||||
|
||||
LOD 表达式看上去很神奇,似乎可以和数据 “神奇” 的贴合在一起,我们要理解到 LOD 背后就是表之间的 join,而不同明细级别就表示不同的 group by 规则这一背后原理,就能比较好的理解为什么 LOD 表达式能这么运作了。
|
||||
|
||||
> 讨论地址是:[精读《15 大 LOD 表达式 - 下》· Issue #370 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/370)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,301 @@
|
||||
[Rust Is The Future of JavaScript Infrastructure](https://leerob.io/blog/rust) 这篇文章讲述了 Rust 正在 JS 基建圈流行的事实:[Webpack](https://github.com/webpack/webpack)、[Babel](https://github.com/babel/babel)、[Terser](https://github.com/terser/terser)、[Prettier](https://github.com/prettier/prettier)、[ESLint](https://github.com/eslint/eslint) 这些前些年才流行起来的工具都已有了 Rust 替代方案,且性能有着 10~100 倍的提升。
|
||||
|
||||
前端基建的迭代浪潮从未停歇,当上面这些工具给 Gulp、js-beautify、tslint 等工具盖上棺材盖时,基于 Rust 的新一代构建工具已经悄悄将棺材盖悬挂在 webpack、babel、prettier、terser、eslint 它们头上,不知道哪天就会盖上。
|
||||
|
||||
原文已经有了不错的 [中文翻译](https://mp.weixin.qq.com/s?__biz=MzkxNDIzNTg4MA==&mid=2247485792&idx=1&sn=682a4dee7ce4d3b47a81baf9ebd7a98a&chksm=c170c1e7f60748f17585d6bfca0cff6edbf71bab95f0a4a1ea0bcf2d43c16d1722666d9fadc1&token=1766743281&lang=zh_CN#rd),值得一提的是,原文一些英文名词对应着特定中文解释,记录如下:
|
||||
|
||||
- low-level programming:~~低级编程~~ 底层编程。
|
||||
- ergonomics:~~人体工程学~~ 人机工程学。
|
||||
- opinionated:~~自以为是,固执的~~ 开箱即用的。
|
||||
- critical adoption:~~批判性采用~~ 技术选型临界点。
|
||||
|
||||
## 精读
|
||||
|
||||
本文不会介绍 Rust 如何使用,而会重点介绍原文提到的 Rust 工具链的一些基本用法,如果你感兴趣,可以立刻替换现有的工具库!
|
||||
|
||||
### swc
|
||||
|
||||
[swc](https://swc.rs/) 是基于 Rust 开发的一系列编译、打包、压缩等工具,并且被广泛应用于更多更上层的 JS 基建,大大推动了 Rust 在 JS 基建的影响力,所以要第一个介绍。
|
||||
|
||||
swc 提供了一系列原子能力,涵盖构建与运行时:
|
||||
|
||||
#### @swc/cli
|
||||
|
||||
`@swc/cli` 可以同时构建 js 与 ts 文件:
|
||||
|
||||
```typescript
|
||||
const a = 1
|
||||
```
|
||||
|
||||
```bash
|
||||
npm i -D @swc/cli
|
||||
npx swc ./main.ts
|
||||
|
||||
# output:
|
||||
# Successfully compiled 1 file with swc.
|
||||
# var a = 1;
|
||||
```
|
||||
|
||||
具体功能与 babel 类似,都可以让浏览器支持先进语法或者 ts,只是 `@swc/cli` 比 babel 快了至少 20 倍。可以通过 `.swcrc` 文件做 [自定义配置](https://swc.rs/docs/configuration/swcrc)。
|
||||
|
||||
#### @swc/core
|
||||
|
||||
你可以利用 `@swc/core` 制作更上层的构建工具,所以它是 `@swc/cli` 的开发者调用版本。基本 API 来自官网开发者文档:
|
||||
|
||||
```typescript
|
||||
const swc = require("@swc/core");
|
||||
|
||||
swc
|
||||
.transform("source code", {
|
||||
// Some options cannot be specified in .swcrc
|
||||
filename: "input.js",
|
||||
sourceMaps: true,
|
||||
// Input files are treated as module by default.
|
||||
isModule: false,
|
||||
|
||||
// All options below can be configured via .swcrc
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "ecmascript",
|
||||
},
|
||||
transform: {},
|
||||
},
|
||||
})
|
||||
.then((output) => {
|
||||
output.code; // transformed code
|
||||
output.map; // source map (in string)
|
||||
});
|
||||
```
|
||||
|
||||
其实就是把 cli 调用改成了 node 调用。
|
||||
|
||||
#### @swc/wasm-web
|
||||
|
||||
`@swc/wasm-web` 可以在浏览器运行时调用 wasm 版的 swc,以得到更好的性能。下面是官方的例子:
|
||||
|
||||
```typescript
|
||||
import { useEffect, useState } from "react";
|
||||
import initSwc, { transformSync } from "@swc/wasm-web";
|
||||
|
||||
export default function App() {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function importAndRunSwcOnMount() {
|
||||
await initSwc();
|
||||
setInitialized(true);
|
||||
}
|
||||
importAndRunSwcOnMount();
|
||||
}, []);
|
||||
|
||||
function compile() {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
const result = transformSync(`console.log('hello')`, {});
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<button onClick={compile}>Compile</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这个例子可以在浏览器运行时做类似 babel 的事情,无论是低代码平台还是在线 coding 平台都可以用它做运行时编译。
|
||||
|
||||
#### @swc/jest
|
||||
|
||||
`@swc/jest` 提供了 Rust 版本的 jest 实现,让 jest 跑得更快。使用方式也很简单,首先安装:
|
||||
|
||||
```bash
|
||||
npm i @swc/jest
|
||||
```
|
||||
|
||||
然后在 `jest.config.js` 配置文件中,将 ts 文件 compile 指向 `@swc/jest` 即可:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": ["@swc/jest"],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### swc-loader
|
||||
|
||||
`swc-loader` 是针对 webpack 的 loader 插件,代替 `babel-loader`:
|
||||
|
||||
```javascript
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: {
|
||||
// `.swcrc` can be used to configure swc
|
||||
loader: "swc-loader"
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### swcpack
|
||||
|
||||
增强了多文件 bundle 成一个文件的功能,基本可以认为是 swc 版本的 webpack,当然性能也会比 `swc-loader` 方案有进一步提升。
|
||||
|
||||
截至目前,该功能还在测试阶段,只要安装了 `@swc/cli` 就可使用,通过创建 `spack.config.js` 后执行 `npx spack` 即可运行,和 webpack 的使用方式一样。
|
||||
|
||||
### Deno
|
||||
|
||||
[Deno](https://deno.land/) 的 linter、code formatter、文档生成器采用 swc 构建,因此也算属于 Rust 阵营。
|
||||
|
||||
Deno 是一种新的 js/ts 运行时,所以我们总喜欢与 node 进行类比。[quickjs](https://bellard.org/quickjs/) 也一样,这三个都是一种对 js 语言的运行器,作为开发者,需求永远是更好的性能、兼容性与生态,三者几乎缺一不可,所以当下虽然不能完全代替 Nodejs,但作为高性能替代方案是很香的,可以基于他们做一些跨端跨平台的解析器,比如 [kraken](https://github.com/openkraken/kraken) 就是基于 quickjs + flutter 实现的一种高性能 web 渲染引擎,是 web 浏览器的替代方案,作为一种跨端方案。
|
||||
|
||||
### esbuild
|
||||
|
||||
[esbuild](https://esbuild.github.io/) 是较早被广泛使用的新一代 JS 基建,是 JS 打包与压缩工具。虽然采用 Go 编写,但性能与 Rust 不相上下,可以与 Rust 风潮放在一起看。
|
||||
|
||||
esbuild 目前有两个功能:编译和压缩,理论上分别可代替 babel 与 terser。
|
||||
|
||||
编译功能的基本用法:
|
||||
|
||||
```js
|
||||
require('esbuild').transformSync('let x: number = 1', {
|
||||
loader: 'ts',
|
||||
})
|
||||
|
||||
// 'let x = 1;\n'
|
||||
```
|
||||
|
||||
压缩功能的基本用法:
|
||||
|
||||
```js
|
||||
require('esbuild').transformSync('fn = obj => { return obj.x }', {
|
||||
minify: true,
|
||||
})
|
||||
|
||||
// 'fn=n=>n.x;\n'
|
||||
```
|
||||
|
||||
压缩功能比较稳定,适合用在生产环境,而编译功能要考虑兼容 webpack 的地方太多,在成熟稳定后才考虑能在生产环境使用,目前其实已经有不少新项目已经在生产环境使用 esbuild 的编译功能了。
|
||||
|
||||
编译功能与 `@swc` 类似,但因为 Rust 支持编译到 wasm,所以 `@swc` 提供了 web 运行时编译能力,而 esbuild 目前还没有看到这种特性。
|
||||
|
||||
### Rome
|
||||
|
||||
[Rome](https://rome.tools/blog/2020/08/08/introducing-rome) 是 Babel 作者做的基于 Nodejs 的前端基建全家桶,包含但不限于 Babel, ESLint, webpack, Prettier, Jest。目前 [计划使用 Rust 重构](https://rome.tools/blog/2021/09/21/rome-will-be-rewritten-in-rust),虽然还没有实现,但我们姑且可以把 Rome 当作 Rust 的一员。
|
||||
|
||||
`rome` 是个全家桶 API,所以你只需要 `yarn add rome` 就完成了所有环境准备工作。
|
||||
|
||||
- `rome bundle` 打包项目。
|
||||
- `rome compile` 编译单个文件。
|
||||
- `rome develop` 调试项目。
|
||||
- `rome parse` 解析文件抽象语法树。
|
||||
- `rome analyzeDependencies` 分析依赖。
|
||||
|
||||
Rome 还将文件格式化与 Lint 合并为了 `rome check` 命令,并提供了[友好 UI 终端提示](https://rome.tools/#command-usage)。
|
||||
|
||||
其实我并不太看好 Rome,因为它负担太重了,测试、编译、Lint、格式化、压缩、打包的琐碎事情太多,把每一块交给社区可能会做得更好,这不现在还在重构中,牵一发而动全身。
|
||||
|
||||
### NAPI-RS
|
||||
|
||||
[NAPI-RS](https://napi.rs/) 提供了高性能的 Rust 到 Node 的衔接层,可以将 Rust 代码编译后成为 Node 可调用文件。下面是官网的例子:
|
||||
|
||||
```rust
|
||||
#[js_function(1)]
|
||||
fn fibonacci(ctx: CallContext) -> Result<JsNumber> {
|
||||
let n = ctx.get::<JsNumber>(0)?.try_into()?;
|
||||
ctx.env.create_int64(fibonacci_native(n))
|
||||
}
|
||||
```
|
||||
|
||||
上面写了一个斐波那契数列函数,直接调用了 `fibonacci_native` 函数实现。为了让这个方法被 Node 调用,首先安装 CLI:`npm i @napi-rs/cli`。
|
||||
|
||||
由于环境比较麻烦,因此需要利用这个脚手架初始化一个工作台,我们在里面写 Rust,然后再利用固定的脚本发布 npm 包。执行 `napi new` 创建一个项目,我们发现入口文件肯定是个 js,毕竟要被 node 引用,大概长这样(我创建了一个 `myLib` 包):
|
||||
|
||||
```js
|
||||
const { loadBinding } = require('@node-rs/helper')
|
||||
|
||||
/**
|
||||
* __dirname means load native addon from current dir
|
||||
* 'myLib' is the name of native addon
|
||||
* the second arguments was decided by `napi.name` field in `package.json`
|
||||
* the third arguments was decided by `name` field in `package.json`
|
||||
* `loadBinding` helper will load `myLib.[PLATFORM].node` from `__dirname` first
|
||||
* If failed to load addon, it will fallback to load from `myLib-[PLATFORM]`
|
||||
*/
|
||||
module.exports = loadBinding(__dirname, 'myLib', 'myLib')
|
||||
```
|
||||
|
||||
所以 loadBinding 才是入口,同时项目文件夹下存在三个系统环境包,分别供不同系统环境调用:
|
||||
|
||||
- `@cool/core-darwin-x64` macOS x64 平台。
|
||||
- `@cool/core-win32-x64` Windows x64 平台。
|
||||
- `@cool/core-linux-arm64-gnu` Linux aarch64 平台。
|
||||
|
||||
`@node-rs/helper` 这个包的作用是引导 node 执行预编译的二进制文件,`loadBinding` 函数会尝试加载当前平台识别的二进制包。
|
||||
|
||||
将 `src/lib.rs` 的代码改成上面斐波那契数列的代码后,执行 `npm run build` 编译。注意在编译前需要安装 rust 开发环境,只要一行脚本即可安装,具体看 [rustup.rs](https://rustup.rs/)。然后把当前项目整体当作 node 包发布即可。
|
||||
|
||||
发布后,就可以在 node 代码中引用啦:
|
||||
|
||||
```javascript
|
||||
import { fibonacci } from 'myLib'
|
||||
|
||||
function hello() {
|
||||
let result = fibonacci(10000)
|
||||
console.log(result)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
NAPI-RS 作为 Rust 与 Node 的桥梁,很好的解决了 Rust 渐进式替换现有 JS 工具链的问题。
|
||||
|
||||
### Rust + WebAssembly
|
||||
|
||||
[Rust + WebAssembly](https://www.rust-lang.org/what/wasm) 说明 Rust 具备编译到 wasm 的能力,虽然编译后代码性能会变得稍慢,但还是比 js 快很多,同时由于 wasm 的可移植性,让 Rust 也变得可移植了。
|
||||
|
||||
其实 Rust 支持编译到 WebAssembly 也不奇怪,因为本来 WebAssembly 的定位之一就是作为其他语言的目标编译产物,然后它本身支持跨平台,这样它就很好的完成了传播的使命。
|
||||
|
||||
WebAssembly 是一个基于栈的虚拟机 ([stack machine](https://webassembly.github.io/spec/core/exec/index.html)),所以跨平台能力一流。
|
||||
|
||||
想要将 Rust 编译为 wasm,除了安装 Rust 开发环境外,还要安装 [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/)。
|
||||
|
||||
安装后编译只需执行 `wasm-pack build` 即可。更多用法可以查看 [API 文档](https://rustwasm.github.io/wasm-pack/book/commands/build.html)。
|
||||
|
||||
### dprint
|
||||
|
||||
[dprint](https://github.com/dprint/dprint) 是用 rust 编写的 js/ts 格式化工具,并提供了 [dprint-node](https://github.com/devongovett/dprint-node) 版本,可以直接作为 node 包,通过 npm 安装使用,从 [源码](https://github.com/devongovett/dprint-node/blob/main/src/lib.rs) 可以看到,使用 [NAPI-RS](https://napi.rs/) 实现。
|
||||
|
||||
`dprint-node` 可以直接在 Node 中使用:
|
||||
|
||||
```js
|
||||
const dprint = require('dprint-node');
|
||||
dprint.format(filePath, code, options);
|
||||
```
|
||||
|
||||
[参数文档](https://dprint.dev/plugins/typescript/config/)。
|
||||
|
||||
### Parcel
|
||||
|
||||
[Parcel](https://parceljs.org/) 严格来说算是上一代 JS 基建,它出现在 Webpack 之后,Rust 风潮之前。不过由于它已经[采用 SWC 重写](https://github.com/parcel-bundler/parcel/pull/6230),所以姑且算是跟上了时髦。
|
||||
|
||||
## 总结
|
||||
|
||||
前端全家桶已经有了一整套 Rust 实现,只是对于存量项目的编译准确性需要大量验证,我们还需要时间等待这些库的成熟度。
|
||||
|
||||
但毫无疑问的是,Rust 语言对 JS 基建支持已经较为完备了,剩下的只是工具层逻辑覆盖率的问题,都可以随时间而解决。而用 Rust 语言重写后的逻辑带来的巨幅性能提升将为社区注入巨大活力,就像原文说的,前端社区可以为了巨大性能提升而引入 Rust 语言,即便这可能导致为社区贡献门槛的提高。
|
||||
|
||||
> 讨论地址是:[精读《Rust 是 JS 基建的未来》· Issue #371 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/371)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,108 @@
|
||||
[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part1) 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第一篇。
|
||||
|
||||
虽然本文写于 2018 年,但如今依然值得学习,因为浏览器实现非常复杂,从细节开始学习很容易迷失方向,缺乏整体感,而这篇文章从宏观层面开始介绍,几乎没有涉及代码实现,全都是思路性的描述,非常适合培养对浏览器整体框架性思维。
|
||||
|
||||
原文有非常多形象的插图与动图,便于加深对知识的理解,所以也推荐直接阅读原文。
|
||||
|
||||
## 概述
|
||||
|
||||
文章先从 CPU、GPU、操作系统开始介绍,因为这些是浏览器运行的基座。
|
||||
|
||||
### CPU、GPU、操作系统、应用的关系
|
||||
|
||||
CPU 即中央处理器,可以处理几乎所有计算。以前的 CPU 是单核的,现在大部分笔记电脑都是多核的,专业服务器甚至有高达 100 多核的。CPU 计算能力很强,但只能一件件事处理,
|
||||
|
||||
GPU 一开始是为图像处理设计的,即主要处理像素点,所以拥有大量并行的处理简单事物的能力,非常适合用来做矩阵运算,而矩阵运算又是计算机图形学的基础,所以大量用在可视化领域。
|
||||
|
||||
CPU、GPU 都是计算机硬件,这些硬件各自都提供了一些接口供汇编语言调用;而操作系统则基于它们之上用 C 语言(如 linux)将硬件管理了起来,包括进程调度、内存分配、用户内核态切换等等;运行在操作系统之上的则是应用程序了,所以应用程序不直接和硬件打交道,而是通过操作系统间接操作硬件。
|
||||
|
||||
> 为什么应用程序不能直接操作硬件呢?这样做有巨大的安全隐患,因为硬件是没有任何抽象与安全措施的,这意味着理论上一个网页可以通过 js 程序,在你打开网页时直接访问你的任意内存地址,读取你的聊天记录,甚至读取历史输入的银行卡密码进行转账操作。
|
||||
|
||||
显然,浏览器作为一个应用程序,运行在操作系统之上。
|
||||
|
||||
### 进程与线程
|
||||
|
||||
为了让程序运行的更安全,操作系统创造了进程与线程的概念(linux 对进程与线程的实现是同一套),进程可以分配独立的内存空间,进程内可以创建多个线程进行工作,这些线程共享内存空间。
|
||||
|
||||
因为线程间共享内存空间,因此不需通信就能交流,但内存地址相互隔离的进程间也有通信需求,需通过 IPC(Inter Process Communication)进行通信。
|
||||
|
||||
进程之间相互独立,即一个进程挂了不会影响到其它进程,而在一个进程中可以创建一个新进程,并与之通信,所以浏览器就采用了这种策略,将 UI、网络、渲染、插件、存储等模块进程独立,并且任意挂掉后都可以被重新唤起。
|
||||
|
||||
### 浏览器架构
|
||||
|
||||
浏览器可以拆分为许多独立的模块,比如:
|
||||
|
||||
- 浏览器模块(Browser):负责整个浏览器内行为协调,调用各个模块。
|
||||
- 网络模块(Network):负责网络 I/O。
|
||||
- 存储模块(Storage):负责本地 I/O。
|
||||
- 用户界面模块(UI):负责浏览器提供给用户的界面模块。
|
||||
- GPU 模块:负责绘图。
|
||||
- 渲染模块(Renderer):负责渲染网页。
|
||||
- 设备模块(Device):负责与各种本地设备交互。
|
||||
- 插件模块(Plugin):负责处理各类浏览器插件。
|
||||
|
||||
基于这些模块,浏览器有两种可用的架构设计,一种是少进程,一种是多进程。
|
||||
|
||||
少进程是指将这些模块放在一个或有限的几个进程里,也就是每个模块一个线程,这样做的好处是最大程度共享了内存空间,对设备要求较低,但问题是只要一个线程挂了都会导致整个浏览器挂掉,因此稳定性较差。
|
||||
|
||||
多进程是指为每个模块(尽量)开辟一个进程,模块间通过 IPC 通信,因此任何模块挂掉都不会影响其它模块,但坏处是内存占用较大,比如浏览器 js 解析与执行引擎 V8 就要在这套架构下拷贝多份实例运行在每个进程中。
|
||||
|
||||
### Chrome 多进程架构的优势
|
||||
|
||||
Chrome 尽量为每个 tab 单独创建一个进程,所以我们才能在某个 tab 未响应时,从容的关闭它,而其它 tab 不会受到影响。不仅是 tab 间,一个 tab 内的 iframe 间也会创建独立的进程,这样做是为了保护网站的安全性。
|
||||
|
||||
### 服务化 - 单/多进程弹性架构
|
||||
|
||||
Chrome 并不满足于采用一种架构,而是在不同环境下切换不同的架构。Chrome 将各功能模块化后,就可以自由决定当前将哪些模块放在一个进程中,将哪些模块启动独立进程,即可以在运行时决定采用哪套进程架构。
|
||||
|
||||
这样做的好处是,可以在资源受限的机器上开启单进程模式,以尽量节约内存开销,实际上在手机应用上就是这么做的;而在资源丰富、内核数量充足的机器上采用独立进程模式,虽然消耗了更多资源,但获得了更好的稳定性。
|
||||
|
||||
### Iframe 独占进程
|
||||
|
||||
[site-isolation](https://developers.google.com/web/updates/2018/07/site-isolation) 将同一个 tab 内不同 iframe 包裹在不同的进程内运行,以确保 iframe 间资源的独占性,以及安全性。该功能直到 2018.7 才更新,是因为背后有许多复杂的工作要处理,比如开发者工具的调试、网页的全局搜索功能,都不能因为进程的隔离而受到影响,Chrome 必须让每个进程单独响应这些操作,并最终聚合在一起,让用户感受不到进程间的阻隔。
|
||||
|
||||
## 精读
|
||||
|
||||
本文从浏览器如何基于操作系统提供的进程、线程概念构建自己的应用程序开始,从硬件、操作系统、软件的分层开始,介绍到浏览器是如何划分模块的,并且分配进程或线程给这些模块运行,这背后的思考非常有价值。
|
||||
|
||||
从宏观角度看,要设计一个安全稳定、高性能、具有拓展性的浏览器,首先要把各功能模块划分清楚,并定义好各模块的通信关系,在各业务场景下制定一套模块协作的流程。
|
||||
|
||||
### 浏览器的主从架构
|
||||
|
||||
类似应用程序的主从模式,浏览器的 Browser 模块可以看作主模块,它本身用于协调其它模块的运行,并维持其它各模块的正常工作,在其它模块失去响应时等待或重新唤起,或者在模块销毁时进行内存回收。
|
||||
|
||||
各从模块也分工明确,比如在浏览器敲击 URL 地址时,会先通过 UI 模块响应用户的输入,并判断输入是否为 URL 地址,因为输入的可能是其它非法参数,或一些查询或设置命令。若输入的确实是 URL 地址,则校验通过后,会通知 Network 网络模块发送请求,UI 模块就不再关心请求是如何处理了。Network 模块也是相对独立的,仅处理请求的发送与接收,如果接收到的是 HTML 网页,则交给 Renderer 模块进行渲染。
|
||||
|
||||
有了这些相对独立且分工明确的模块划分后,将这些模块作为线程或进程管理就都不会影响它们的业务逻辑了,唯一影响的就是内存是否共享,以及某个模块 crash 后是否会影响到其它模块了,所以基于这个架构,判断设备类型,以采用单进程或多进程模式就变得简单了很多,且这个进程弹性架构本身也不需要入侵各模块业务逻辑,本身就是一套独立的机制。
|
||||
|
||||
浏览器作为非常复杂的应用程序,想要持续维护,就必须对每个功能点都进行合理的设计,让模块间高内聚、低耦合,这样才不至于让任何修改牵一发而动全身。
|
||||
|
||||
### tab、iframe 进程隔离
|
||||
|
||||
微前端的沙箱隔离方案也比较火,这里可以和浏览器 tab/iframe 隔离做个对比。
|
||||
|
||||
基于 js 运行时的沙箱方案大多都因为吐槽 iframe 慢而诞生的,一般会基于 `with` 改变沙箱代码的上下文,修改访问的全局对象引用,但基于 js 原型链特征,为了阻断向原型链追溯到主应用代码,一般会采用 `proxy` 对 `with` mock 的变量进行访问阻断。
|
||||
|
||||
还有一些方案利用创建空 iframe 获取到 document 变量传递给沙箱,一定程度做到了访问隔离,且对 document 添加的监听会随 iframe 销毁而销毁,便于控制。
|
||||
|
||||
还有一些更加彻底的尝试,将 js 代码扔到 web worker 运行,并通过 mock 模拟了 worker 运行时缺失的 dom API。
|
||||
|
||||
对比这些方案可以发现,只有最后 worker 的方案是最彻底的,因为浏览器创建的 worker 进程是完全资源隔离的,想要和浏览器主线程通信只能利用 `postMessage`,虽然有一些基于 ArrayBuffer 的内存共享方案,但因为支持的数据类型具有针对性,也不会存在安全问题。
|
||||
|
||||
回到浏览器开发者的视角,为什么 iframe 隔离要花费九牛二虎之力拆分多进程,最后再费很大功夫拼接回来,还原出一个相对无缝的体验?浏览器厂商其实完全可以利用上面提到的 js 运行时能力,对 API 语法进行改造,创建一个逻辑上的沙盒环境。
|
||||
|
||||
我认为本质原因是浏览器要实现的沙盒必须是进程层面的,也就是对内存访问权限的绝对隔离,因为逻辑层面的隔离可能随着各浏览器厂商实现差异,或 API 本身存在的逻辑漏洞而导致越权情况的出现,所以如果需要构造一个完全安全的沙盒,最好利用浏览器提供的 API 创建新的进程处理沙盒代码。
|
||||
|
||||
## 总结
|
||||
|
||||
本文介绍了浏览器是如何基于操作系统做宏观架构设计的,主要就说了一件事,即对进程,线程模型的弹性使用。同时在 tab、iframe 的设计中也要考虑到安全性要求,在必要的时候采用进程,在浏览器自身模块间因为没有安全性问题,所以可对进程模型进行灵活切换。
|
||||
|
||||
> 讨论地址是:[精读《深入了解现代浏览器一》· Issue #374 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/374)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,77 @@
|
||||
[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part2) 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第二篇。
|
||||
|
||||
## 概述
|
||||
|
||||
本篇重点介绍了 **浏览器路由跳转后发生了什么**,下一篇会介绍浏览器的渲染进程是如何渲染网页的,环环相扣。
|
||||
|
||||
在上一篇介绍了,browser process 包含 UI thread、network thread 和 storage thread,当我们在浏览器菜单栏输入网址并敲击回车时,这套动作均由 browser process 的 UI thread 响应。
|
||||
|
||||
接下来,按照几种不同的路由跳转场景,分别介绍了内部流程。
|
||||
|
||||
### 普通的跳转
|
||||
|
||||
第一步,UI thread 响应输入,并判断是否为一个合法的网址,当然输入的也可能是个搜索协议,这就会导致分发到另外的服务处理。
|
||||
|
||||
第二步,如果第一步输入的是合法网址,则 UI thread 会通知 network thread 获取网页内容,network thread 会寻找合适的协议处理网络请求,一般会通过 [DNS 协议](https://en.wikipedia.org/wiki/Domain_Name_System) 寻址,通过 [TLS 协议](https://en.wikipedia.org/wiki/Transport_Layer_Security) 建立安全链接。如果服务器返回了比如 301 重定向信息,network thread 会通知 UI thread 这个信息,再启动一遍第二步。
|
||||
|
||||
第三步,读取响应内容,在这一步 network thread 会首先读取首部一些字节,即我们常说的响应头,其中包含 [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) 告知返回内容是什么。如果返回内容是 HTML,则 network thread 会将数据传送给 renderer process。这一步还会校验安全性,比如 [CORB](https://www.chromium.org/Home/chromium-security/corb-for-developers) 或 [cross-site](https://en.wikipedia.org/wiki/Cross-site_scripting) 问题。
|
||||
|
||||
第四步,寻找 renderer process。一旦所有检查都完成,network thread 会通知 UI thread 已经准备好跳转了(注意此时并没有加载完所有数据,第三步只是检查了首字节),UI thread 会通知 renderer process 进行渲染。为了提升性能,UI thread 在通知 network thread 的同时就会实例化一个 renderer process 等着,一旦 network thread 完毕后就可以立即进入渲染阶段,如果检查失败则丢弃提前实例化的 renderer process。
|
||||
|
||||
第五步,确认导航。第四步后,browser process 通过 IPC 向 renderer process 传送 stream([精读《web streams》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/214.%E7%B2%BE%E8%AF%BB%E3%80%8Aweb%20streams%E3%80%8B.md))数据。此时导航会被确认,浏览器的各个状态(比如导航状态、前进后退历史)将会被修改,同时为了方便 tab 关闭后快速恢复,会话记录会被存储在硬盘。
|
||||
|
||||
额外步骤,加载完成。当 renderer process 加载完成后(具体做了什么下一篇会说明),会通知 browser process `onLoad` 事件,此时浏览器完成最终加载完毕状态,loading 圆圈也会消失,各类 onLoad 的回调触发。注意此时 js 可能会继续加载远程资源,但这都是加载状态完成后的事了。
|
||||
|
||||
### 跳转到别的网站
|
||||
|
||||
当你准备跳转到别的网站时,在执行普通跳转流程前,还会响应 [beforeunload](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) 事件,这个事件注册在 renderer process,所以 browser process 需要检查 renderer process 是否注册了这个响应。注册 `beforeunload` 无论如何都会拖慢关闭 tab 的速度,所以如无必要请勿注册。
|
||||
|
||||
如果跳转是 js 发出的,那么执行跳转就由 renderer process 触发,browser process 来执行,后续流程就是普通的跳转流程。要注意的是,当执行跳转时,会触发原网站 `unload` 等事件([网页生命周期](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#overview_of_page_lifecycle_states_and_events)),所以这个由旧的 renderer process 响应,而新网站会创建一个新的 renderer process 处理,当旧网页全部关闭时,才会销毁旧的 renderer process。
|
||||
|
||||
也就是说,即便只有一个 tab,在跳转时,也可能会在短时间内存在多个 renderer process。
|
||||
|
||||
### Service Worker
|
||||
|
||||
[Service Worker](https://developers.google.com/web/fundamentals/primers/service-workers) 可以在页面加载前执行一些逻辑,甚至改变网页内容,但浏览器仍然把 Service Worker 实现在了 renderer process 中。
|
||||
|
||||
当 Service Worker 被注册后,会被丢到一个作用域中,当 UI thread 执行时会检查这个作用域是否注册了 Service Worker,如果有,则 network thread 会创建一个 renderer process 执行 Service Worker(因为是 js 代码)。然后网络响应会被 Service Worker 接管。
|
||||
|
||||
但这样会慢一步,所以 UI thread 往往会在注册 Service Worker 的同时告诉 network thread 发送请求,这就是 [Navigation Preload](https://developers.google.com/web/updates/2017/02/navigation-preload) 机制。
|
||||
|
||||
本文介绍了网页跳转时发生的步骤,涉及 browser process、UI thread、network thread、renderer process 的协同。
|
||||
|
||||
## 精读
|
||||
|
||||
也许你会有疑问,为什么是 renderer process 而不是 renderer thread?因为相比 process(进程)相比 thread(线程),之间数据是被操作系统隔离的,为了网页间无法相互读取数据(mysite.com 读取你 baidu.com 正在输入的账号密码),浏览器必须为每个 tab 创建一个独立的进程,甚至每个 iframe 都必须是独立进程。
|
||||
|
||||
读完第二篇,应该能更深切的感受到模块间合理分工的重要性。
|
||||
|
||||
UI thread 处理浏览器 UI 的展现与用户交互,比如当前加载的状态变化,历史前进后退,浏览器地址栏的输入、校验与监听按下 Enter 等事件,但不会涉及诸如发送请求、解析网页内容、渲染等内容。
|
||||
|
||||
network thread 也仅处理网络相关的事情,它主要关心通信协议、安全协议,目标就是快速准确的找到网站服务器,并读取其内容。network thread 会读取内容头做一些前置判断,读取内容和 renderer process 做的事情是有一定重合的,但 network thread 读取内容头仅为了判断内容类型,以便交给渲染引擎还是下载管理器(比如一个 zip 文件),所以为了不让渲染引擎知道下载管理器的存在,读取内容头必须由 network thread 来做。
|
||||
|
||||
与 renderer process 的通信也是由 browser process 来做的,也就是 UI thread、network thread 一旦要创建或与 renderer process 通信,都会交由它们所在的 browser process 处理。
|
||||
|
||||
renderer process 仅处理渲染逻辑,它不关心是从哪来的,比如是网络请求过来的,还是 Service Worker 拦截后修改的,也不关心当前浏览器状态是什么,它只管按照约定的接口规范,在指定的节点抛出回调,而修改应用状态由其它关心的模块负责,比如 `onLoad` 回调触发后,browser process 处理浏览器的状态就是一个例子。
|
||||
|
||||
再比如 renderer process 里点击了一个新的跳转链接,这个事情发生在 renderer process,但会交给 browser process 处理,因为每个模块解耦的非常彻底,所以任何复杂工作都能找到一个能响应它的模块,而这个模块也只要处理这个复杂工作的一部分,其余部分交给其它模块就好了,这就是大型应用维护的秘诀。
|
||||
|
||||
所以在浏览器运行周期里,有着非常清晰的逻辑链路,这些模块必须事先规划设计好,很难想象这些模块分工是在开发中逐渐形成的。
|
||||
|
||||
最后提到加速优化,Chrome 惯用技巧就是,用资源换时间。即宁可浪费潜在资源,也要让事物尽可能的并发,这些从提前创建 renderer process、提前发起 network process 都能看出来。
|
||||
|
||||
## 总结
|
||||
|
||||
深入了解现代浏览器二介绍了网页跳转时发生的,browser process 与 renderer process 是如何协同的。
|
||||
|
||||
也许这篇文章可以帮助你回答 “聊聊在浏览器地址栏输入 www.baidu.com 并回车后发生了什么事儿吧!”
|
||||
|
||||
> 讨论地址是:[精读《深入了解现代浏览器二》· Issue #375 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/375)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,93 @@
|
||||
[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part3) 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第三篇。
|
||||
|
||||
## 概述
|
||||
|
||||
本篇宏观的介绍 renderer process 做了哪些事情。
|
||||
|
||||
浏览器 tab 内 html、css、javascript 内容基本上都由 renderer process 的主线程处理,除了一些 js 代码会放在 web worker 或 service worker 内,所以浏览器主线程核心工作就是解析 web 三剑客并生成可交互的用户界面。
|
||||
|
||||
### 解析阶段
|
||||
|
||||
首先 renderer process 主线程会解析 HTML 文本为 DOM(Document Object Model),直译为中文就是文档对象模型,所以首先要把文本结构化才能继续处理。不仅是浏览器,代码的解析也得首先经历 Parse 阶段。
|
||||
|
||||
对于 HTML 的 link、img、script 标签需要加载远程资源的,浏览器会调用 network thread 优先并行处理,但遇到 script 标签就必须停下来优先执行,因为 js 代码可能会改变任何 dom 对象,这可能导致浏览器要重新解析。所以如果你的代码没有修改 dom 的副作用,可以添加 async、defer 标签,或 JS 模块的方式使浏览器不必等待 js 的执行。
|
||||
|
||||
### 样式计算
|
||||
|
||||
只有 DOM 是不够的,style 标签申明的样式需要作用在 DOM 上,所以基于 DOM,浏览器要生成 CSSOM,这个 CSSOM 主要是基于 css 选择器(selector)确定作用节点的。
|
||||
|
||||
### 布局
|
||||
|
||||
有了 DOM、CSSOM 仍然不足以绘制网页,因为我们仅知道结构和样式,但不知道元素的位置,这就需要生成 LayoutTree 以描述布局的结构。
|
||||
|
||||
LayoutTree 和 DOM 结构很像了,但比如 `display: none` 的元素不会出现在 LayoutTree 上,所以 LayoutTree 仅考虑渲染结构,而 DOM 是一个综合描述结构,它不适合直接用来渲染。
|
||||
|
||||
原文特别提到,LayoutTree 有个很大的技术难点,即排版,Chrome 专门有一整个团队在攻克这个技术难题。为什么排版这么难?可以从这几个例子中体会冰山一角:盒模型间碰撞、字体撑开内容导致换行,引发更大区域的重新排版、一个盒模型撑开挤压另一个盒模型,但另一个盒模型大小变化后内容排版也随之变化,导致盒模型再次变化,这个变化又导致了外部其它盒模型的布局变化。
|
||||
|
||||
布局最难的地方在于,需要对所有奇奇怪怪的布局定式做一个尽量合理的处理,而很多时候布局定式间规则是相互冲突的。而且这还不考虑布局引擎的修改在数亿网页上引发未知 BUG 的风险。
|
||||
|
||||
### 绘图
|
||||
|
||||
有了 DOM、CSSOM、LayoutTree 就够了吗?还不行,还缺少最后一环 PaintRecord,这个指绘图记录,它会记录元素的层级关系,以决定元素绘制的顺序。因为 LayoutTree 仅决定了物理结构,但不决定元素的上下空间结构。
|
||||
|
||||
有了 DOM、CSSOM、LayoutTree、PaintRecord 之后,终于可以绘图了。然而当 HTML 变化时,重绘的代价是巨大的,因为上面任何一步的计算结果都依赖前面一步,HTML 改变时,需要对 DOM、CSSOM、LayoutTree、PaintRecord 进行重新计算。
|
||||
|
||||
大部分时候浏览器都可以在 16ms 内完成,使 FPS 保持在 60 左右,但当页面结构过于复杂,这些计算本身超过了 16ms,或其中遇到 js 代码的阻塞,都会导致用户感觉到卡顿。当然对于 js 卡顿问题可以通过 `requestAnimationFrame` 把逻辑运算分散在各帧空闲时进行,也可以独立到 web worker 里。
|
||||
|
||||
### 合成
|
||||
|
||||
绘图的步骤称为 rasterizing(光栅化)。在 Chrome 最早发布时,采用了一种较为简单的光栅化方案,即仅渲染可视区域内的像素点,当滚动后,再补充渲染当前滚动位置的像素点。这样做会导致渲染永远滞后于滚动。
|
||||
|
||||
现在一般采用较为成熟的合成技术(compositing),即将渲染内容分层绘制与渲染,这可以大大提升性能,并可通过 CSS 属性 `will-change` 手动申明为一个新层(不要滥用)。
|
||||
|
||||
浏览器会根据 LayoutTree 分析后得到 LayerTree(层树),并根据它逐层渲染。
|
||||
|
||||
合成层会将绘图内容切分为多个栅格并交由 GPU 渲染,因此性能会非常好。
|
||||
|
||||
## 精读
|
||||
|
||||
### 从渲染分层看性能优化
|
||||
|
||||
本篇提到了浏览器渲染的 5 个重要环节:解析、样式、布局、绘图、合成,是前端开发者日常工作中对浏览器体感最深的部分,也是优化最长发生在的部分。
|
||||
|
||||
其实从性能优化角度来看,解析环节可以被替代为 JS 环节,因为现代 JS 框架往往没有什么 HTML 模版内容要解析,几乎全是 JS 操作 DOM,所以可以看作 5 个新环节:JS、样式、布局、绘图、合成。
|
||||
|
||||
值得注意的是,几乎每层的计算都依赖上层的结果,但并不是每层都一定会重复计算,我们需要尤其注意以下几种情况:
|
||||
|
||||
1. 修改元素几何属性(位置、宽高等)会触发所有层的重新计算,因为这是一个非常重量级的修改。
|
||||
2. 修改某个元素绘图属性(比如颜色和背景色),并不影响位置,则会跳过布局层。
|
||||
3. 修改比如 transform 属性会跳过布局与绘图层,这看上去很不可思议。
|
||||
|
||||
对于第三点,由于 transform 的内容会提升到合成层并交由 GPU 渲染,因此并不会与浏览器主线程的布局、绘图放在一起处理,所以视觉上这个元素的确产生了位移,但它和修改 `left`、`top` 的位移在实现上却有本质的不同。
|
||||
|
||||
所以站在浏览器开发者的角度,可以轻松理解为什么这种优化不是奇技淫巧了,因为本身浏览器的实现就把布局、绘图与合成层的行为分离开了,不同的代码底层方案不同,性能肯定会不同。你可以通过 [csstriggers](https://csstriggers.com/) 查看不同 css 属性会引发哪些层的重计算。
|
||||
|
||||
当然作为开发者还是可以吐槽,为什么浏览器不能 “自动把 `left` `top` 与 `transform` 的实现细节屏蔽,并自动进行合理的分层”,然而如果浏览器厂商做不到这一点,开发者还是主动去了解实现原理吧。
|
||||
|
||||
### 隐式合成层、层爆炸、层自动合并
|
||||
|
||||
除了 `transform`、`will-change` 属性外,还有很多种情况元素会提升到合成层,比如 `video`、`canvas`、`iframe`,或 `fixed` 元素,但这些都有明确的规则,所以属于显示合成。
|
||||
|
||||
而隐式合成是指元素没有被特别标记,但也被提升到合成层的情况,这种情况常见发生在 `z-index` 元素产生重叠时,下方的元素显示申明提升到合成层,则浏览器为了保证 `z-index` 覆盖关系,就要隐式把上方的元素提升到合成层。
|
||||
|
||||
层爆炸是指隐式合成的原因,当 css 出现一些复杂行为时(比如轨迹动画),浏览器无法实时捕捉哪些元素位于当前元素上方,所以只好把所有元素都提升到合成层,当合成层数量过多,主线程与 GPU 的通信可能会成为瓶颈,反而影响性能。
|
||||
|
||||
浏览器也会支持层自动合并,比如隐式提升到合成层时,多个元素会自动合并在一个合成层里。但这种方式也并不总是靠谱,自动处理毕竟猜不到开发者的意图,所以最好的优化方式是开发者主动干预。
|
||||
|
||||
我们只要注意将所有显示提升到合成层的元素放在 `z-index` 的上方,这样浏览器就有了判断依据,不用再担惊受怕会不会这个元素突然移动到某个元素的位置,导致压住了那个元素,于是又不得不把这个元素给隐式提升到合成层以保证它们之间顺序的正确性,因为这个元素本来就位于其它元素的最上方。
|
||||
|
||||
## 总结
|
||||
|
||||
读完这篇文章,希望你能根据浏览器在渲染进程的实现原理,总结出更多代码级别的性能优化经验。
|
||||
|
||||
最后想要吐槽的是,浏览器规范由于是逐步迭代的,因此看似都在描述位置的 css 属性其实背后实现原理是不同的,虽然这个规则体现在 W3C 规范上,但如果仅从属性名是很难看出来端倪的,因此想要做极致性能优化就必须了解浏览器实现原理。
|
||||
|
||||
> 讨论地址是:[精读《深入了解现代浏览器三》· Issue #379 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/379)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,149 @@
|
||||
[Inside look at modern web browser](https://developers.google.com/web/updates/2018/09/inside-browser-part4) 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第四篇。
|
||||
|
||||
## 概述
|
||||
|
||||
前几章介绍了浏览器的基础进程、线程以及它们之间协同的关系,并重点说到了渲染进程是如何处理页面绘制的,那么最后一章也就深入到了浏览器是如何处理页面中事件的。
|
||||
|
||||
全篇站在浏览器实现的视角思考问题,非常有趣。
|
||||
|
||||
### 输入进入合成器
|
||||
|
||||
这是第一小节的标题。乍一看可能不明白在说什么,但这句话就是本文的核心知识点。为了更好的理解这句话,先要解释输入与合成器是什么:
|
||||
|
||||
- 输入:不仅包括输入框的输入,其实所有用户操作在浏览器眼中都是输入,比如滚动、点击、鼠标移动等等。
|
||||
- 合成器:第三节说过的,渲染的最后一步,这一步在 GPU 进行光栅化绘图,如果与浏览器主线程解耦的化效率会非常高。
|
||||
|
||||
所以输入进入合成器的意思是指,在浏览器实际运行的环境中,合成器不得不响应输入,这可能会导致合成器本身渲染被阻塞,导致页面卡顿。
|
||||
|
||||
### "non-fast" 滚动区域
|
||||
|
||||
由于 js 代码可以绑定事件监听,而且事件监听中存在一种 `preventDefault()` 的 API 可以阻止事件的原生效果比如滚动,所以在一个页面中,浏览器会对所有创建了此监听的区块标记为 "non-fast" 滚动区域。
|
||||
|
||||
注意,只要创建了 `onwheel` 事件监听就会标记,而不是说调用了 `preventDefault()` 才会标记,因为浏览器不可能知道业务什么时候调用,所以只能一刀切。
|
||||
|
||||
为什么这种区域被称为 "non-fast"?因为在这个区域触发事件时,合成器必须与渲染进程通信,让渲染进程执行 js 事件监听代码并获得用户指令,比如是否调用了 `preventDefault()` 来阻止滚动?如果阻止了就终止滚动,如果没有阻止才会继续滚动,如果最终结果是不阻止,但这个等待时间消耗是巨大的,在低性能设备比如手机上,滚动延迟甚至有 10~100ms。
|
||||
|
||||
然而这并不是设备性能差导致的,因为滚动是在合成器发生的,如果它可以不与渲染进程通信,那么即便是 500 元的安卓机也可以流畅的滚动。
|
||||
|
||||
### 注意事件委托
|
||||
|
||||
更有意思的是,浏览器支持一种事件委托的 API,它可以将事件委托到其父节点一并监听。
|
||||
|
||||
这本是一个非常方便的 API,但对浏览器实现可能是一个灾难:
|
||||
|
||||
```js
|
||||
document.body.addEventListener('touchstart', event => {
|
||||
if (event.target === area) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
如果浏览器解析到上面的代码,只能用无语来形容。因为这意味着必须对全页面都进行 "non-fast" 标记,因为代码委托的是整个 document!这会导致滚动非常慢,因为在页面任何地方滚动都要发生一次合成器与渲染进程的通信。
|
||||
|
||||
所以最好的办法就是不要写这种监听。但还有一种方案是,告诉浏览器你不会 `preventDefault()`,这是因为 chrome 通过对应用源码统计后发现,大约 80% 的事件监听没有 `preventDefault()`,而仅仅是做别的事情,所以合成器应该可以与渲染进程的事件处理并行进行,这样既不卡顿,逻辑也不会丢失。所以添加了一种 `passive: true` 的标记,标识当前事件可以并行处理:
|
||||
|
||||
```js
|
||||
document.body.addEventListener('touchstart', event => {
|
||||
if (event.target === area) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}, {passive: true});
|
||||
```
|
||||
|
||||
这样就不会卡顿了,但 `preventDefault()` 也会失效。
|
||||
|
||||
### 检查事件是否可取消
|
||||
|
||||
对于 `passive: true` 的情况,事件就实际上变得不可取消了,所以我们最好在代码里做一层判断:
|
||||
|
||||
```js
|
||||
document.body.addEventListener('touchstart', event => {
|
||||
if (event.cancelable && event.target === area) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}, {passive: true});
|
||||
```
|
||||
|
||||
然而这仅仅是阻止执行没有意义的 `preventDefault()`,并不能阻止滚动。这种情况下,最好的办法是通过 css 申明来阻止横向移动,因为这个判断不会发生在渲染进程,所以不会导致合成器与渲染进程的通信:
|
||||
|
||||
```css
|
||||
#area {
|
||||
touch-action: none;
|
||||
}
|
||||
```
|
||||
|
||||
### 事件合并
|
||||
|
||||
由于事件触发频率可能比浏览器帧率还要高(1 秒 120 次),如果浏览器坚持对每个事件都进行响应,而一次事件都必须在 js 里响应一次的话,会导致大量事件阻塞,因为当 FPS 为 60 时,一秒也仅能执行 60 次事件响应,所以事件积压是无法避免的。
|
||||
|
||||
为了解决这个问题,浏览器在针对可能导致积压的事件,比如滚动事件时,将多个事件合并到一次 js 中,仅保留最终状态。
|
||||
|
||||
如果不希望丢掉事件中间过程,可以使用 `getCoalescedEvents` 从合并事件中找回每一步事件的状态:
|
||||
|
||||
```js
|
||||
window.addEventListener('pointermove', event => {
|
||||
const events = event.getCoalescedEvents();
|
||||
for (let event of events) {
|
||||
const x = event.pageX;
|
||||
const y = event.pageY;
|
||||
// draw a line using x and y coordinates.
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 精读
|
||||
|
||||
只要我们认识到事件监听必须运行在渲染进程,而现代浏览器许多高性能 “渲染” 其实都在合成层采用 GPU 做,所以看上去方便的事件监听肯定会拖慢页面流畅度。
|
||||
|
||||
但就这件事在 React 17 中有过一次讨论 [Touch/Wheel Event Passiveness in React 17](https://github.com/facebook/react/issues/19651)(实际上在即将到来的 18 该问题还在讨论中 [React 18 not passive wheel / touch event listeners support](https://github.com/facebook/react/issues/22794)),因为 React 可以直接在元素上监听 Touch、Wheel 事件,但其实框架采用了委托的方式在 document(后在 app 根节点)统一监听,这就导致了用户根本无从决定事件是否为 `passive`,如果框架默认 `passive`,会导致 `preventDefault()` 失效,否则性能得不到优化。
|
||||
|
||||
就结论而言,React 目前还是对几个受影响的事件 `touchstart` `touchmove` `wheel` 采用 `passive` 模式,即:
|
||||
|
||||
```tsx
|
||||
const Test = () => (
|
||||
<div
|
||||
// 没有用的,无法阻止滚动,因为委托处默认 passive
|
||||
onWheel={event => event.preventDefault()}
|
||||
>
|
||||
...
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
虽然结论如此而且对性能友好,但并不是一个让所有人都能满意的方案,我们看看当时 Dan 是如何思考,并给了哪些解决方案的。
|
||||
|
||||
首先背景是,React 16 事件委托绑定在 document 上,React 17 事件委托绑定在 App 根节点上,而根据 chrome 的优化,绑定在 document 的事件委托默认是 `passive` 的,而其它节点的不会,因此对 React 17 来说,如果什么都不做,仅改变绑定节点位置,就会存在一个 Break Change。
|
||||
|
||||
1. 第一种方案是坚持 Chrome 性能优化的精神,委托时依然 pasive 处理。这样处理至少和 React 16 一样,`preventDefault()` 都是失效的,虽然不正确,但至少不是 BreakChange。
|
||||
2. 第二种方案即什么都不做,这导致原本默认 `passive` 的因为绑定到非 document 节点上而 `non-passive` 了,这样做不仅有性能问题,而且 API 会存在 BreackChange,虽然这种做法更 “原生”。
|
||||
3. touch/wheel 不再采用委托,意味着浏览器可以有更少的 "non-fast" 区域,而 `preventDefault()` 也可以生效了。
|
||||
|
||||
最终选择了第一个方案,因为暂时不希望在 React API 层面出现行为不一致的 BreakChange。
|
||||
|
||||
然而 React 18 是一次 BreakChange 的时机,目前还没有进一步定论。
|
||||
|
||||
## 总结
|
||||
|
||||
从浏览器角度看待问题会让你具备上帝视角而不是开发者视角,你不会再觉得一些奇奇怪怪的优化逻辑是 Hack 了,因为你了解浏览器背后是如何理解与实现的。
|
||||
|
||||
不过我们也会看到一些和实现强绑定的无奈,在前端开发框架实现时造成了不可避免的困扰。毕竟作为一个不了解浏览器实现的开发者,自然会认为 `preventDefault()` 绑定在滚动事件时,一定可以阻止默认滚动行为呀,但为什么因为:
|
||||
|
||||
- 浏览器分为合成层和渲染进程,通信成本较高导致滚动事件监听会引发滚动卡顿。
|
||||
- 为了避免通信,浏览器默认为 document 绑定开启 `passive` 策略减少 "non-fast" 区域。
|
||||
- 开启了 `passive` 的事件监听 `preventDefault()` 会失效,因为这层实现在 js 里而不是 GPU。
|
||||
- React16 采用事件代理,把元素 `onWheel` 代理到 document 节点而非当前节点。
|
||||
- React17 将 document 节点绑定下移到了 App 根节点,因此浏览器优化后的 `passive` 失效了。
|
||||
- React 为了保持 API 不发生 BreakChange,因此将 App 根节点绑定的事件委托默认补上了 `passive`,使其表现与绑定在 document 一样。
|
||||
|
||||
总之就是 React 与浏览器实现背后的纠纷,导致滚动行为阻止失效,而这个结果链条传导到了开发者身上,而且有明显感知。但了解背后原因后,你应该能理解一下 React 团队的痛苦吧,因为已有 API 确实没有办法描述是否 `passive` 这个行为,所以这是个暂时无法解决的问题。
|
||||
|
||||
> 讨论地址是:[精读《深入了解现代浏览器四》· Issue #381 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/381)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,657 @@
|
||||
immutablejs、immer 等库已经让 js 具备了 immutable 编程的可能性,但还存在一些无解的问题,即 “怎么保证一个对象真的不可变”。
|
||||
|
||||
如果不是拍胸脯担保,现在还真没别的办法。或许你觉得 `frozen` 是个 good idea,但它内部仍然可以增加非 `frozen` 的 key。
|
||||
|
||||
另一个问题是,当我们 debug 调试应用数据的时候,看到状态发生 `[]` -> `[]` 变化时,无论在控制台、断点、redux devtools 还是 `.toString()` 都看不出来引用有没有变化,除非把变量值分别拿到进行 `===` 运行时判断。但引用变与没变可是一个大问题,它甚至能决定业务逻辑的正确与否。
|
||||
|
||||
但现阶段我们没有任何处理办法,如果不能接受完全使用 Immutablejs 定义对象,就只能摆胸脯保证自己的变更一定是 immutable 的,这就是 js 不可变编程被许多聪明人吐槽的原因,觉得在不支持 immutable 的编程语言下强行应用不可变思维是一种很别扭的事。
|
||||
|
||||
[proposal-record-tuple](https://github.com/tc39/proposal-record-tuple) 解决的就是这个问题,它让 js 原生支持了 **不可变数据类型**(高亮、加粗)。
|
||||
|
||||
## 概述 & 精读
|
||||
|
||||
JS 有 7 种原始类型:string, number, bigint, boolean, undefined, symbol, null. 而 Records & Tuples 提案一下就增加了三种原始类型!这三种原始类型完全是为 immutable 编程环境服务的,也就是说,可以让 js 开出一条原生 immutable 赛道。
|
||||
|
||||
这三种原始类型分别是 Record, Tuple, Box:
|
||||
|
||||
- Record: 类对象结构的深度不可变基础类型,如 `#{ x: 1, y: 2 }`。
|
||||
- Tuple: 类数组结构的深度不可变基础类型,如 `#[1, 2, 3, 4]`。
|
||||
- Box: 可以定义在上面两个类型中,存储对象,如 `#{ prop: Box(object) }`。
|
||||
|
||||
核心思想可以总结为一句话:因为这三个类型为基础类型,所以在比较时采用值对比(而非引用对比),因此 `#{ x: 1, y: 2} === #{ x: 1, y: 2 }`。这真的解决了大问题!如果你还不了解 js 不支持 immutable 之痛,请不要跳过下一节。
|
||||
|
||||
### js 不支持 immutable 之痛
|
||||
|
||||
虽然很多人都喜欢 mvvm 的 reactive 特征(包括我也写了不少 mvvm 轮子和框架),但不可变数据永远是开发大型应用最好的思想,它可以非常可靠的保障应用数据的可预测性,同时不需要牺牲性能与内存,它使用起来没有 mutable 模式方便,但它永远不会出现预料外的情况,这对打造稳定的复杂应用至关重要,甚至比便捷性更加重要。当然可测试也是个非常重要的点,这里不详细展开。
|
||||
|
||||
然而 js 并不原生支持 immutable,这非常令人头痛,也造成了许多困扰,下面我试图解释一下这个困扰。
|
||||
|
||||
如果你觉得非原始类型按照引用对比很棒,那你一定一眼能看出下面的结果是正确的:
|
||||
|
||||
```js
|
||||
assert({ a: 1 } !== { a: 1 })
|
||||
```
|
||||
|
||||
但如果是下面的情况呢?
|
||||
|
||||
```js
|
||||
console.log(window.a) // { a: 1 }
|
||||
console.log(window.b) // { a: 1 }
|
||||
assert(window.a === window.b) // ???
|
||||
```
|
||||
|
||||
**结果是不确定**,虽然这两个对象长得一样,但我们拿到的 scope 无法推断其是否来自同一个引用,如果来自于相同的引用,则断言通过,否则即便看上去值一样,也会 throw error。
|
||||
|
||||
更大的麻烦是,即便这两个对象长得完全不一样,我们也不敢轻易下结论:
|
||||
|
||||
```js
|
||||
console.log(window.a) // { a: 1 }
|
||||
// do some change..
|
||||
console.log(window.b) // { b: 1 }
|
||||
assert(window.a === window.b) // ???
|
||||
```
|
||||
|
||||
因为 b 的值可能在中途被修改,但确实与 a 来自同一个引用,我们无法断定结果到底是什么。
|
||||
|
||||
另一个问题则是应用状态变更的扑朔迷离。试想我们开发了一个树形菜单,结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"label": "root",
|
||||
"children": [{
|
||||
"id": "2",
|
||||
"label": "apple",
|
||||
}, {
|
||||
"id": "3",
|
||||
"label": "orange",
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
如果我们调用 `updateTreeNode('3', { id: '3', title: 'banana' })`,在 immutable 场景下我们仅更新 id 为 "1", "3" 组件的引用,而 id 为 "2" 的引用不变,那么这棵树节点 "2" 就不会重渲染,这是血统纯正的 immutable 思维逻辑。
|
||||
|
||||
但当我们保存下这个新状态后,要进行 “状态回放”,会发现其实应用状态进行了一次变更,整个描述 json 变成了:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"label": "root",
|
||||
"children": [{
|
||||
"id": "2",
|
||||
"label": "apple",
|
||||
}, {
|
||||
"id": "3",
|
||||
"label": "banana",
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
但如果我们拷贝上面的文本,把应用状态直接设置为这个结果,会发现与 “应用回放按钮” 的效果不同,这时 id "2" 也重渲染了,因为它的引用变化了。
|
||||
|
||||
问题就是我们无法根据肉眼观察出引用是否变化了,即便两个结构一模一样,也无法保证引用是否相同,进而导致无法推断应用的行为是否一致。如果没有人为的代码质量管控,出现非预期的引用更新几乎是难以避免的。
|
||||
|
||||
这就是 Records & Tuples 提案要解决问题的背景,我们带着这个理解去看它的定义,就更好学习了。
|
||||
|
||||
### Records & Tuples 在用法上与对象、数组保持一致
|
||||
|
||||
Records & Tuples 提案说明,不可变数据结构除了定义时需要用 `#` 符号申明外,使用时与普通对象、数组无异。
|
||||
|
||||
Record 用法与普通 object 几乎一样:
|
||||
|
||||
```js
|
||||
const proposal = #{
|
||||
id: 1234,
|
||||
title: "Record & Tuple proposal",
|
||||
contents: `...`,
|
||||
// tuples are primitive types so you can put them in records:
|
||||
keywords: #["ecma", "tc39", "proposal", "record", "tuple"],
|
||||
};
|
||||
|
||||
// Accessing keys like you would with objects!
|
||||
console.log(proposal.title); // Record & Tuple proposal
|
||||
console.log(proposal.keywords[1]); // tc39
|
||||
|
||||
// Spread like objects!
|
||||
const proposal2 = #{
|
||||
...proposal,
|
||||
title: "Stage 2: Record & Tuple",
|
||||
};
|
||||
console.log(proposal2.title); // Stage 2: Record & Tuple
|
||||
console.log(proposal2.keywords[1]); // tc39
|
||||
|
||||
// Object functions work on Records:
|
||||
console.log(Object.keys(proposal)); // ["contents", "id", "keywords", "title"]
|
||||
```
|
||||
|
||||
下面的例子说明,Records 与 object 在函数内处理时并没有什么不同,这个在 FAQ 里提到是一个非常重要的特性,可以让 immutable 完全融入现在的 js 生态:
|
||||
|
||||
```js
|
||||
const ship1 = #{ x: 1, y: 2 };
|
||||
// ship2 is an ordinary object:
|
||||
const ship2 = { x: -1, y: 3 };
|
||||
|
||||
function move(start, deltaX, deltaY) {
|
||||
// we always return a record after moving
|
||||
return #{
|
||||
x: start.x + deltaX,
|
||||
y: start.y + deltaY,
|
||||
};
|
||||
}
|
||||
|
||||
const ship1Moved = move(ship1, 1, 0);
|
||||
// passing an ordinary object to move() still works:
|
||||
const ship2Moved = move(ship2, 3, -1);
|
||||
|
||||
console.log(ship1Moved === ship2Moved); // true
|
||||
// ship1 and ship2 have the same coordinates after moving
|
||||
```
|
||||
|
||||
Tuple 用法与普通数组几乎一样:
|
||||
|
||||
```js
|
||||
const measures = #[42, 12, 67, "measure error: foo happened"];
|
||||
|
||||
// Accessing indices like you would with arrays!
|
||||
console.log(measures[0]); // 42
|
||||
console.log(measures[3]); // measure error: foo happened
|
||||
|
||||
// Slice and spread like arrays!
|
||||
const correctedMeasures = #[
|
||||
...measures.slice(0, measures.length - 1),
|
||||
-1
|
||||
];
|
||||
console.log(correctedMeasures[0]); // 42
|
||||
console.log(correctedMeasures[3]); // -1
|
||||
|
||||
// or use the .with() shorthand for the same result:
|
||||
const correctedMeasures2 = measures.with(3, -1);
|
||||
console.log(correctedMeasures2[0]); // 42
|
||||
console.log(correctedMeasures2[3]); // -1
|
||||
|
||||
// Tuples support methods similar to Arrays
|
||||
console.log(correctedMeasures2.map(x => x + 1)); // #[43, 13, 68, 0]
|
||||
```
|
||||
|
||||
在函数内处理时,拿到一个数组或 Tuple 并没有什么需要特别注意的区别:
|
||||
|
||||
```js
|
||||
const ship1 = #[1, 2];
|
||||
// ship2 is an array:
|
||||
const ship2 = [-1, 3];
|
||||
|
||||
function move(start, deltaX, deltaY) {
|
||||
// we always return a tuple after moving
|
||||
return #[
|
||||
start[0] + deltaX,
|
||||
start[1] + deltaY,
|
||||
];
|
||||
}
|
||||
|
||||
const ship1Moved = move(ship1, 1, 0);
|
||||
// passing an array to move() still works:
|
||||
const ship2Moved = move(ship2, 3, -1);
|
||||
|
||||
console.log(ship1Moved === ship2Moved); // true
|
||||
// ship1 and ship2 have the same coordinates after moving
|
||||
```
|
||||
|
||||
由于 Record 内不能定义普通对象(比如定义为 # 标记的不可变对象),如果非要使用普通对象,只能包裹在 Box 里,并且在获取值时需要调用 `.unbox()` 拆箱,并且就算修改了对象值,在 Record 或 Tuple 层面也不会认为发生了变化:
|
||||
|
||||
```js
|
||||
const myObject = { x: 2 };
|
||||
|
||||
const record = #{
|
||||
name: "rec",
|
||||
data: Box(myObject)
|
||||
};
|
||||
|
||||
console.log(record.data.unbox().x); // 2
|
||||
|
||||
// The box contents are classic mutable objects:
|
||||
record.data.unbox().x = 3;
|
||||
console.log(myObject.x); // 3
|
||||
|
||||
console.log(record === #{ name: "rec", data: Box(myObject) }); // true
|
||||
```
|
||||
|
||||
另外不能在 Records & Tuples 内使用任何普通对象或 new 对象实例,除非已经用转化为了普通对象:
|
||||
|
||||
```js
|
||||
const instance = new MyClass();
|
||||
const constContainer = #{
|
||||
instance: instance
|
||||
};
|
||||
// TypeError: Record literals may only contain primitives, Records and Tuples
|
||||
|
||||
const tuple = #[1, 2, 3];
|
||||
|
||||
tuple.map(x => new MyClass(x));
|
||||
// TypeError: Callback to Tuple.prototype.map may only return primitives, Records or Tuples
|
||||
|
||||
// The following should work:
|
||||
Array.from(tuple).map(x => new MyClass(x))
|
||||
```
|
||||
|
||||
### 语法
|
||||
|
||||
Records & Tuples 内只能使用 Record、Tuple、Box:
|
||||
|
||||
```js
|
||||
#{}
|
||||
#{ a: 1, b: 2 }
|
||||
#{ a: 1, b: #[2, 3, #{ c: 4 }] }
|
||||
#[]
|
||||
#[1, 2]
|
||||
#[1, 2, #{ a: 3 }]
|
||||
```
|
||||
|
||||
不支持空数组项:
|
||||
|
||||
```js
|
||||
const x = #[,]; // SyntaxError, holes are disallowed by syntax
|
||||
```
|
||||
|
||||
为了防止引用追溯到上层,破坏不可变性质,不支持定义原型链:
|
||||
|
||||
```js
|
||||
const x = #{ __proto__: foo }; // SyntaxError, __proto__ identifier prevented by syntax
|
||||
|
||||
const y = #{ ["__proto__"]: foo }; // valid, creates a record with a "__proto__" property.
|
||||
```
|
||||
|
||||
也不能在里面定义方法:
|
||||
|
||||
```js
|
||||
#{ method() { } } // SyntaxError
|
||||
```
|
||||
|
||||
同时,一些破坏不可变稳定结构的特性也是非法的,比如 key 不可以是 Symbol:
|
||||
|
||||
```js
|
||||
const record = #{ [Symbol()]: #{} };
|
||||
// TypeError: Record may only have string as keys
|
||||
```
|
||||
|
||||
不能直接使用对象作为 value,除非用 Box 包裹:
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
const record = #{ prop: obj }; // TypeError: Record may only contain primitive values
|
||||
const record2 = #{ prop: Box(obj) }; // ok
|
||||
```
|
||||
|
||||
### 判等
|
||||
|
||||
判等是最核心的地方,Records & Tuples 提案要求 == 与 === 原生支持 immutable 判等,是 js 原生支持 immutable 的一个重要表现,所以其判等逻辑与普通的对象判等大相径庭:
|
||||
|
||||
首先看上去值相等,就真的相等,因为基础类型仅做值对比:
|
||||
|
||||
```js
|
||||
assert(#{ a: 1 } === #{ a: 1 });
|
||||
assert(#[1, 2] === #[1, 2]);
|
||||
```
|
||||
|
||||
这与对象判等完全不同,而且把 Record 转换为对象后,判等就遵循对象的规则了:
|
||||
|
||||
```js
|
||||
assert({ a: 1 } !== { a: 1 });
|
||||
assert(Object(#{ a: 1 }) !== Object(#{ a: 1 }));
|
||||
assert(Object(#[1, 2]) !== Object(#[1, 2]));
|
||||
```
|
||||
|
||||
另外 Records 的判等与 key 的顺序无关,因为有个隐式 key 排序规则:
|
||||
|
||||
```js
|
||||
assert(#{ a: 1, b: 2 } === #{ b: 2, a: 1 });
|
||||
|
||||
Object.keys(#{ a: 1, b: 2 }) // ["a", "b"]
|
||||
Object.keys(#{ b: 2, a: 1 }) // ["a", "b"]
|
||||
```
|
||||
|
||||
Box 是否相等取决于内部对象引用是否相等:
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
assert(Box(obj) === Box(obj));
|
||||
assert(Box({}) !== Box({}));
|
||||
```
|
||||
|
||||
对于 `+0` `-0` 之间,`NaN` 与 `NaN` 对比,都可以安全判定为相等,但 `Object.is` 因为是对普通对象的判断逻辑,所以会认为 `#{ a: -0 }` 不等于 `#{ a: +0 }`,因为认为 `-0` 不等于 `+0`,这里需要特别注意。另外 Records & Tulpes 也可以作为 Map、Set 的 key,并且按照值相等来查找:
|
||||
|
||||
```js
|
||||
assert(#{ a: 1 } === #{ a: 1 });
|
||||
assert(#[1] === #[1]);
|
||||
|
||||
assert(#{ a: -0 } === #{ a: +0 });
|
||||
assert(#[-0] === #[+0]);
|
||||
assert(#{ a: NaN } === #{ a: NaN });
|
||||
assert(#[NaN] === #[NaN]);
|
||||
|
||||
assert(#{ a: -0 } == #{ a: +0 });
|
||||
assert(#[-0] == #[+0]);
|
||||
assert(#{ a: NaN } == #{ a: NaN });
|
||||
assert(#[NaN] == #[NaN]);
|
||||
assert(#[1] != #["1"]);
|
||||
|
||||
assert(!Object.is(#{ a: -0 }, #{ a: +0 }));
|
||||
assert(!Object.is(#[-0], #[+0]));
|
||||
assert(Object.is(#{ a: NaN }, #{ a: NaN }));
|
||||
assert(Object.is(#[NaN], #[NaN]));
|
||||
|
||||
// Map keys are compared with the SameValueZero algorithm
|
||||
assert(new Map().set(#{ a: 1 }, true).get(#{ a: 1 }));
|
||||
assert(new Map().set(#[1], true).get(#[1]));
|
||||
assert(new Map().set(#[-0], true).get(#[0]));
|
||||
```
|
||||
|
||||
### 对象模型如何处理 Records & Tuples
|
||||
|
||||
对象模型是指 `Object` 模型,大部分情况下,所有能应用于普通对象的方法都可无缝应用于 Record,比如 `Object.key` 或 `in` 都可与处理普通对象无异:
|
||||
|
||||
```js
|
||||
const keysArr = Object.keys(#{ a: 1, b: 2 }); // returns the array ["a", "b"]
|
||||
assert(keysArr[0] === "a");
|
||||
assert(keysArr[1] === "b");
|
||||
assert(keysArr !== #["a", "b"]);
|
||||
assert("a" in #{ a: 1, b: 2 });
|
||||
```
|
||||
|
||||
值得一提的是如果 wrapper 了 `Object` 在 Record 或 Tuple,提案还准备了一套完备的实现方案,即 `Object(record)` 或 `Object(tuple)` 会冻结所有属性,并将原型链最高指向 `Tuple.prototype`,对于数组跨界访问也只能返回 undefined 而不是沿着原型链追溯。
|
||||
|
||||
### Records & Tuples 的标准库支持
|
||||
|
||||
对 Record 与 Tuple 进行原生数组或对象操作后,返回值也是 immutable 类型的:
|
||||
|
||||
```js
|
||||
assert(Object.keys(#{ a: 1, b: 2 }) !== #["a", "b"]);
|
||||
assert(#[1, 2, 3].map(x => x * 2), #[2, 4, 6]);
|
||||
```
|
||||
|
||||
还可通过 `Record.fromEntries` 和 `Tuple.from` 方法把普通对象或数组转成 Record, Tuple:
|
||||
|
||||
```js
|
||||
const record = Record({ a: 1, b: 2, c: 3 });
|
||||
const record2 = Record.fromEntries([#["a", 1], #["b", 2], #["c", 3]]); // note that an iterable will also work
|
||||
const tuple = Tuple(...[1, 2, 3]);
|
||||
const tuple2 = Tuple.from([1, 2, 3]); // note that an iterable will also work
|
||||
|
||||
assert(record === #{ a: 1, b: 2, c: 3 });
|
||||
assert(tuple === #[1, 2, 3]);
|
||||
Record.from({ a: {} }); // TypeError: Can't convert Object with a non-const value to Record
|
||||
Tuple.from([{}, {} , {}]); // TypeError: Can't convert Iterable with a non-const value to Tuple
|
||||
```
|
||||
|
||||
此方法不支持嵌套,因为标准 API 仅考虑一层,递归一般交给业务或库函数实现,就像 `Object.assign` 一样。
|
||||
|
||||
Record 与 Tuple 也都是可迭代的:
|
||||
|
||||
```js
|
||||
const tuple = #[1, 2];
|
||||
|
||||
// output is:
|
||||
// 1
|
||||
// 2
|
||||
for (const o of tuple) { console.log(o); }
|
||||
|
||||
const record = #{ a: 1, b: 2 };
|
||||
|
||||
// TypeError: record is not iterable
|
||||
for (const o of record) { console.log(o); }
|
||||
|
||||
// Object.entries can be used to iterate over Records, just like for Objects
|
||||
// output is:
|
||||
// a
|
||||
// b
|
||||
for (const [key, value] of Object.entries(record)) { console.log(key) }
|
||||
```
|
||||
|
||||
`JSON.stringify` 会把 Record & Tuple 转化为普通对象:
|
||||
|
||||
```js
|
||||
JSON.stringify(#{ a: #[1, 2, 3] }); // '{"a":[1,2,3]}'
|
||||
JSON.stringify(#[true, #{ a: #[1, 2, 3] }]); // '[true,{"a":[1,2,3]}]'
|
||||
```
|
||||
|
||||
但同时建议实现 `JSON.parseImmutable` 将一个 JSON 直接转化为 Record & Tuple 类型,其 API 与 `JSON.parse` 无异。
|
||||
|
||||
Tuple.prototype 方法与 Array 很像,但也有些不同之处,主要区别是不会修改引用值,而是创建新的引用,具体可看 [appendix](https://github.com/tc39/proposal-record-tuple/blob/main/NS-Proto-Appendix.md#tuple-prototype)。
|
||||
|
||||
由于新增了三种原始类型,所以 typeof 也会新增三种返回结果:
|
||||
|
||||
```js
|
||||
assert(typeof #{ a: 1 } === "record");
|
||||
assert(typeof #[1, 2] === "tuple");
|
||||
assert(typeof Box({}) === "box");
|
||||
```
|
||||
|
||||
Record, Tuple, Box 都支持作为 Map、Set 的 key,并按照其自身规则进行判等,即
|
||||
|
||||
```js
|
||||
const record1 = #{ a: 1, b: 2 };
|
||||
const record2 = #{ a: 1, b: 2 };
|
||||
|
||||
const map = new Map();
|
||||
map.set(record1, true);
|
||||
assert(map.get(record2));
|
||||
```
|
||||
|
||||
```js
|
||||
const record1 = #{ a: 1, b: 2 };
|
||||
const record2 = #{ a: 1, b: 2 };
|
||||
|
||||
const set = new Set();
|
||||
set.add(record1);
|
||||
set.add(record2);
|
||||
assert(set.size === 1);
|
||||
```
|
||||
|
||||
但不支持 WeakMap、WeakSet:
|
||||
|
||||
```js
|
||||
const record = #{ a: 1, b: 2 };
|
||||
const weakMap = new WeakMap();
|
||||
|
||||
// TypeError: Can't use a Record as the key in a WeakMap
|
||||
weakMap.set(record, true);
|
||||
```
|
||||
|
||||
```js
|
||||
const record = #{ a: 1, b: 2 };
|
||||
const weakSet = new WeakSet();
|
||||
|
||||
// TypeError: Can't add a Record to a WeakSet
|
||||
weakSet.add(record);
|
||||
```
|
||||
|
||||
原因是不可变数据没有一个可预测的垃圾回收时机,这样如果用在 Weak 系列反而会导致无法及时释放,所以 API 不匹配。
|
||||
|
||||
最后提案还附赠了理论基础与 FAQ 章节,下面也简单介绍一下。
|
||||
|
||||
### 理论基础
|
||||
|
||||
#### 为什么要创建新的原始类型,而不是像其他库一样在上层处理?
|
||||
|
||||
一句话说就是让 js 原生支持 immutable 就必须作为原始类型。假如不作为原始类型,就不可能让 ==, === 操作符原生支持这个类型的特定判等,也就会导致 immutable 语法与其他 js 代码仿佛处于两套逻辑体系下,妨碍生态的统一。
|
||||
|
||||
#### 开发者会熟悉这套语法吗?
|
||||
|
||||
由于最大程度保证了与普通对象与数组处理、API 的一致性,所以开发者上手应该会比较容易。
|
||||
|
||||
#### 为什么不像 Immutablejs 一样使用 `.get` `.set` 方法操作?
|
||||
|
||||
这会导致生态割裂,代码需要关注对象到底是不是 immutable 的。一个最形象的例子就是,当 Immutablejs 与普通 js 操作库配合时,需要写出类似如下代码:
|
||||
|
||||
```js
|
||||
state.jobResult = Immutable.fromJS(
|
||||
ExternalLib.processJob(
|
||||
state.jobDescription.toJS()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
这有非常强的割裂感。
|
||||
|
||||
#### 为什么不使用全局 Record, Tuple 方法代替 `#` 申明?
|
||||
|
||||
下面给了两个对比:
|
||||
|
||||
```js
|
||||
// with the proposed syntax
|
||||
const record = #{
|
||||
a: #{
|
||||
foo: "string",
|
||||
},
|
||||
b: #{
|
||||
bar: 123,
|
||||
},
|
||||
c: #{
|
||||
baz: #{
|
||||
hello: #[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// with only the Record/Tuple globals
|
||||
const record = Record({
|
||||
a: Record({
|
||||
foo: "string",
|
||||
}),
|
||||
b: Record({
|
||||
bar: 123,
|
||||
}),
|
||||
c: Record({
|
||||
baz: Record({
|
||||
hello: Tuple(
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
很明显后者没有前者简洁,而且也打破了开发者对对象、数组 Like 的认知。
|
||||
|
||||
#### 为什么采用 #[]/#{} 语法?
|
||||
|
||||
采用已有关键字可能导致歧义或者兼容性问题,另外其实还有 `{| |}` `[| |]` 的 [提案](https://github.com/tc39/proposal-record-tuple/issues/10),但目前 `#` 的赢面比较大。
|
||||
|
||||
#### 为什么是深度不可变?
|
||||
|
||||
这个提案喷了一下 `Object.freeze`:
|
||||
|
||||
```js
|
||||
const object = {
|
||||
a: {
|
||||
foo: "bar",
|
||||
},
|
||||
};
|
||||
Object.freeze(object);
|
||||
func(object);
|
||||
```
|
||||
|
||||
由于只保障了一层,所以 `object.a` 依然是可变的,既然要 js 原生支持 immutable,希望的肯定是深度不可变,而不是只有一层。
|
||||
|
||||
另外由于这个语法会在语言层面支持不可变校验,而深度不可变校验是非常重要的。
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 如何基于已有不可变对象创建一个新不可变对象?
|
||||
|
||||
大部分语法都是可以使用的,比如解构:
|
||||
|
||||
```js
|
||||
// Add a Record field
|
||||
let rec = #{ a: 1, x: 5 }
|
||||
#{ ...rec, b: 2 } // #{ a: 1, b: 2, x: 5 }
|
||||
|
||||
// Change a Record field
|
||||
#{ ...rec, x: 6 } // #{ a: 1, x: 6 }
|
||||
|
||||
// Append to a Tuple
|
||||
let tup = #[1, 2, 3];
|
||||
#[...tup, 4] // #[1, 2, 3, 4]
|
||||
|
||||
// Prepend to a Tuple
|
||||
#[0, ...tup] // #[0, 1, 2, 3]
|
||||
|
||||
// Prepend and append to a Tuple
|
||||
#[0, ...tup, 4] // #[0, 1, 2, 3, 4]
|
||||
```
|
||||
|
||||
对于类数组的 Tuple,可以使用 `with` 语法替换新建一个对象:
|
||||
|
||||
```js
|
||||
// Change a Tuple index
|
||||
let tup = #[1, 2, 3];
|
||||
tup.with(1, 500) // #[1, 500, 3]
|
||||
```
|
||||
|
||||
但在深度修改时也遇到了绕不过去的问题,目前有一个 [提案](https://github.com/rickbutton/proposal-deep-path-properties-for-record) 在讨论这件事,这里提到一个有意思的语法:
|
||||
|
||||
```js
|
||||
const state1 = #{
|
||||
counters: #[
|
||||
#{ name: "Counter 1", value: 1 },
|
||||
#{ name: "Counter 2", value: 0 },
|
||||
#{ name: "Counter 3", value: 123 },
|
||||
],
|
||||
metadata: #{
|
||||
lastUpdate: 1584382969000,
|
||||
},
|
||||
};
|
||||
|
||||
const state2 = #{
|
||||
...state1,
|
||||
counters[0].value: 2,
|
||||
counters[1].value: 1,
|
||||
metadata.lastUpdate: 1584383011300,
|
||||
};
|
||||
|
||||
assert(state2.counters[0].value === 2);
|
||||
assert(state2.counters[1].value === 1);
|
||||
assert(state2.metadata.lastUpdate === 1584383011300);
|
||||
|
||||
// As expected, the unmodified values from "spreading" state1 remain in state2.
|
||||
assert(state2.counters[2].value === 123);
|
||||
```
|
||||
|
||||
`counters[0].value: 2` 看上去还是蛮新颖的。
|
||||
|
||||
#### 与 [Readonly Collections](https://github.com/tc39/proposal-readonly-collections) 的关系?
|
||||
|
||||
互补。
|
||||
|
||||
#### 可以基于 Class 创建 Record 实例吗?
|
||||
|
||||
目前不考虑。
|
||||
|
||||
#### TS 也有 Record 与 Tuple 关键字,之间的关系是?
|
||||
|
||||
熟悉 TS 的同学都知道只是名字一样而已。
|
||||
|
||||
#### 性能预期是?
|
||||
|
||||
这个问题挺关键的,如果这个提案性能不好,那也无法用于实际生产。
|
||||
|
||||
当前阶段没有对性能提出要求,但在 Stage4 之前会给出厂商优化的最佳实践。
|
||||
|
||||
## 总结
|
||||
|
||||
如果这个提案与嵌套更新提案一起通过,在 js 使用 immutable 就得到了语言层面的保障,包括 Immutablejs、immerjs 在内的库是真的可以下岗啦。
|
||||
|
||||
> 讨论地址是:[精读《Records & Tuples 提案》· Issue #384 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/384)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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 @@
|
||||
继前一篇 [精读《Records & Tuples 提案》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/223.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20%E6%8F%90%E6%A1%88%E3%80%8B.md),已经有人在思考这个提案可以帮助 React 解决哪些问题了,比如这篇 [Records & Tuples for React](https://sebastienlorber.com/records-and-tuples-for-react),就提到了许多 React 痛点可以被解决。
|
||||
|
||||
其实我比较担忧浏览器是否能将 Records & Tuples 性能优化得足够好,这将是它能否大规模应用,或者说我们是否放心把问题交给它解决的最关键因素。本文基于浏览器可以完美优化其性能的前提,一切看起来都挺美好,我们不妨基于这个假设,看看 Records & Tuples 提案能解决哪些问题吧!
|
||||
|
||||
## 概述
|
||||
|
||||
[Records & Tuples Proposal](https://github.com/tc39/proposal-record-tuple) 提案在上一篇精读已经介绍过了,不熟悉可以先去看一下提案语法。
|
||||
|
||||
### 保证不可变性
|
||||
|
||||
虽然现在 React 也能用 Immutable 思想开发,但大部分情况无法保证安全性,比如:
|
||||
|
||||
```tsx
|
||||
const Hello = ({ profile }) => {
|
||||
// prop mutation: throws TypeError
|
||||
profile.name = 'Sebastien updated';
|
||||
return <p>Hello {profile.name}</p>;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [profile, setProfile] = React.useState(#{
|
||||
name: 'Sebastien',
|
||||
});
|
||||
// state mutation: throws TypeError
|
||||
profile.name = 'Sebastien updated';
|
||||
return <Hello profile={profile} />;
|
||||
}
|
||||
```
|
||||
|
||||
归根结底,我们不会总使用 `freeze` 来冻结对象,大部分情况下需要人为保证引用不被修改,其中的潜在风险依然存在。但使用 Record 表示状态,无论 TS 还是 JS 都会报错,立刻阻止问题扩散。
|
||||
|
||||
### 部分代替 useMemo
|
||||
|
||||
比如下面的例子,为了保障 `apiFilters` 引用不变,需要对其 `useMemo`:
|
||||
|
||||
```tsx
|
||||
const apiFilters = useMemo(
|
||||
() => ({ userFilter, companyFilter }),
|
||||
[userFilter, companyFilter],
|
||||
);
|
||||
const { apiData, loading } = useApiData(apiFilters);
|
||||
```
|
||||
|
||||
但 Record 模式不需要 memo,因为 js 引擎会帮你做类似的事情:
|
||||
|
||||
```tsx
|
||||
const {apiData,loading} = useApiData(#{ userFilter, companyFilter })
|
||||
```
|
||||
|
||||
### 用在 useEffect
|
||||
|
||||
这段写的很啰嗦,其实和代替 useMemo 差不多,即:
|
||||
|
||||
```tsx
|
||||
const apiFilters = #{ userFilter, companyFilter };
|
||||
|
||||
useEffect(() => {
|
||||
fetchApiData(apiFilters).then(setApiDataInState);
|
||||
}, [apiFilters]);
|
||||
```
|
||||
|
||||
你可以把 `apiFilters` 当做一个引用稳定的原始对象看待,如果它确实变化了,那一定是值改变了,所以才会引发取数。如果把上面的 `#` 号去掉,每次组件刷新都会取数,而实际上都是多余的。
|
||||
|
||||
### 用在 props 属性
|
||||
|
||||
可以更方便定义不可变 props 了,而不需要提前 useMemo:
|
||||
|
||||
```tsx
|
||||
<ExpensiveChild someData={#{ attr1: 'abc', attr2: 'def' }} />;
|
||||
```
|
||||
|
||||
### 将取数结果转化为 Record
|
||||
|
||||
这个目前还真做不到,除非用性能非常差的 `JSON.stringify` 或 `deepEqual`,用法如下:
|
||||
|
||||
```tsx
|
||||
const fetchUserAndCompany = async () => {
|
||||
const response = await fetch(
|
||||
`https://myBackend.com/userAndCompany`,
|
||||
);
|
||||
return JSON.parseImmutable(await response.text());
|
||||
};
|
||||
```
|
||||
|
||||
即利用 Record 提案的 `JSON.parseImmutable` 将后端返回值也转化为 Record,这样即便重新查询,但如果返回结果完全不变,也不会导致重渲染,或者局部变化也只会导致局部重渲染,而目前我们只能放任这种情况下全量重渲染。
|
||||
|
||||
然而这对浏览器实现 Record 的新能优化提出了非常严苛的要求,因为假设后端返回的数据有几十 MB,我们不知道这种内置 API 会导致多少的额外开销。
|
||||
|
||||
假设浏览器使用非常 Magic 的办法做到了几乎零开销,那么我们应该在任何时候都用 `JSON.parseImmutable` 解析而不是 `JSON.parse`。
|
||||
|
||||
### 生成查询参数
|
||||
|
||||
也是利用了 `parseImmutable` 方法,让前端可以精确发送请求,而不是每次 `qs.parse` 生成一个新引用就发一次请求:
|
||||
|
||||
```tsx
|
||||
// This is a non-performant, but working solution.
|
||||
// Lib authors should provide a method such as qs.parseRecord(search)
|
||||
const parseQueryStringAsRecord = (search) => {
|
||||
const queryStringObject = qs.parse(search);
|
||||
// Note: the Record(obj) conversion function is not recursive
|
||||
// There's a recursive conversion method here:
|
||||
// https://tc39.es/proposal-record-tuple/cookbook/index.html
|
||||
return JSON.parseImmutable(
|
||||
JSON.stringify(queryStringObject),
|
||||
);
|
||||
};
|
||||
|
||||
const useQueryStringRecord = () => {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => parseQueryStringAsRecord(search), [
|
||||
search,
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
还提到一个有趣的点,即到时候配套工具库可能提供类似 `qs.parseRecord(search)` 的方法把 `JSON.parseImmutable` 包装掉,也就是这些生态库想要 “无缝” 接入 Record 提案其实需要做一些 API 改造。
|
||||
|
||||
### 避免循环产生的新引用
|
||||
|
||||
即便原始对象引用不变,但我们写几行代码随便 `.filter` 一下引用就变了,而且无论返回结果是否变化,引用都一定会改变:
|
||||
|
||||
```tsx
|
||||
const AllUsers = [
|
||||
{ id: 1, name: 'Sebastien' },
|
||||
{ id: 2, name: 'John' },
|
||||
];
|
||||
|
||||
const Parent = () => {
|
||||
const userIdsToHide = useUserIdsToHide();
|
||||
const users = AllUsers.filter(
|
||||
(user) => !userIdsToHide.includes(user.id),
|
||||
);
|
||||
return <UserList users={users} />;
|
||||
};
|
||||
|
||||
const UserList = React.memo(({ users }) => (
|
||||
<ul>
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
));
|
||||
```
|
||||
|
||||
要避免这个问题就必须 `useMemo`,但在 Record 提案下不需要:
|
||||
|
||||
```tsx
|
||||
const AllUsers = #[
|
||||
#{ id: 1, name: 'Sebastien' },
|
||||
#{ id: 2, name: 'John' },
|
||||
];
|
||||
|
||||
const filteredUsers = AllUsers.filter(() => true);
|
||||
AllUsers === filteredUsers;
|
||||
// true
|
||||
```
|
||||
|
||||
### 作为 React key
|
||||
|
||||
这个想法更有趣,如果 Record 提案保证了引用严格不可变,那我们完全可以拿 `item` 本身作为 `key`,而不需要任何其他手段,这样维护成本会大大降低。
|
||||
|
||||
```tsx
|
||||
const list = #[
|
||||
#{ country: 'FR', localPhoneNumber: '111111' },
|
||||
#{ country: 'FR', localPhoneNumber: '222222' },
|
||||
#{ country: 'US', localPhoneNumber: '111111' },
|
||||
];
|
||||
<>
|
||||
{list.map((item) => (
|
||||
<Item key={item} item={item} />
|
||||
))}
|
||||
</>
|
||||
```
|
||||
|
||||
当然这依然建立在浏览器非常高效实现 Record 的前提,假设浏览器采用 `deepEqual` 作为初稿实现这个规范,那么上面这坨代码可能导致本来不卡的页面直接崩溃退出。
|
||||
|
||||
### TS 支持
|
||||
|
||||
也许到时候 ts 会支持如下方式定义不可变变量:
|
||||
|
||||
```tsx
|
||||
const UsersPageContent = ({
|
||||
usersFilters,
|
||||
}: {
|
||||
usersFilters: #{nameFilter: string, ageFilter: string}
|
||||
}) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
// poor-man's fetch
|
||||
useEffect(() => {
|
||||
fetchUsers(usersFilters).then(setUsers);
|
||||
}, [usersFilters]);
|
||||
return <Users users={users} />;
|
||||
};
|
||||
```
|
||||
|
||||
那我们就可以真的保证 `usersFilters` 是不可变的了。因为在目前阶段,编译时 ts 是完全无法保障变量引用是否会变化。
|
||||
|
||||
### 优化 css-in-js
|
||||
|
||||
采用 Record 与普通 object 作为 css 属性,对 css-in-js 的区别是什么?
|
||||
|
||||
```tsx
|
||||
const Component = () => (
|
||||
<div
|
||||
css={#{
|
||||
backgroundColor: 'hotpink',
|
||||
}}
|
||||
>
|
||||
This has a hotpink background.
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
由于 css-in-js 框架对新的引用会生成新 className,所以如果不主动保障引用不可变,会导致渲染时 className 一直变化,不仅影响调试也影响性能,而 Record 可以避免这个担忧。
|
||||
|
||||
## 精读
|
||||
|
||||
总结下来,其实 Record 提案并不是解决之前无法解决的问题,而是用更简洁的原生语法解决了复杂逻辑才能解决的问题。这带来的优势主要在于 “不容易写出问题代码了”,或者让 Immutable 在 js 语言的上手成本更低了。
|
||||
|
||||
现在看下来这个规范有个严重担忧点就是性能,而 stage2 并没有对浏览器实现性能提出要求,而是给了一些建议,并在 stage4 之前给出具体性能优化建议方案。
|
||||
|
||||
其中还是提到了一些具体做法,包括快速判断真假,即对数据结构操作时的优化。
|
||||
|
||||
快速判真可以采用类似 hash-cons 快速判断结构相等,可能是将一些关键判断信息存在 hash 表中,进而不需要真的对结构进行递归判断。
|
||||
|
||||
快速判假可以通过维护散列表快速判断,或者我觉得也可以用上数据结构一些经典算法,比如布隆过滤器,就是用在高效快速判否场景的。
|
||||
|
||||
### Record 降低了哪些心智负担
|
||||
|
||||
其实如果应用开发都是 hello world 复杂度,那其实 React 也可以很好的契合 immutable,比如我们给 React 组件传递的 props 都是 boolean、string 或 number:
|
||||
|
||||
```tsx
|
||||
<ExpensiveChild userName="nick" age={18} isAdmin />;
|
||||
```
|
||||
|
||||
比如上面的例子,完全不用关心引用会变化,因为我们用的原始类型本身引用就不可能变化,比如 `18` 不可能突变成 `19`,如果子组件真的想要 `19`,那一定只能创建一个新的,总之就是没办法改变我们传递的原始类型。
|
||||
|
||||
如果我们永远在这种环境下开发,那 React 结合 immutable 会非常美妙。但好景不长,我们总是要面对对象、数组的场景,然而这些类型在 js 语法里不属于原始类型,我们了解到还有 “引用” 这样一种说法,两个值不一样对象可能是 `===` 全等的。
|
||||
|
||||
可以认为,Record 就是把这个顾虑从语法层面消除了,即 `#{ a: 1 }` 也可以看作像 `18`,`19` 一样的数字,不可能有人改变它,所以从语法层面你就会像对 `19` 这个数字一样放心 `#{ a: 1 }` 不会被改变。
|
||||
|
||||
当然这个提案面临的最大问题就是 “如何将拥有子结构的类型看作原始类型”,也许 JS 引擎将它看作一种特别的字符串更贴合其原理,但难点是这又违背了整个语言体系对子结构的默认认知,Box 装箱语法尤其别扭。
|
||||
|
||||
## 总结
|
||||
|
||||
看了这篇文章的畅想,React 与 Records & Tulpes 结合的一定会很好,但前提是浏览器对其性能优化必须与 “引用对比” 大致相同才可以,这也是较为少见,对性能要求如此苛刻的特性,因为如果没有性能的加持,其便捷性将毫无意义。
|
||||
|
||||
> 讨论地址是:[精读《Records & Tuples for React》· Issue #385 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/385)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,103 @@
|
||||
Excel 现在可利用 js 根据单元格数据生成图表、表格,或通过 js 拓展自定义函数拓展内置 Excel 表达式。
|
||||
|
||||
我们来学习一下 Excel js API 开放是如何设计的,从中学习到一些开放 API 设计经验。
|
||||
|
||||
API 文档:[Excel JavaScript API overview](https://docs.microsoft.com/en-us/office/dev/add-ins/reference/overview/excel-add-ins-reference-overview)
|
||||
|
||||
## 精读
|
||||
|
||||
Excel 将利用 JS API 开放了大量能力,包括用户能通过界面轻松做到的,也包括无法通过界面操作做到的。
|
||||
|
||||
### 为什么需要开放 JS API
|
||||
|
||||
Excel 已经具备了良好的易用性,以及 [formula](https://support.microsoft.com/en-us/office/overview-of-formulas-in-excel-ecfdc708-9162-49e8-b993-c311f47ca173) 这个强大的公式。在之前 [精读《Microsoft Power Fx》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/211.%E7%B2%BE%E8%AF%BB%E3%80%8AMicrosoft%20Power%20Fx%E3%80%8B.md) 提到过,formula 就是 Excel 里的 Power FX,属于画布低代码语言,不过在 Excel 里叫做 “公式” 更合适。
|
||||
|
||||
已经具备这么多能力,为何还需要 JS API 呢?一句话概括就是,在 JS API 内可以使用 formula,即 JS API 是公式能力的超集,它包含了对 Excel 工作簿的增删改查、数据的限制、RangeAreas 操作、图表、透视表,甚至可以自定义 formula 函数。
|
||||
|
||||
也就是说,JS API 让 Excel “可编程化”,即以开发者视角对 Excel 进行二次拓展,包括对公式进行二次拓展,使 Excel 覆盖更多场景。
|
||||
|
||||
### JS API 可以用在哪些地方
|
||||
|
||||
从 Excel 流程中最开始的工作薄、工作表环节,到最细节的单元格数据校验都可通过 JS API 支持,目前看来 Excel JS API 并没有设置能力边界,而且还会不断完善,将 Excel 全生命周期中一切可编程的地方开放出来。
|
||||
|
||||
首先是对工作薄、工作表的操作,以及对工作表用户操作的监听,或者对工作表进行只读设置。这一类 API 的目的是对 Excel 这个整体进行编程操作。
|
||||
|
||||
第二步就是对单元格级别进行操作,比如对单元格进行区域选中,获取选中区域,或者设置单元格属性、颜色,或者对单元格数据进行校验。自定义公式也在这个环节,因为单元格的值可以是公式,而公式可以利用 JS API 拓展。
|
||||
|
||||
最后一步是拓展行为,即在单元格基础上引入图表、透视表拓展。虽然这些功能在 UI 按钮上也可以操作出来,但 JS API 可以实现 UI 界面配置不出来的逻辑,对于非常复杂的逻辑行为,即便 UI 可以配置出来,可读性也远没有代码高。除了表格透视表外、还可以创建一些自定义形状,基本的几何图形、图片和 SVG 都支持。
|
||||
|
||||
### JS API 设计
|
||||
|
||||
比较有趣的是,Excel 并没有抽象 “单元格” 对象,即便我们所有人都认为单元格就是 Excel 的代表。
|
||||
|
||||
这么做是出于 API 设计的合理性,因为 Excel 使用 Range 概念表示连续单元格。比如:
|
||||
|
||||
```js
|
||||
Excel.run(function (context) {
|
||||
var sheet = context.workbook.worksheets.getActiveWorksheet();
|
||||
|
||||
var headers = [
|
||||
["Product", "Quantity", "Unit Price", "Totals"]
|
||||
];
|
||||
var headerRange = sheet.getRange("B2:E2");
|
||||
headerRange.values = headers;
|
||||
headerRange.format.fill.color = "#4472C4";
|
||||
headerRange.format.font.color = "white";
|
||||
|
||||
return context.sync();
|
||||
});
|
||||
```
|
||||
|
||||
可以发现,Range 让 Excel 聚焦在批量单元格 API,即把单元格看做一个范围,整体 API 都可以围绕一个范围去设计。这种设计理念的好处是,把范围局限在单格单元格,就可以覆盖 Cell 概念,而聚焦在多个单元格时,可以很方便的基于二维数据结构创建表格、折线图等分析图形,因为二维结构的数据才是结构化数据。
|
||||
|
||||
或者可以说,结构化数据是 Excel 最核心的概念,而单元格无法体现结构化。结构化数据的好处是,一张工作表就是一个可以用来分析的数据集,在其之上无论是基于单元格的条件格式,还是创建分析图表,都是一种数据二次分析行为,这都得益于结构化数据,所以 Excel JS API 必然围绕结构化数据进行抽象。
|
||||
|
||||
再从 API 语法来看,除了工作薄这个级别的 API 采用了 `Excel.createWorkbook();` 之外,其他大部分 API 都是以下形式:
|
||||
|
||||
```js
|
||||
Excel.run(function (context) {
|
||||
// var sheet = context.workbook.worksheets.getItem("Sample");
|
||||
// 对 sheet 操作 ..
|
||||
return context.sync();
|
||||
});
|
||||
```
|
||||
|
||||
最外层的函数 `Excel.run` 是注入 `context` 用的,而且也可以保证执行的时候 Excel context 已经准备好了。而 `context.sync()` 是同步操作,即使当前对 context 的操作生效。所以 Excel JS API 是命令式的,也不会做类似 MVVM 的双向绑定,所以在操作过程中数据和 Excel 状态不会发生变化,直到执行 `context.sync()`。
|
||||
|
||||
注意到这点后,就可以理解为什么要把某些代码写在 `context.sync().then` 里了,比如:
|
||||
|
||||
```js
|
||||
Excel.run(function (ctx) {
|
||||
var pivotTable = context.workbook.worksheets.getActiveWorksheet().pivotTables.getItem("Farm Sales");
|
||||
|
||||
// Get the totals for each data hierarchy from the layout.
|
||||
var range = pivotTable.layout.getDataBodyRange();
|
||||
var grandTotalRange = range.getLastRow();
|
||||
grandTotalRange.load("address");
|
||||
return context.sync().then(function () {
|
||||
// Sum the totals from the PivotTable data hierarchies and place them in a new range, outside of the PivotTable.
|
||||
var masterTotalRange = context.workbook.worksheets.getActiveWorksheet().getRange("E30");
|
||||
masterTotalRange.formulas = [["=SUM(" + grandTotalRange.address + ")"]];
|
||||
});
|
||||
}).catch(errorHandlerFunction);
|
||||
```
|
||||
|
||||
这个从透视表获取数据的例子,只有执行 `context.sync()` 后才能拿到 `grandTotalRange.address`。
|
||||
|
||||
## 总结
|
||||
|
||||
微软还在 Office 套件 Excel、Outlook、Word 中推出了 [ScriptLab](https://docs.microsoft.com/zh-cn/office/dev/add-ins/overview/explore-with-script-lab) 功能,就可以在 Excel 的 ScriptLab 里编写 Excel JS API。
|
||||
|
||||
在 Excel JS API 之上,还有一个 [通用 API](https://docs.microsoft.com/zh-cn/javascript/api/office?view=common-js-preview),定义为跨应用的通用 API,这样 Excel JS API 就可以把精力聚焦在 Excel 产品本身能力上。
|
||||
|
||||
> 讨论地址是:[精读《Excel JS API》· Issue #387 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/387)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,166 @@
|
||||
[2021 JavaScript Rising Stars](https://risingstars.js.org/2021/en) 每年都会对前端开源项目进行点评,其依据是去年 Star 的增幅。Star 虽然只是一个维度,但至少反应了流行度,根据这个排行榜可以大体分析出前端社区的趋势。
|
||||
|
||||
## 精读
|
||||
|
||||
该榜单包含整体榜单、前端框架、Node 框架、构建工具、Vue 生态、React 生态、CSS-In-JS、测试、移动端、桌面、静态建站、状态管理、GraphQL 共 13 个子榜单,都是前端开源最活跃的几个领域,下面分别介绍。
|
||||
|
||||
### 整体榜单
|
||||
|
||||
第一名 [zx](https://github.com/google/zx) 是一个命令行工具,它基于 Node 语法拓展了 Bash 支持,可以非常方便的进行 Node 与 Bash 之间的输入输出,就像 Node 原生就支持 Bash 一样。它解决了离不开 Bash,但 Bash 写起大段逻辑不如 Node 自然的痛点。
|
||||
|
||||
第二名 [vite](https://github.com/vitejs/vite) 是去年最闪耀的星,它是一个 bundless 概念的前端构建工具,最初服务于 vue,后来进行框架无关升级后,在 react、angular 生态都大受欢迎。它解决了 webpack 编译太慢,其他 bundless 方案不够开箱即用且存在大量兼容问题的痛点。
|
||||
|
||||
第三名 [next.js](https://github.com/vercel/next.js) 2016 年开始的项目,是一个大而全的 React 全家桶,定位就是各大厂都会自己做一套的前端一体化框架,但它更时髦,不断加入许多流行功能比如 Server Component。这和 next.js 所在的明星公司 Vercel 有关,这家公司挖了大量开源知名人物,包括 Svelte 作者与 React 团队核心成员,所以也许未来社区的新玩具会先用在 next.js 再独立开源。它给出了前端最佳实践,并解决了没有精力持续给项目进行全方位优化,或追逐不上潮流的问题,因为 next.js 本身正在成为前端潮流的发源地。
|
||||
|
||||
第四名 [react](https://github.com/facebook/react) 不用多说了,数据驱动、响应式编程、函数式的领军框架,它改变了前端开发效率。
|
||||
|
||||
第五名 [tauri](https://github.com/tauri-apps/tauri) 比 electron 更轻量的桌面应用开发框架,基于任何前端框架。它解决了前端开发者遇到桌面应用开发场景时各平台巨大的原生开发学习成本的痛点。
|
||||
|
||||
第六名 [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) 是 css 框架,它提供了大量语义化 className,提供了许多最佳实践,让你有机会把 css 打理的井然有序。它解决了前端项目 css 杂乱无章又没有人真的在意的痛点。
|
||||
|
||||
第七名 [vscode](https://github.com/microsoft/vscode) 宇宙级 IDE,它解决了程序员没有真正趁手软件写代码的痛点。
|
||||
|
||||
第八名 [Slidev](https://github.com/slidevjs/slidev) 是一个把 markdown 渲染成 PPT 的框架,基于 vite + vue 等技术栈开发。用它开发的 PPT 非常简洁美观,非常适合在公开场合分享时使用,不仅看起来赏心悦目,还可以不经意间切换到 Markdown 源码 hotfix 一下小错误,展示出你的极客精神。它解决了你真的只想展示几句话,但又要以 PPT 方式 show 出来的痛点。
|
||||
|
||||
第九名 [NocoDB](https://github.com/nocodb/nocodb) 是一个支持多种数据源的数据库 UI 管理工具。但其实它有更大的格局,即对标 [airtable](https://www.airtable.com/),即用 NocoDB 连接数据库后,一切数据可视化的操作与功能都成为了可能,且提供了大量工作常用的甘特图、电子表格等视图,并可互相转换,最终其实数据存储到连接的数据库,但你无需关心细节。它解决了基于二维表格数据开发各类生产工具需投入大量研发资源的痛点。
|
||||
|
||||
第十名 [Vue](https://github.com/vuejs/vue) 和 React 一样不多说了。
|
||||
|
||||
### 前端框架
|
||||
|
||||
第一名 [react](https://github.com/facebook/react) 在整体榜单里了。
|
||||
|
||||
第二名 [Vue](https://github.com/vuejs/vue) 也在整体榜单里了。
|
||||
|
||||
第三名 [svelte](https://github.com/sveltejs/svelte) 是一个类似 vue 的框架,但特色是极度重视编译时,而忽略运行时,即运行时除了必要逻辑外是完全不引入任何 runtime 框架的。说实话我觉得和 vue、react 相比在正儿八经项目中并没有核心优势,因为它并没有那种魔法能力,可以极大的减少大型项目体积与提升性能,反而会受制于其语法与编译时的特性产生副作用。但唯一一个好处是框架无关,即利用 svelte 编译的组件几乎没有额外运行时框架代码,可以最低成本,最大隔离性的与其他项目结合。
|
||||
|
||||
第四名 [angular](https://github.com/angular/angular) 笔者已经很久没有关注 angular 框架了,无法给出什么点评。但从 svelte 新增热度超过 angular 来看,可能大部分开发者对 angular 的态度和我一样。
|
||||
|
||||
第五名 [solid](https://github.com/solidjs/solid) 类似 svelte,提前编译,按需打包,重要的是,其类似 React `useEffect` 的 API `createEffect` 在依赖变化后,仅该函数会重新执行,而不会导致整个组件重新执行,在点对点更新上做得更极致。
|
||||
|
||||
前端框架的亮点是 svelte 与 solid 的概念,即重编译时,轻运行时,更加原子化的更新粒度,与更直接的调用原生浏览器方法带来性能提升。很难不让人觉得这是一个前端框架新趋势,但我翻了不少资料发现,这种创新带来的收益在正常项目里微乎其微,所以实际上 2021 年前端框架还是没能跳出三巨头创造新的概念,而以 svelte 与 solid 为代表的 “静态化” 框架只能算微创新。
|
||||
|
||||
### Node 框架
|
||||
|
||||
第一名 [next.js](https://github.com/vercel/next.js) 在整体榜单里了,在 Node 框架一骑绝尘。
|
||||
|
||||
第二名 [nest](https://github.com/nestjs/nest) 和 next.js 很像,据我当时的了解,是因为 next.js 起步较慢,源码还不支持 ts,所以就有了这个更时髦的新框架。但实际上 next.js 早就全部改为 ts 了,而且正如整体榜单所说,现在已经开始引领潮流了,所以不怪 nest 定位重合,只能怪 next.js 后续发力太猛了。nest 的唯一特点就是没有绑定 UI 库。
|
||||
|
||||
第三名 [Strapi](https://github.com/strapi/strapi) 专门为 API 场景服务,提供了一个 API 管理后台,解决了只需要一个便捷 API 管理,而不希望了解一个大而全的后端框架的痛点。
|
||||
|
||||
第四名 [remix](https://github.com/remix-run/remix) 其实和 next.js 定位差不多,由 react-router 作者开发,才开源不久,需要进一步观察。
|
||||
|
||||
第五名 [nuxt.js](https://github.com/nuxt/nuxt.js) 是 vue 领域的 next.js。
|
||||
|
||||
值得一提的是,svelte 也有自己的专属框架 [sveltekit](https://kit.svelte.dev/),所以 Node 后端框架之争大部分其实在打全栈的牌,毕竟 Node 的优势就是支持 js 语言,而当前端应用基于某个框架编写时,如果有一个 Node 框架可以无缝集成这个前端框架,它就比非 Node 框架更优。
|
||||
|
||||
不过大厂几乎都是前后端分离的,所以这种全栈优势框架在国内没有太多出场机会,如果你是一个个人博主,还是首推使用全栈框架建站。
|
||||
|
||||
### 构建工具
|
||||
|
||||
第一名 [vite](https://github.com/vitejs/vite) 在整体榜单里了,在构建工具里也是一骑绝尘。
|
||||
|
||||
第二名 [esbuild](https://github.com/evanw/esbuild) 是用 go 编写的构建工具,适用使用范围更广,其压缩模块在 bundless 还未成熟时就被各大构建全家桶提前集成了,而 vite 也是基于 esbuild 进行编译的,但 vite 的火热度更高,说明了整体 bundless 方案已在 2021 年成熟了。
|
||||
|
||||
第三名 [swc](https://github.com/swc-project/swc) 因采用 rust 编写而知名,类似 esbuild,但因为依托 rust 编译到 wasm 的特性,支持了在线编译器,非常方便。swc 还被大量新生代构建工具作为基建,这在 [精读《Rust 是 JS 基建的未来》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/218.%E7%B2%BE%E8%AF%BB%E3%80%8ARust%20%E6%98%AF%20JS%20%E5%9F%BA%E5%BB%BA%E7%9A%84%E6%9C%AA%E6%9D%A5%E3%80%8B.md) 时提到过。
|
||||
|
||||
第四名 [turborepo](https://github.com/vercel/turborepo) 是用 go 写的 monorepo 项目管理工具,是 lerna 的替代品。
|
||||
|
||||
第五名 [nx](https://github.com/nrwl/nx) 也是一个 monorepo 管理工具。
|
||||
|
||||
与框架不同,构建工具往往呈现套娃结构,不是你中有我,就是我中有你,每个热门库都重点解决某一块关键问题,不断套娃套娃,最后套成一个很棒的全家桶。
|
||||
|
||||
### Vue 生态
|
||||
|
||||
第一名 [Slidev](https://github.com/slidevjs/slidev) 在整体榜单里了。
|
||||
|
||||
第二名 [Vue Element Admin](https://github.com/PanJiaChen/vue-element-admin) 基于 vue 的管理后台,在权限验证有一些最佳实践,使用 vuex 管理状态。
|
||||
|
||||
第三名 [Headless UI](https://github.com/tailwindlabs/headlessui) 是一个完全无样式的基础组件库,支持 React 与 Vue,官网的例子都是利用 [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) 内置样式组合而成的。它解决了 UI 组件库绑定样式后,自定义样式 “实际上非常恶心” 的痛点。
|
||||
|
||||
第四名 [Naive UI](https://github.com/TuSimple/naive-ui) 是一个 Vue 组件库,没有太多特别之处,但竟然上了排行榜。看了一下 star 趋势,在 2021.6 月份 star 涨幅是之后的十倍,估计刚开源推广了一波,后续涨幅很慢了,不出意外明年会跌出这个榜单。
|
||||
|
||||
第五名 [vue-next](https://github.com/vuejs/vue-next) 即 vue3,star 数量只有 vue2 的 13%,但今年 star 增幅有 vue2 的一半。
|
||||
|
||||
vue3 还自带了状态管理库 [pinia](https://github.com/vuejs/pinia),其生态已经非常完备。
|
||||
|
||||
### React 生态
|
||||
|
||||
第一名 [next.js](https://github.com/vercel/next.js) 在整体榜单里了。
|
||||
|
||||
第二名 [Ant Design](https://github.com/ant-design/ant-design) 虽然立志成为西湖区最好的 React 组件库,但事实上已经成为了全球最好的 React 组件库。
|
||||
|
||||
第三名 [MUI](https://github.com/mui-org/material-ui) 就是大名鼎鼎的 material design UI 组件库,我对它影响最深的是按钮点击后出现的水波纹,这是 material design 的一大特色。早在 2014 年就创建了,在 Ant Design 没火的时候,是开源组件库首选。
|
||||
|
||||
第四名 [remix](https://github.com/remix-run/remix) 在 Node 框架榜单里了,和 next.js 一样,是绑定了 React 生态的 Node 框架,所以也出现在 React 生态中。
|
||||
|
||||
第五名 [react-use](https://github.com/streamich/react-use) 是很小巧的 React Hook 库,提供了如 `usePrevious`、`useDebounce` 等常用的 Hook。
|
||||
|
||||
看完整个 React 生态榜单,无论是优质生态库数量,还是去年增长的 Star 数,都比 Vue 生态更胜一筹。这背后是无副作用的纯函数与自动依赖收集的响应式视图之争,甚至在 React 生态里也有比如 mobx-react 等优质 MVVM 库,这两种编程范式都会长期并存。
|
||||
|
||||
### CSS-In-JS
|
||||
|
||||
第一名 [vanilla-extract](https://github.com/seek-oss/vanilla-extract) 作为 2021 年的黑马,主打零运行时与 TS 支持。零运行时是通过 @vanilla-extract/webpack-plugin 插件在编译时就完成内容输出。
|
||||
|
||||
第二名 [styled-components](https://github.com/styled-components/styled-components) 是推出最早,也最成熟的一个 CSS-In-JS 框架,虽然版本间出现过运行时不兼容让我放弃过,但不得不说是这个方向的鼻祖。
|
||||
|
||||
第三名 [stitches](https://github.com/modulz/stitches) 和第一名很像,也主打零运行时,不过没有提对 TS 是否友好。
|
||||
|
||||
第四名 [Twin](https://github.com/ben-rogerson/twin.macro) 基于 [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) 实现了 CSS-In-JS 版的语法,可以认为是内置了一套最佳实践的 CSS-In-JS 库,也没解决太大的痛点,只是如果你同时喜欢 Tailwind CSS 与 CSS-In-JS,可能会爱屋及乌的选择 Twin。
|
||||
|
||||
第五名 [Emotion](https://github.com/emotion-js/emotion) 也是一个相对完备的库,基本上 CSS-In-JS 各类语法都能支持。
|
||||
|
||||
相比传统 CSS-In-JS 库,第一名 vanilla-extract 的零运行时是一大亮点,是这个方向的新趋势。
|
||||
|
||||
### 测试
|
||||
|
||||
第一名 [Playwright](https://github.com/microsoft/playwright) 是一个跨浏览器跨平台的测试框架,可以利用 js 代码打开任意 url 地址截图或者对比,解决了搭建自动化测试平台需要从零开始编写底层框架的痛点。
|
||||
|
||||
第二名 [Storybook](https://github.com/storybookjs/storybook) 是非常有名的文档工具,很多开源组件、项目的文档都基于 Storybook 创建。神奇的是它还支持[单元测试](https://storybook.js.org/docs/react/writing-tests/introduction),在你访问 UI 组件时进行测试并打印出测试结果。Storybook 已经变成了一个 all-in-one 的组件开发工具。
|
||||
|
||||
第三名 [Cypress](https://github.com/cypress-io/cypress) 与 Playwright 且诞生比较早,但由于不支持多 tab 页面,且仅支持 js,所以仅在前端流行,在测试工程师角度却不如支持多语言的 Playwright 好用。
|
||||
|
||||
第四名 [Puppeteer](https://github.com/puppeteer/puppeteer) 是 2017 年谷歌推出基于 Chrome 无头浏览器的测试工具,但 2020 年微软的 Playwright 具有跨浏览器特性还是更胜一筹。
|
||||
|
||||
第五名 [Jest](https://github.com/facebook/jest) 是代码级别单测工具的佼佼者,覆盖了全框架,只要你想对代码进行单元测试,选 Jest 是不会错的。
|
||||
|
||||
测试框架围绕单测与浏览器测试这两个子领域,2021 年在浏览器测试领域出现了跨浏览器这个特色方向,在单测领域没有太大变化,顶多出了一个 [Vitest](https://github.com/vitest-dev/vitest) 让单测跑得更快,这个库在 2022 年稳定后可能会大放异彩,甚至可能因为 Vite 流行的原因取代 Jest。
|
||||
|
||||
### 移动端
|
||||
|
||||
第一名 [ReactNative](https://github.com/facebook/react-native) 是基于 React 的 Mobile Native 开发框架,笔者用过一段时间,只能说不能抱有太大期待,因为极大的局限了 web 语法,如果你觉得仅掌握前端知识就可以轻松使用,那么一定会让你失望,不要一开始就抱着这种期待。另外跨端真是非常痛,比如 `SwitchAndroid`、`SwitchIOS` 让你感受不到 Write Once, Run everywhere(虽然官方也没这么说)。
|
||||
|
||||
第二名 [Ionic](https://github.com/ionic-team/ionic-framework) 是一个跨前端框架的跨平台构建工具,解决了 ReactNative 无法 Run everywhere 的痛点,但也带来了不够灵活的问题,即无法使用平台特定特性。
|
||||
|
||||
第三名 [Expo](https://github.com/expo/expo) 是基于 ReactNative 的一站式跨端开发工具,它的 App 使用非常傻瓜化,并且内置了调试能力,可以说是把 ReactNative 要踩的坑帮你踩完了。
|
||||
|
||||
第四名 [Quasar](https://github.com/quasarframework/quasar) 可以认为是 Vue 版的 ReactNative。
|
||||
|
||||
第五名 [Flipper](https://github.com/facebook/flipper) 是一个 Native 应用调试工具,可以认为是手机应用版本的 Chrome DevTools,支持连接远程终端,解决了手机应用难以用电脑调试的痛点。
|
||||
|
||||
其实还少了 [Flutter](https://github.com/flutter/flutter) 这个优秀框架,虽然不属于前端方向,但就像前端脚手架越来越多用 Rust、Go 写一样,Native 用 Dart 也是可以接受的。
|
||||
|
||||
从前端角度看移动端,唯一需求就是 Write Once,Run Anywhere,然后再把调试体验做好一些,Native 的兼容性、拓展性做强一些,就是一个完美方案了。
|
||||
|
||||
说到跨端,基于 Flutter 的 [kraken](https://github.com/openkraken/kraken) 也绝对值得一提,它利用 Flutter 高一执行渲染层能力,并解决了 Dart 生态对前端不友好的问题,做了一个 html+css+js 到 dart 的桥接层,如果明年可以在手淘稳定覆盖大量场景,那一定是个值得考虑的方案。
|
||||
|
||||
## 总结
|
||||
|
||||
还有更多榜单就不一一总结了,如果觉得不过瘾,可以去 [2021 JavaScript Rising Stars](https://risingstars.js.org/2021/en) 翻翻这些 top star 项目的介绍和源码深入了解一下。
|
||||
|
||||
最后总结一下 2021 前端领域的几个关键特征:
|
||||
|
||||
- 编程语言全面开花。以后 JS 开发者不等于前端开发者了,因为 Go、Rust、Dart、C++ 语言都可以为前端服务,并且 2021 年是真的有不少场景做到了生产环境可用,不论我们接不接受,前端不止有 JS 一种语言了。
|
||||
- 前端开发全家桶逐渐产生技术壁垒。在前几年,抄一个前端全家桶很容易,在过程中还可以学到很多底层知识,但现在前端全家桶的积累越来越多,涉及的领域越来越广,甚至 next.js 引入的特性会超越你自己调制的全家桶,这说明全家桶的知识量已经逐渐达到个人知识广度的极限,如果你没有足够精力持续学习,跟进时代步伐的最好方式是使用一个成熟的全家桶。
|
||||
|
||||
> 讨论地址是:[精读《2021 前端新秀回顾》· Issue #390 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/390)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,306 @@
|
||||
[Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案给 js 增加了 Pipe 语法,这次结合 [A pipe operator for JavaScript: introduction and use cases](https://2ality.com/2022/01/pipe-operator.html) 文章一起深入了解这个提案。
|
||||
|
||||
## 概述
|
||||
|
||||
Pipe 语法可以将函数调用按顺序打平。如下方函数,存在三层嵌套,但我们解读时需要由内而外阅读,因为调用顺序是由内而外的:
|
||||
|
||||
```js
|
||||
const y = h(g(f(x)))
|
||||
```
|
||||
|
||||
Pipe 可以将其转化为正常顺序:
|
||||
|
||||
```js
|
||||
const y = x |> f(%) |> g(%) |> h(%)
|
||||
```
|
||||
|
||||
Pipe 语法有两种风格,分别来自 Microsoft 的 [F#](https://en.wikipedia.org/wiki/F_Sharp_(programming_language)) 与 Facebook 的 [Hack](https://en.wikipedia.org/wiki/Hack_(programming_language))。
|
||||
|
||||
之所以介绍这两个,是因为 js 提案首先要决定 “借鉴” 哪种风格。js 提案最终采用了 Hack 风格,因此我们最好把 F# 与 Hack 的风格都了解一下,并对其优劣做一个对比,才能知其所以然。
|
||||
|
||||
### Hack Pipe 语法
|
||||
|
||||
Hack 语法相对冗余,在 Pipe 时使用 `%` 传递结果:
|
||||
|
||||
```js
|
||||
'123.45' |> Number(%)
|
||||
```
|
||||
|
||||
这个 `%` 可以用在任何地方,基本上原生 js 语法都支持:
|
||||
|
||||
```js
|
||||
value |> someFunction(1, %, 3) // function calls
|
||||
value |> %.someMethod() // method call
|
||||
value |> % + 1 // operator
|
||||
value |> [%, 'b', 'c'] // Array literal
|
||||
value |> {someProp: %} // object literal
|
||||
value |> await % // awaiting a Promise
|
||||
value |> (yield %) // yielding a generator value
|
||||
```
|
||||
|
||||
### F# Pipe 语法
|
||||
|
||||
F# 语法相对精简,默认不使用额外符号:
|
||||
|
||||
```js
|
||||
'123.45' |> Number
|
||||
```
|
||||
|
||||
但在需要显式声明参数时,为了解决上一个 Pipe 结果符号从哪来的问题,写起来反而更为复杂:
|
||||
|
||||
```js
|
||||
2 |> $ => add2(1, $)
|
||||
```
|
||||
|
||||
### await 关键字 - Hack 优
|
||||
|
||||
F# 在 `await` `yield` 时需要特殊语法支持,而 Hack 可以自然的使用 js 内置关键字。
|
||||
|
||||
```js
|
||||
// Hack
|
||||
value |> await %
|
||||
// F#
|
||||
value |> await
|
||||
```
|
||||
|
||||
F# 代码看上去很精简,但实际上付出了高昂的代价 - `await` 是一个仅在 Pipe 语法存在的关键字,而非普通 `await` 关键字。如果不作为关键字处理,执行逻辑就变成了 `await(value)` 而不是 `await value`。
|
||||
|
||||
### 解构 - F# 优
|
||||
|
||||
正因为 F# 繁琐的变量声明,反而使得在应对解构场景时得心应手:
|
||||
|
||||
```js
|
||||
// F#
|
||||
value |> ({ a, b }) => someFunction(a, b)
|
||||
// Hack
|
||||
value |> someFunction(%.a, %.b)
|
||||
```
|
||||
|
||||
Hack 也不是没有解构手段,只是比较繁琐。要么使用立即调用函数表达式 IIFE:
|
||||
|
||||
```js
|
||||
value |> (({ a, b }) => someFunction(a, b))(%)
|
||||
```
|
||||
|
||||
要么使用 `do` 关键字:
|
||||
|
||||
```js
|
||||
value |> do { const { a, b } = %; someFunction(a, b) }
|
||||
```
|
||||
|
||||
但 Hack 虽败犹荣,因为解决方法都使用了 js 原生提供的语法,所以反而体现出与 js 已有生态亲和性更强,而 F# 之所以能优雅解决,全都归功于自创的语法,这些语法虽然甜,但割裂了 js 生态,这是 F# like 提案被放弃的重要原因之一。
|
||||
|
||||
### 潜在改进方案
|
||||
|
||||
虽然选择了 Hack 风格,但 F# 与 Hack 各有优劣,所以列了几点优化方案。
|
||||
|
||||
#### 利用 Partial Application Syntax 提案降低 F# 传参复杂度
|
||||
|
||||
F# 被诟病的一个原因是传参不如 Hack 简单:
|
||||
|
||||
```js
|
||||
// Hack
|
||||
2 |> add2(1, %)
|
||||
// F#
|
||||
2 |> $ => add2(1, $)
|
||||
```
|
||||
|
||||
但如果利用处于 stage1 的提案 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 可以很好的解决问题。
|
||||
|
||||
这里就要做一个小插曲了。js 对柯里化没有原生支持,但 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 提案解决了这个问题,语法如下:
|
||||
|
||||
```js
|
||||
const add = (x, y) => x + y;
|
||||
const addOne = add~(1, ?);
|
||||
addOne(2); // 3
|
||||
```
|
||||
|
||||
即利用 `fn~(?, arg)` 的语法,将任意函数柯里化。这个特性解决 F# 传参复杂问题简直绝配,因为 F# 的每一个 Pipe 都要求是一个函数,我们可以将要传参的地方记为 `?`,这样返回值还是一个函数,完美符合 F# 的语法:
|
||||
|
||||
```js
|
||||
// F#
|
||||
2 |> add~(1, ?)
|
||||
```
|
||||
|
||||
上面的例子拆开看就是:
|
||||
|
||||
```js
|
||||
const addOne = add~(1, ?)
|
||||
2 |> addOne
|
||||
```
|
||||
|
||||
想法很美好,但 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 得先落地。
|
||||
|
||||
#### 融合 F# 与 Hack 语法
|
||||
|
||||
在简单情况下使用 F#,需要利用 `%` 传参时使用 Hack 语法,两者混合在一起写就是:
|
||||
|
||||
```js
|
||||
const resultArray = inputArray
|
||||
|> filter(%, str => str.length >= 0) // Hack
|
||||
|> map(%, str => '['+str+']') // Hack
|
||||
|> console.log // F#
|
||||
```
|
||||
|
||||
不过这个 [提案](https://github.com/tc39/proposal-smart-pipelines) 被废弃了。
|
||||
|
||||
#### 创造一个新的操作符
|
||||
|
||||
如果用 `|>` 表示 Hack 语法,用 `|>>` 表示 F# 语法呢?
|
||||
|
||||
```js
|
||||
const resultArray = inputArray
|
||||
|> filter(%, str => str.length >= 0) // Hack
|
||||
|> map(%, str => '['+str+']') // Hack
|
||||
|>> console.log // F#
|
||||
```
|
||||
|
||||
也是看上去很美好,但这个特性连提案都还没有。
|
||||
|
||||
### 如何用现有语法模拟 Pipe
|
||||
|
||||
即便没有 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案,也可以利用 js 现有语法模拟 Pipe 效果,以下是几种方案。
|
||||
|
||||
#### Function.pipe()
|
||||
|
||||
利用自定义函数构造 pipe 方法,该语法与 F# 比较像:
|
||||
|
||||
```js
|
||||
const resultSet = Function.pipe(
|
||||
inputSet,
|
||||
$ => filter($, x => x >= 0)
|
||||
$ => map($, x => x * 2)
|
||||
$ => new Set($)
|
||||
)
|
||||
```
|
||||
|
||||
缺点是不支持 `await`,且存在额外函数调用。
|
||||
|
||||
#### 使用中间变量
|
||||
|
||||
说白了就是把 Pipe 过程拆开,一步步来写:
|
||||
|
||||
```js
|
||||
const filtered = filter(inputSet, x => x >= 0)
|
||||
const mapped = map(filtered, x => x * 2)
|
||||
const resultSet = new Set(mapped)
|
||||
```
|
||||
|
||||
没什么大问题,就是比较冗余,本来可能一行能解决的问题变成了三行,而且还声明了三个中间变量。
|
||||
|
||||
#### 复用变量
|
||||
|
||||
改造一下,将中间变量变成复用的:
|
||||
|
||||
```js
|
||||
let $ = inputSet
|
||||
$ = filter($, x => x >= 0)
|
||||
$ = map($, x => x * 2)
|
||||
const resultSet = new Set($)
|
||||
```
|
||||
|
||||
这样做可能存在变量污染,可使用 IIFE 解决。
|
||||
|
||||
## 精读
|
||||
|
||||
Pipe Operator 语义价值非常明显,甚至可以改变编程的思维方式,在串行处理数据时非常重要,因此命令行场景非常常见,如:
|
||||
|
||||
```bash
|
||||
cat "somefile.txt" | echo
|
||||
```
|
||||
|
||||
因为命令行就是典型的输入输出场景,而且大部分都是单输入、单输出。
|
||||
|
||||
在普通代码场景,特别是处理数据时也需要这个特性,大部分具有抽象思维的代码都进行了各种类型的管道抽象,比如:
|
||||
|
||||
```js
|
||||
const newValue = pipe(
|
||||
value,
|
||||
doSomething1,
|
||||
doSomething2,
|
||||
doSomething3
|
||||
)
|
||||
```
|
||||
|
||||
如果 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案通过,我们就不需要任何库实现 pipe 动作,可以直接写成:
|
||||
|
||||
```js
|
||||
const newValue = value |> doSomething1(%) |> doSomething2(%) |> doSomething3(%)
|
||||
```
|
||||
|
||||
这等价于:
|
||||
|
||||
```js
|
||||
const newValue = doSomething3(doSomething2(doSomething1(value)))
|
||||
```
|
||||
|
||||
显然,利用 pipe 特性书写处理流程更为直观,执行逻辑与阅读逻辑是一致的。
|
||||
|
||||
### 实现 pipe 函数
|
||||
|
||||
即便没有 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案,我们也可以一行实现 pipe 函数:
|
||||
|
||||
```js
|
||||
const pipe = (...args) => args.reduce((acc, el) => el(acc))
|
||||
```
|
||||
|
||||
但要实现 Hack 参数风格是不可能的,顶多实现 F# 参数风格。
|
||||
|
||||
### js 实现 pipe 语法的考虑
|
||||
|
||||
从 [提案](https://github.com/tc39/proposal-pipeline-operator#tc39-has-rejected-f-pipes-multiple-times) 记录来看,F# 失败有三个原因:
|
||||
|
||||
- 内存性能问题。
|
||||
- `await` 特殊语法。
|
||||
- 割裂 js 生态。
|
||||
|
||||
其中割裂 js 生态是指因 F# 语法的特殊性,如果有太多库按照其语法实现功能,可能导致无法被非 Pipe 语法场景所复用。
|
||||
|
||||
甚至还有部分成员反对 [隐性编程(Tacit programming)](https://en.wikipedia.org/wiki/Tacit_programming),以及柯里化提案 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application),这些会使 js 支持的编程风格与现在差异过大。
|
||||
|
||||
看来处于鄙视链顶端的编程风格在 js 是否支持不是能不能的问题,而是想不想的问题。
|
||||
|
||||
### pipe 语法的弊端
|
||||
|
||||
下面是普通 `setState` 语法:
|
||||
|
||||
```ts
|
||||
setState(state => ({
|
||||
...state,
|
||||
value: 123
|
||||
}))
|
||||
```
|
||||
|
||||
如果改为 `immer` 写法如下:
|
||||
|
||||
```ts
|
||||
setState(produce(draft => draft.value = 123))
|
||||
```
|
||||
|
||||
得益于 ts 类型自动推导,在内层 `produce` 里就已经知道 `value` 是数值类型,此时如果输入字符串会报错,而如果其在另一个上下文的 `setState` 内,类型也会随着上下文的变化而变化。
|
||||
|
||||
但如果写成 pipe 模式:
|
||||
|
||||
```ts
|
||||
produce(draft => draft.value = 123) |> setState
|
||||
```
|
||||
|
||||
因为先考虑的是如何修改数据,此时还不知道后面的 pipe 流程是什么,所以 `draft` 的类型无法确定。所以 pipe 语法仅适用于固定类型的数据处理流程。
|
||||
|
||||
## 总结
|
||||
|
||||
pipe 直译为管道,潜在含义是 “数据像流水线一样被处理”,也可以形象理解为每个函数就是一个不同的管道,显然下一个管道要处理上一个管道的数据,并将结果输出到下一个管道作为输入。
|
||||
|
||||
合适的管道数量与体积决定了一条生产线是否高效,过多的管道类型反而会使流水线零散而杂乱,过少的管道会让流水线笨重不易拓展,这是工作中最大的考验。
|
||||
|
||||
> 讨论地址是:[精读《pipe operator for JavaScript》· Issue #395 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/395)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,308 @@
|
||||
[vue-lit](https://github.com/yyx990803/vue-lit) 基于 [lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) + [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 仅用 70 行代码就给模版引擎实现了 [Vue Composition API](https://vuejs.org/guide/extras/composition-api-faq.html),用来开发 web component。
|
||||
|
||||
## 概述
|
||||
|
||||
```html
|
||||
<my-component></my-component>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
html,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
onUnmounted
|
||||
} from 'https://unpkg.com/@vue/lit'
|
||||
|
||||
defineComponent('my-component', () => {
|
||||
const state = reactive({
|
||||
text: 'hello',
|
||||
show: true
|
||||
})
|
||||
const toggle = () => {
|
||||
state.show = !state.show
|
||||
}
|
||||
const onInput = e => {
|
||||
state.text = e.target.value
|
||||
}
|
||||
|
||||
return () => html`
|
||||
<button @click=${toggle}>toggle child</button>
|
||||
<p>
|
||||
${state.text} <input value=${state.text} @input=${onInput}>
|
||||
</p>
|
||||
${state.show ? html`<my-child msg=${state.text}></my-child>` : ``}
|
||||
`
|
||||
})
|
||||
|
||||
defineComponent('my-child', ['msg'], (props) => {
|
||||
const state = reactive({ count: 0 })
|
||||
const increase = () => {
|
||||
state.count++
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('child mounted')
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
console.log('child updated')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('child unmounted')
|
||||
})
|
||||
|
||||
return () => html`
|
||||
<p>${props.msg}</p>
|
||||
<p>${state.count}</p>
|
||||
<button @click=${increase}>increase</button>
|
||||
`
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
上面定义了 `my-component` 与 `my-child` 组件,并将 `my-child` 作为 `my-component` 的默认子元素。
|
||||
|
||||
```js
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
html,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
onUnmounted
|
||||
} from 'https://unpkg.com/@vue/lit'
|
||||
```
|
||||
|
||||
`defineComponent` 定义 custom element,第一个参数是自定义 element 组件名,必须遵循原生 API [customElements.define](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) 对组件名的规范,组件名必须包含中划线。
|
||||
|
||||
`reactive` 属于 [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 提供的响应式 API,可以创建一个响应式对象,在渲染函数中调用时会自动进行依赖收集,这样在 Mutable 方式修改值时可以被捕获,并自动触发对应组件的重渲染。
|
||||
|
||||
`html` 是 [lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) 提供的模版函数,通过它可以用 [Template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) 原生语法描述模版,是一个轻量模版引擎。
|
||||
|
||||
`onMounted`、`onUpdated`、`onUnmounted` 是基于 [web component lifecycle](https://developers.google.com/web/fundamentals/web-components/customelements#reactions) 创建的生命周期函数,可以监听组件创建、更新与销毁时机。
|
||||
|
||||
接下来看 `defineComponent` 的内容:
|
||||
|
||||
```js
|
||||
defineComponent('my-component', () => {
|
||||
const state = reactive({
|
||||
text: 'hello',
|
||||
show: true
|
||||
})
|
||||
const toggle = () => {
|
||||
state.show = !state.show
|
||||
}
|
||||
const onInput = e => {
|
||||
state.text = e.target.value
|
||||
}
|
||||
|
||||
return () => html`
|
||||
<button @click=${toggle}>toggle child</button>
|
||||
<p>
|
||||
${state.text} <input value=${state.text} @input=${onInput}>
|
||||
</p>
|
||||
${state.show ? html`<my-child msg=${state.text}></my-child>` : ``}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
借助模版引擎 [lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) 的能力,可以同时在模版中传递变量与函数,再借助 [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 能力,让变量变化时生成新的模版,更新组件 dom。
|
||||
|
||||
## 精读
|
||||
|
||||
阅读源码可以发现,vue-lit 巧妙的融合了三种技术方案,它们配合方式是:
|
||||
|
||||
1. 使用 [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 创建响应式变量。
|
||||
2. 利用模版引擎 [lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) 创建使用了这些响应式变量的 HTML 实例。
|
||||
3. 利用 [web component](https://developers.google.com/web/fundamentals/web-components/customelements) 渲染模版引擎生成的 HTML 实例,这样创建的组件具备隔离能力。
|
||||
|
||||
其中响应式能力与模版能力分别是 [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity)、[lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) 这两个包提供的,我们只需要从源码中寻找剩下的两个功能:如何在修改值后触发模版刷新,以及如何构造生命周期函数的。
|
||||
|
||||
首先看如何在值修改后触发模版刷新。以下我把与重渲染相关代码摘出来了:
|
||||
|
||||
```js
|
||||
import {
|
||||
effect
|
||||
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
|
||||
|
||||
customElements.define(
|
||||
name,
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
const template = factory.call(this, props)
|
||||
const root = this.attachShadow({ mode: 'closed' })
|
||||
effect(() => {
|
||||
render(template(), root)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
可以清晰的看到,首先 `customElements.define` 创建一个原生 web component,并利用其 API 在初始化时创建一个 `closed` 节点,该节点对外部 API 调用关闭,即创建的是一个不会受外部干扰的 web component。
|
||||
|
||||
然后在 `effect` 回调函数内调用 `html` 函数,即在使用文档里返回的模版函数,由于这个模版函数中使用的变量都采用 `reactive` 定义,所以 `effect` 可以精准捕获到其变化,并在其变化后重新调用 `effect` 回调函数,实现了 “值变化后重渲染” 的功能。
|
||||
|
||||
然后看生命周期是如何实现的,由于生命周期贯穿整个实现流程,因此必须结合全量源码看,下面贴出全量核心代码,上面介绍过的部分可以忽略不看,只看生命周期的实现:
|
||||
|
||||
```js
|
||||
let currentInstance
|
||||
|
||||
export function defineComponent(name, propDefs, factory) {
|
||||
if (typeof propDefs === 'function') {
|
||||
factory = propDefs
|
||||
propDefs = []
|
||||
}
|
||||
|
||||
customElements.define(
|
||||
name,
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
const props = (this._props = shallowReactive({}))
|
||||
currentInstance = this
|
||||
const template = factory.call(this, props)
|
||||
currentInstance = null
|
||||
this._bm && this._bm.forEach((cb) => cb())
|
||||
const root = this.attachShadow({ mode: 'closed' })
|
||||
let isMounted = false
|
||||
effect(() => {
|
||||
if (isMounted) {
|
||||
this._bu && this._bu.forEach((cb) => cb())
|
||||
}
|
||||
render(template(), root)
|
||||
if (isMounted) {
|
||||
this._u && this._u.forEach((cb) => cb())
|
||||
} else {
|
||||
isMounted = true
|
||||
}
|
||||
})
|
||||
}
|
||||
connectedCallback() {
|
||||
this._m && this._m.forEach((cb) => cb())
|
||||
}
|
||||
disconnectedCallback() {
|
||||
this._um && this._um.forEach((cb) => cb())
|
||||
}
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this._props[name] = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function createLifecycleMethod(name) {
|
||||
return (cb) => {
|
||||
if (currentInstance) {
|
||||
;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const onBeforeMount = createLifecycleMethod('_bm')
|
||||
export const onMounted = createLifecycleMethod('_m')
|
||||
export const onBeforeUpdate = createLifecycleMethod('_bu')
|
||||
export const onUpdated = createLifecycleMethod('_u')
|
||||
export const onUnmounted = createLifecycleMethod('_um')
|
||||
```
|
||||
|
||||
生命周期实现形如 `this._bm && this._bm.forEach((cb) => cb())`,之所以是循环,是因为比如 `onMount(() => cb())` 可以注册多次,因此每个生命周期都可能注册多个回调函数,因此遍历将其依次执行。
|
||||
|
||||
而生命周期函数还有一个特点,即并不分组件实例,因此必须有一个 `currentInstance` 标记当前回调函数是在哪个组件实例注册的,而这个注册的同步过程就在 `defineComponent` 回调函数 `factory` 执行期间,因此才会有如下的代码:
|
||||
|
||||
```js
|
||||
currentInstance = this
|
||||
const template = factory.call(this, props)
|
||||
currentInstance = null
|
||||
```
|
||||
|
||||
这样,我们就将 `currentInstance` 始终指向当前正在执行的组件实例,而所有生命周期函数都是在这个过程中执行的,**因此当调用生命周期回调函数时,`currentInstance` 变量必定指向当前所在的组件实例**。
|
||||
|
||||
接下来为了方便,封装了 `createLifecycleMethod` 函数,在组件实例上挂载了一些形如 `_bm`、`_bu` 的数组,比如 `_bm` 表示 `beforeMount`,`_bu` 表示 `beforeUpdate`。
|
||||
|
||||
接下来就是在对应位置调用对应函数了:
|
||||
|
||||
首先在 `attachShadow` 执行之前执行 `_bm` - `onBeforeMount`,因为这个过程确实是准备组件挂载的最后一步。
|
||||
|
||||
然后在 `effect` 中调用了两个生命周期,因为 `effect` 会在每次渲染时执行,所以还特意存储了 `isMounted` 标记是否为初始化渲染:
|
||||
|
||||
```js
|
||||
effect(() => {
|
||||
if (isMounted) {
|
||||
this._bu && this._bu.forEach((cb) => cb())
|
||||
}
|
||||
render(template(), root)
|
||||
if (isMounted) {
|
||||
this._u && this._u.forEach((cb) => cb())
|
||||
} else {
|
||||
isMounted = true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
这样就很容易看懂了,只有初始化渲染过后,从第二次渲染开始,在执行 `render`(该函数来自 `lit-html` 渲染模版引擎)之前调用 `_bu` - `onBeforeUpdate`,在执行了 `render` 函数后调用 `_u` - `onUpdated`。
|
||||
|
||||
由于 `render(template(), root)` 根据 `lit-html` 的语法,会直接把 `template()` 返回的 HTML 元素挂载到 `root` 节点,而 `root` 就是这个 web component `attachShadow` 生成的 shadow dom 节点,因此这句话执行结束后渲染就完成了,所以 `onBeforeUpdate` 与 `onUpdated` 一前一后。
|
||||
|
||||
最后几个生命周期函数都是利用 web component 原生 API 实现的:
|
||||
|
||||
```js
|
||||
connectedCallback() {
|
||||
this._m && this._m.forEach((cb) => cb())
|
||||
}
|
||||
disconnectedCallback() {
|
||||
this._um && this._um.forEach((cb) => cb())
|
||||
}
|
||||
```
|
||||
|
||||
分别实现 `mount`、`unmount`。这也说明了浏览器 API 分层的清晰之处,只提供创建和销毁的回调,而更新机制完全由业务代码实现,不管是 [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 的 `effect` 也好,还是 `addEventListener` 也好,都不关心,所以如果在这之上做完整的框架,需要自己根据实现 `onUpdate` 生命周期。
|
||||
|
||||
最后的最后,还利用 `attributeChangedCallback` 生命周期监听自定义组件 html attribute 的变化,然后将其直接映射到对 `this._props[name]` 的变化,这是为什么呢?
|
||||
|
||||
```js
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this._props[name] = newValue
|
||||
}
|
||||
```
|
||||
|
||||
看下面的代码片段就知道原因了:
|
||||
|
||||
```js
|
||||
const props = (this._props = shallowReactive({}))
|
||||
const template = factory.call(this, props)
|
||||
effect(() => {
|
||||
render(template(), root)
|
||||
})
|
||||
```
|
||||
|
||||
早在初始化时,就将 `_props` 创建为响应式变量,这样只要将其作为 [lit-html](https://github.com/lit/lit/blob/main/packages/lit-html/README.md) 模版表达式的参数(对应 `factory.call(this, props)` 这段,而 `factory` 就是 `defineComponent('my-child', ['msg'], (props) => { ..` 的第三个参数),这样一来,只要这个参数变化了就会触发子组件的重渲染,因为这个 `props` 已经经过 Reactive 处理了。
|
||||
|
||||
## 总结
|
||||
|
||||
[vue-lit](https://github.com/yyx990803/vue-lit) 实现非常巧妙,学习他的源码可以同时了解一下几种概念:
|
||||
|
||||
- reative。
|
||||
- web component。
|
||||
- string template。
|
||||
- 模版引擎的精简实现。
|
||||
- 生命周期。
|
||||
|
||||
以及如何将它们串起来,利用 70 行代码实现一个优雅的渲染引擎。
|
||||
|
||||
最后,用这种模式创建的 web component 引入的 runtime lib 在 gzip 后只有 6kb,但却能享受到现代化框架的响应式开发体验,如果你觉得这个 runtime 大小可以忽略不计,那这就是一个非常理想的创建可维护 web component 的 lib。
|
||||
|
||||
> 讨论地址是:[精读《vue-lit 源码》· Issue #396 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/396)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,143 @@
|
||||
Markdown 即便在 2022 年也非常常用,比如这篇文章依然采用 Markdown 编写。
|
||||
|
||||
但 Markdown 是否应该成为文本编辑领域的默认技术选型呢?答案是否定的。我找到了一篇批判无脑使用 Markdown 作为技术选型的好文 [Thoughts On Markdown](https://www.smashingmagazine.com/2022/02/thoughts-on-markdown/),它提到 Markdown 在标准化、结构化、组件化都存在硬伤,如果你真的想做一个现代化的文本结构编辑器,不要采用 Markdown。
|
||||
|
||||
## 概述
|
||||
|
||||
Markdown 流传甚广,甚至已成为我们的第二语言。Markdown 最早的解析器由 John Gruber 在 2004 年基于 Perl 编写发布,那时候 Markdown 只有一个目的,即为了方便网络写作。
|
||||
|
||||
网络写作必须基于 HTML 规范,而 HTML 规范对大部分人上手成本太高,因此 Markdown 就是基于文本创建的更易理解,或者说上手成本更低,甚至傻瓜化的一种语法,而要解析这个语法需要配套一个解析器,将这种语法文本最终转化为 HTML。
|
||||
|
||||
而数字化发展到今天,Markdown 已不再适合当下的写作场景了,主要原因有二:
|
||||
|
||||
1. Markdown 不再适合当下富交互、内容形态的编写。
|
||||
2. Markdown 纯文本的开发体验不再满足当代开发者日益提高的体验需求。
|
||||
|
||||
首先还是从 Markdown 思想开始介绍。
|
||||
|
||||
### Markdown 的核心思想
|
||||
|
||||
Markdown 最大优势就是好上手,不需要接触 HTML 这种复杂的嵌套语句(虽然对程序员来说 HTML 也简单到处于鄙视链底端)。原文抽象了三个优势:
|
||||
|
||||
1. 基于文本的合适抽象。虽然 HTML 甚至代码都是文本,但 “合适” 这个词很重要,即任何文本都可以是 Markdown,只要加一点点小标记就能描述专业结构,学习成本极低。
|
||||
2. 有大量生态工具。比如语法解析器、高亮、格式转换、格式化、渲染等工具完备。
|
||||
3. 编辑内容便于维护。比如 Markdown 很方便作为源码存储,而其他格式的富文本可能并不方便在源码里维护。
|
||||
|
||||
如果把 Markdown 与数据库表结构做比较,那数据库的理解成本真是太高了。
|
||||
|
||||
但是在如今后端即服务的时代,数据库访问越来越轻松,甚至出现大量如 AirTable 等 SAAS 产品将结构化数据快速转化为应用,其实接触了这些后才真正发现,结构化数据对开发者有多重要。Markdown 用来写写文章还是不错的,但用来表达逻辑结构最后一定会引发灾难后果,原文作者的团队就深受 Markdown 技术选型的困扰,被迫解决大量远超预期的难题。
|
||||
|
||||
如果真的要在 Markdown 的坑越走越深,就必须使用语法拓展来满足自定义诉求。
|
||||
|
||||
### Markdown 语法拓展
|
||||
|
||||
最初 Markdown 语法是不支持表格的,如果想用 Markdown 绘制一张表格,只能使用原生 HTML 标签:`<tabke></table>`,当然,这也说明了 Markdown 本质就是给 HTML 加强了便捷的语法,我们完全可以将其当 HTML 使用。
|
||||
|
||||
然而并不是所有创作平台都支持 `<table></table>` 语法的,笔者自己就经常受到困扰,比如有些平台会屏蔽原生 HTML 语法,已保障所谓的 “安全性” 或者内容体验的 “一致性”,而这些平台为了弥补缺失的绘制表格能力,往往会支持一些自定义语法,更糟糕的是不支持,这就说到了 Markdown 的语法拓展。
|
||||
|
||||
Markdown 有哪些拓展呢?比如:[multiMarkdown](https://fletcherpenney.net/multimarkdown/)、[commonMark](https://commonmark.org/)、[Github Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github) 等等。
|
||||
|
||||
这里随便举个例子,比如标准 MD 格式,其实第一行最后要加两个空格才能换行,但 GFM 取消了这个限制。这虽然更方便了,但暴露出平台间规范的不一致性,导致 Markdown 跨平台基本一定被坑。
|
||||
|
||||
而各平台拓展的语法,我们是否有足够的精力学习和记忆呢?先不说能不能记得下来,首先值不值得学习就是个问题,为什么一个网络写作平台需要占用写手学习与认知成本,而不是想办法去简化写作流程呢?所以语法拓展看似很美好,但放在写手角度,或者整个互联网各平台林立的角度来看,这种非标准的做法一定不靠谱,没有用户觉得你的平台有资格 “教他语法”,除非你是微信,钉钉或者飞书。
|
||||
|
||||
原文提到的观点是:
|
||||
|
||||
1. 作为写手,你不知道 Markdown 哪些语法可用,哪些语法不可用。
|
||||
2. 标准规范存在一些 [模糊地带](https://johnmacfarlane.net/babelmark2/faq.html#what-is-this-for) 导致开发者实现时也会遇到各种纠结。
|
||||
|
||||
原文还提到一个语法拓展导致理解成本增高的例子:slack 平台自定义的 [mrkdown](https://api.slack.com/reference/surfaces/formatting#basics) 就不支持 `[link](https://slack.com)` 方式描述链接,而使用了 `<link|https://slack.com>` 语法。
|
||||
|
||||
总结来说,Markdown 语法拓展本应该是件好事,但实际无标准导致了标准的百花齐放,使 Markdown 成为了实际上没有标准的状态,整体来看弊端更多。
|
||||
|
||||
### Markdown 面向的用户群
|
||||
|
||||
Markdown 的对自己的定位其实很不清晰,这也导致了一直不想确定标准化语法。
|
||||
|
||||
最初 Markdown 是服务给熟悉 HTML 的人提供的标记语言,而后来面向用户群实质上转向了开发者,因为开发者才会想到拓展语法以满足更复杂的使用场景,Markdown 原生语法无法适应越来越复杂的视觉展示形态。
|
||||
|
||||
如今 Markdown 的主要用户已经是开发人员与对代码感兴趣的人了,这倒不是说开发者有多喜欢它,而是在说 Markdown 的受众变窄了。如今任何一款面向非开发者群体的文档编辑器都不会采用 Markdown 了,而是所见即所得的 WYSIWYG(what you see is what you want)模式。
|
||||
|
||||
这个转变的过程是痛苦的,但现在来看,富文本编辑器不应用用 Markdown 语法,而是 WYSIWYG 模式已经是共识了。
|
||||
|
||||
### 从段落到区块、从文章到应用
|
||||
|
||||
简单来说,即 Markdown 已经不适应当前 HTML 丰富的生态了,能轻松描述段落的标记语言,遇到富有交互的组件区块时,不得不引入例如 [MDX](https://mdxjs.com/) 等方案,但这样的方案根本只适合程序员群体,完全无法移植。
|
||||
|
||||
网络浏览形态也从简单的文章发展到具有整体性的应用,这些应用拥有复杂的布局、样式与交互,如果你尝试基于 Markdown 拓展语法来支持,最后可能发现还不如直接用原生 HTML。
|
||||
|
||||
### 对结构化内容的诉求
|
||||
|
||||
从编程角度理解就是 “组件复用”。Markdown 原生语法无法实现内容的复用,如果必须要复用内容,只能将其重复写在每一处,势必造成巨大同步成本。
|
||||
|
||||
比如 Jekyll 就提出了 [FrontMatter](https://jekyllrb.com/docs/front-matter/) 概念用来创建复用的变量:
|
||||
|
||||
```yml
|
||||
---
|
||||
food: Pizza
|
||||
---
|
||||
|
||||
<h1>{{ page.food }}</h1>
|
||||
```
|
||||
|
||||
### WYSIWYG 编辑器不应将 HTML 作为底层数据结构
|
||||
|
||||
虽然浏览器真正将 HTML 作为底层数据结构,但这并不代表所见即所得的编辑器也可以如此,这也是为什么浏览器只能提供从源码到 UI 的输出,而不能提供从 UI 编辑到源码的反向输入。
|
||||
|
||||
因为用户的输入与 HTML 并不是一一对应关系,其中存在大量模糊地带,比如当前光标处在粗体与细体文字中间,那下一个输入到底算加粗还是不加粗呢?从 UI 上看不到加粗标签。再有,如果 HTML 存在冗余,其实当前光标所在位置已经被加粗标签包裹了好几层,但因为光标所在区域又被另一个样式标签覆盖成非加粗模式,当再次输入时可能就跳出了覆盖范围,重新变成了加粗,这个过程符合用户预期吗?从技术上,这种复杂标签结构也几乎无法被处理,因为组合花样实在太多。
|
||||
|
||||
现代大多数编辑器都以 JSON 格式存储数据结构,就因为其结构化且易于检索。
|
||||
|
||||
结构化最重要的体现是,其生成的 HTML 结构可以是稳定的,即对于一个既加粗又标红的文字,一定包裹在一个 `<strong style="color: red">` 标签里,而不是 `<strong><div style="color: red">`,也就是这种模式根本没把 HTML 作为结构化数据去看待,自然就不会出现歧义。
|
||||
|
||||
Markdown 也是一样,其本身也会出现类似 HTML 标签的二义性,不适合作为底层数据结构存储。
|
||||
|
||||
## 精读
|
||||
|
||||
批判 Markdown 的文章不多见,笔者也是看了之后才恍然发现 Markdown 竟然有这么多缺点。笔者结合自己的经验谈谈 Markdown 的缺点吧。
|
||||
|
||||
### 不支持富交互的无奈
|
||||
|
||||
Markdown 仅能支持简单的 HTML 结构,而无法描述逻辑区块。Github 上大部分 Readme 都采用图片来实现这些功能,包括状态卡片、构建结果、个人信息名片等,可惜交互能力还是太弱,我觉得有朝一日 Github 应该会推出比如 Block 小区块的概念,让这些区块可以直接插入 Markdown 成为一个可交互的元素。
|
||||
|
||||
### MDX 解决了 Markdown 的痛点吗?
|
||||
|
||||
看似完美兼容 JSX 与 Markdown 的 MDX 曾经也是笔者写作的救命稻草,但该方案移植性是一大痛点,组件只能在自己部署的网站用,如果你想把文章发布到另一个平台,完全不可能。
|
||||
|
||||
这还仅是笔者的视角,如果从 Markdown 生态来看,MDX 面向用户仅是程序员群体,根本没有解决其使命 “方便网络写作”,而程序员最终也会抛弃 MDX 而转向开发所见即所得编辑器解决问题。
|
||||
|
||||
### Markdown 到 HTML 的转换存在逻辑问题
|
||||
|
||||
Markdown 本质上还是一种脱离 HTML 的文本表示结构,看上去解耦很优雅,实际上会遇到不少不一致的问题。
|
||||
|
||||
比如说连续敲击多个空格会出现什么情况呢?在 Markdown 会变成一个引用区块,那如何才能展示多个空格呢?谁也不知道,可能需要查阅具体平台提供的额外语法才可以做到。
|
||||
|
||||
这种大体上用起来方便,但细节无法定制,甚至用户无法控制的情况会大大伤害已经深度使用 Markdown 的用户,此时用户要么硬着头皮发明新语法解决这些漏洞,要么就完全放弃 Markdown 了。
|
||||
|
||||
### 结构化能力不足
|
||||
|
||||
看上去 Markdown 的语法挺具有结构化的,但实际上 Markdown 的结构化不具有强约束力。
|
||||
|
||||
拿 JSON 作对比,比如我们可以用 JSON 拓展出 [https://json-schema.org/](jsonSchema) 结构,这个结构甚至可以反推出一个完整的表单应用,其原因是 JSON 可以针对每一个 Key、层级下定义,首先有结构,其次才有内容。
|
||||
|
||||
而 Markdown 正好反过来,是先有内容,再有结构。比如我们可以在 Markdown 任何地方写任何 HTML 标签,或者任意段落的问题,这些内容是无法被序列化的,即便我们按照浏览器解析 HTML 的规则解析成 JSON,也无法从中方便的提取信息。
|
||||
|
||||
背后的根本原因是,Markdown 本身定位就是 “近乎于 UI 渲染结果” 的,而实际上浏览器渲染 UI 背后是需要一套严谨的 HTML 语法,因为 UI 与背后语法并不能一一建立映射,一个稳定的渲染逻辑只能是从源码推导到渲染,而不能从渲染反推出源码。Markdown 本身定位就近乎于渲染结果,所以结构化能力不足是天然的问题。
|
||||
|
||||
## 总结
|
||||
|
||||
记得语雀早期内部试用时,编辑态还是采用 Markdown 的,但后来很快就把 Markdown 的编辑入口下掉了,这件事还引发了不少开发者的不满,甚至还有一些 Markdown 编辑的插件被开发出来,一度很受欢迎。但渐渐的我们都习惯用所见即所得方式编辑了,Markdown 唯一留给我们的印象就是快捷键,比如 `###` 后敲入空格可以生成 `h3` 标题段落,而语雀编辑器也在富交互组件区块上越走越远,要是当年被 Markdown 锁定住了技术,也不可能有今天这么高级的编辑体验。
|
||||
|
||||
所以技术前瞻性真的很重要,Markdown 所有程序员都爱,但提前看到它在当前互联网发展阶段的局限性,并设计一套结构化数据代替 Markdown 结构不是所有人都能想到的,我们需要以动态的眼光看待技术,也要放下技术人的偏见,把偏爱让位于产品定位。
|
||||
|
||||
> 讨论地址是:[精读《对 Markdown 的思考》· Issue #397 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/397)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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 上的图:
|
||||
|
||||

|
||||
|
||||
每一种西文字体的 x-height 是不一样的。非常幸运,Jukka Korpela 做了一网站专门可以测量 web 上字体的 x-height。其中,Arial 的 X-HEIGHT RATIO 是 0.519,而 Tahoma 是 0.545,Times New Roman 是 0.448。Arial 和 Times New Roman 之间的比例差距大概 17%。
|
||||
|
||||
@@ -47,6 +48,7 @@ Atomic Design 书中提到模块化思路以及原子级的模块抽象的方法
|
||||
这是第一个问题,第二个问题是中文字体没有 x-height,也就是说中文字体就等同于西文字体的全大写,错落的美感都没有。而且中文有一个问题是因为字形之前的差异,每个字之间的留白都不尽相同,看上去又会差一些。一般情况下,靠行间距来弥补视觉差,但总体上要排版达到西文字体的效果要花一些功夫。
|
||||
|
||||
2. 屏幕
|
||||
|
||||
我们的字体大小使用的是 points(pt),points 是一个物理衡量,它的标准是 72 points per inch(PPI)。但我们不同设备的 PPI 都是不一样的,那么造成了同样的设定在不同屏幕下看到的字体也会有差异。
|
||||
|
||||
Macbook Pro 的 PPI 是 220,Dell XPS 的 PPI 是 165,iPhone 7 有 326,但 iPhone 7p 的 PPI 有 401,而一般 HDTV 的 PPI 是 30。其中,iPhone,Macbook 都是 retina 屏。
|
||||
@@ -64,4 +66,4 @@ Macbook Pro 的 PPI 是 220,Dell XPS 的 PPI 是 165,iPhone 7 有 326,但
|
||||
## 总结
|
||||
曾经有国外的设计师有写文用黄金比例来构建字号与行高的关系,在一片喝彩中看到了资深设计师的反对,主要也是从以上和一些其它因素来说关系是比较难设定。
|
||||
|
||||
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
|
||||
今天看到我们的设计与理性之间建立的关系,我还是比较坚信建立这种关系背后带来的是更大的价值。
|
||||
|
||||
@@ -5,14 +5,14 @@ ES 模块为 JavaScript 开发者带来了官方并且标准化的模块系统
|
||||
## 1. 引言
|
||||
|
||||
精读文章主要讨论了下面几点:
|
||||
- 模块旨在解决那些问题;
|
||||
- 模块旨在解决哪些问题;
|
||||
- 模块为开发者带来哪些;
|
||||
- ES 模块化的工作机制;
|
||||
- ES 模块化的现状;
|
||||
|
||||
## 2. 内容概要
|
||||
|
||||
### 模块旨在解决那些问题
|
||||
### 模块旨在解决哪些问题
|
||||
|
||||
JavaScript 开发可以简单地抽象成维护变量,赋值和计算操作。大量的代码在用于操作变量,开发者需要懂得如何去组织和维护这些变量。JavaScript 提供了一种方式,即函数作用域。在一个函数内只需要考虑这个函数的变量问题。不必去担心其他函数会操作这些变量。当然,随之带来的问题是,变量无法共享,无法在不同的函数之间相互共享变量。如果想要在作用域外共享变量,只能通过外层作用域,或者全局作用域。
|
||||
|
||||
@@ -78,7 +78,7 @@ ES 模块需要借助模块加载器来实现这三步。加载器在不同的
|
||||
|
||||
这就意味着我们必须一层一层的遍历文件树,转化文件并找出依赖,最后查找并且加载这些依赖。如果主线程正在等待去下载这些文件,那么很多的任务会堆积在队列中。这是因为浏览器环境下下载用了很长时间。
|
||||
|
||||
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种查分构建的方式是 ES 模块和 CJS 模块最本质的不同。
|
||||
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种差分构建的方式是 ES 模块和 CJS 模块最本质的不同。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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[]>(
|
||||
|
||||
@@ -36,7 +36,7 @@ RPC 主要用来做服务器之间的方法调用,影响其性能最重要因
|
||||
|
||||
gRPC 主要用于服务之间传输,这里拿 Nodejs 举例:
|
||||
|
||||
1. 定义接口。由于 gRPC 使用 protobufs,所以接口定义文件就是 `helloword.proto`:
|
||||
1. 定义接口。由于 gRPC 使用 protobufs,所以接口定义文件就是 `helloworld.proto`:
|
||||
|
||||
```protobufs
|
||||
// The greeting service definition.
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
**也就是学习技术细节是没有技术门槛,随着年龄的增加,如果只累积了大家都能学会的内容,那么当旧知识被淘汰后,学习新知识的速度又不如年轻人快,会逐渐失去经验优势。**
|
||||
|
||||
那么如何利用无门槛的特征,将其变为门槛呢?那就是任何年龄段学习技术细节都很容易,在你需要深入细节的时候再深入进去,不需要深入的时候把时间花在了解宏观架构上。
|
||||
那么如何利用无门槛的特征,将其变为门槛呢?任何年龄段学习技术细节都很容易,应该在你需要深入细节的时候再深入进去,不需要深入的时候把时间花在了解宏观架构上。
|
||||
|
||||
就是培养高效的学习能力,能准确判断某个技术细节是否有必要掌握,如需要该如何快速掌握核心内容,并在掌握之后不留恋,可以快速抽身出来继续全局性思考。这种思维是有门槛的,技术专家都可以做到这一点。
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
这要看怎么理解业务与技术的关系,比如建设 “数据联邦”,光是了解各个不同的存储系统技术细节可能就要花很久,而实际上是没必要将所有技术细节都弄懂的,只要定好一个通用交互规范,各存储系统各自封装一套符合这个规范的交互接口即可。
|
||||
|
||||
做成事往往需要宏观的技术思维,需要将许多技术点链接在一起。举个例子,做成事就类似于军官指挥作战,做成的目的是通过制定打法赢得战争,而不是自己冲锋陷阵并测量敌人壕沟的宽度。关心技术细节只最终落实到每个人具体实施项中的一部分,技术细节的目标累加起来才是做成事。
|
||||
做成事往往需要宏观的技术思维,需要将许多技术点链接在一起。举个例子,做成事就类似于军官指挥作战,做成的目的是通过制定打法赢得战争,而不是自己冲锋陷阵并测量敌人壕沟的宽度。关心技术细节只是最终落实到每个人具体实施项中的一部分,技术细节的目标累加起来才能做成事。
|
||||
|
||||
## 2.2 搞清楚业务对技术的真实诉求
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
拥有技术思维的人,容易沉迷于解决不切实际的问题,或者是别人解决过的问题。这种思维对技术学习是非常有帮助的,但如果长期不能转变这种思维,对公司来说是无法创造什么价值的。
|
||||
|
||||
拥有业务思维的人,首先要懂业务,只有懂业务,跟着对的业务,才能对未来又信心,知道自己的付出可以换来回报。
|
||||
拥有业务思维的人,首先要懂业务,只有懂业务,跟着对的业务,才能对未来有信心,知道自己的付出可以换来回报。
|
||||
|
||||
懂业务后,才知道如何通过技术帮助业务获得成功。
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
现在技术点越来越多,如果什么技术细节都要详细了解,最终一定不能有很好的全局视野。比较好的状态是找几个重点深入了解,其他的技术点在掌握了全局技术视野后再考虑深入。
|
||||
|
||||
在互联网初期,很多技术框架还不完善时,技术借力的意义不大,毕竟也没有多少东西可用。
|
||||
在互联网初期,很多技术框架还不完善,技术借力的意义不大,毕竟也没有多少东西可用。
|
||||
|
||||
但是现在无论前端还是后端的技术、轮子已经眼花缭乱了,能掌握这些已有技术的人,价值已经逐渐大于会完整了解某些技术细节的人。一个优秀的专家应该能快速定位要解决的业务问题是否有成熟的技术方案,如何以最小的投入产出比实现,同时保持良好的维护性应变业务维护。
|
||||
|
||||
|
||||
@@ -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 的做法让我想到了自家小区物业费的收取方式,每年年初都会提前征收一整年的物业费,抛开商业手法不谈,这至少意味着物业对业务整整一年的承诺,这种承诺支撑了物业后续一整年的服务,也支撑了每周下一次的精读文章。
|
||||
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
[zustand](https://github.com/pmndrs/zustand) 是一个非常时髦的状态管理库,也是 2021 年 Star 增长最快的 React 状态管理库。它的理念非常函数式,API 设计的很优雅,值得学习。
|
||||
|
||||
## 概述
|
||||
|
||||
首先介绍 [zustand](https://github.com/pmndrs/zustand) 的使用方法。
|
||||
|
||||
### 创建 store
|
||||
|
||||
通过 `create` 函数创建 store,回调可拿到 `get` `set` 就类似 Redux 的 `getState` 与 `setState`,可以获取 store 瞬时值与修改 store。返回一个 hook 可以在 React 组件中访问 store。
|
||||
|
||||
```typescript
|
||||
import create from 'zustand'
|
||||
|
||||
const useStore = create((set, get) => ({
|
||||
bears: 0,
|
||||
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
|
||||
removeAllBears: () => set({ bears: 0 })
|
||||
}))
|
||||
```
|
||||
|
||||
上面例子是全局唯一的 store,也可以通过 `createContext` 方式创建多实例 store,结合 Provider 使用:
|
||||
|
||||
```tsx
|
||||
import create from 'zustand'
|
||||
import createContext from 'zustand/context'
|
||||
|
||||
const { Provider, useStore } = createContext()
|
||||
|
||||
const createStore = () => create(...)
|
||||
|
||||
const App = () => (
|
||||
<Provider createStore={createStore}>
|
||||
...
|
||||
</Provider>
|
||||
)
|
||||
```
|
||||
|
||||
### 访问 store
|
||||
|
||||
通过 `useStore` 在组件中访问 store。与 redux 不同的是,无论普通数据还是函数都可以存在 store 里,且函数也通过 selector 语法获取。因为函数引用不可变,所以实际上下面第二个例子不会引发重渲染:
|
||||
|
||||
```typescript
|
||||
function BearCounter() {
|
||||
const bears = useStore(state => state.bears)
|
||||
return <h1>{bears} around here ...</h1>
|
||||
}
|
||||
|
||||
function Controls() {
|
||||
const increasePopulation = useStore(state => state.increasePopulation)
|
||||
return <button onClick={increasePopulation}>one up</button>
|
||||
}
|
||||
```
|
||||
|
||||
如果嫌访问变量需要调用多次 `useStore` 麻烦,可以自定义 compare 函数返回一个对象:
|
||||
|
||||
```typescript
|
||||
const { nuts, honey } = useStore(state => ({ nuts: state.nuts, honey: state.honey }), shallow)
|
||||
```
|
||||
|
||||
### 细粒度 memo
|
||||
|
||||
利用 `useCallback` 甚至可以跳过普通 compare,而仅关心外部 id 值的变化,如:
|
||||
|
||||
```typescript
|
||||
const fruit = useStore(useCallback(state => state.fruits[id], [id]))
|
||||
```
|
||||
|
||||
原理是 id 变化时,`useCallback` 返回值才会变化,而 `useCallback` 返回值如果不变,`useStore` 的 compare 函数引用对比就会为 `true`,非常巧妙。
|
||||
|
||||
### set 合并与覆盖
|
||||
|
||||
`set` 函数第二个参数默认为 `false`,即合并值而非覆盖整个 store,所以可以利用这个特性清空 store:
|
||||
|
||||
```typescript
|
||||
const useStore = create(set => ({
|
||||
salmon: 1,
|
||||
tuna: 2,
|
||||
deleteEverything: () => set({ }, true), // clears the entire store, actions included
|
||||
}))
|
||||
```
|
||||
|
||||
### 异步
|
||||
|
||||
所有函数都支持异步,因为修改 store 并不依赖返回值,而是调用 `set`,所以是否异步对数据流框架来说都一样。
|
||||
|
||||
### 监听指定变量
|
||||
|
||||
还是用英文比较表意,即 `subscribeWithSelector`,这个中间件可以让我们把 selector 用在 subscribe 函数上,相比于 redux 传统的 subscribe,就可以有针对性的监听了:
|
||||
|
||||
```typescript
|
||||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
const useStore = create(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })))
|
||||
|
||||
// Listening to selected changes, in this case when "paw" changes
|
||||
const unsub2 = useStore.subscribe(state => state.paw, console.log)
|
||||
// Subscribe also exposes the previous value
|
||||
const unsub3 = useStore.subscribe(state => state.paw, (paw, previousPaw) => console.log(paw, previousPaw))
|
||||
// Subscribe also supports an optional equality function
|
||||
const unsub4 = useStore.subscribe(state => [state.paw, state.fur], console.log, { equalityFn: shallow })
|
||||
// Subscribe and fire immediately
|
||||
const unsub5 = useStore.subscribe(state => state.paw, console.log, { fireImmediately: true })
|
||||
```
|
||||
|
||||
后面还有一些结合中间件、immer、localstorage、redux like、devtools、combime store 就不细说了,都是一些细节场景。值得一提的是,所有特性都是正交的。
|
||||
|
||||
## 精读
|
||||
|
||||
其实大部分使用特性都在利用 React 语法,所以可以说 50% 的特性属于 React 通用特性,只是写在了 [zustand](https://github.com/pmndrs/zustand) 文档里,看上去像是 zustand 的特性,所以这个库真的挺会借力的。
|
||||
|
||||
### 创建 store 实例
|
||||
|
||||
任何数据流管理工具,都有一个最核心的 store 实例。对 zustand 来说,便是定义在 `vanilla.ts` 文件的 `createStore` 了。
|
||||
|
||||
`createStore` 返回一个类似 redux store 的数据管理实例,拥有四个非常常见的 API:
|
||||
|
||||
```typescript
|
||||
export type StoreApi<T extends State> = {
|
||||
setState: SetState<T>
|
||||
getState: GetState<T>
|
||||
subscribe: Subscribe<T>
|
||||
destroy: Destroy
|
||||
}
|
||||
```
|
||||
|
||||
首先 `getState` 的实现:
|
||||
|
||||
```typescript
|
||||
const getState: GetState<TState> = () => state
|
||||
```
|
||||
|
||||
就是这么简单粗暴。再看 `state`,就是一个普通对象:
|
||||
|
||||
```typescript
|
||||
let state: TState
|
||||
```
|
||||
|
||||
这就是数据流简单的一面,没有魔法,数据存储用一个普通对象,仅此而已。
|
||||
|
||||
接着看 `setState`,它做了两件事,修改 `state` 并执行 `listenser`:
|
||||
|
||||
```typescript
|
||||
const setState: SetState<TState> = (partial, replace) => {
|
||||
const nextState = typeof partial === 'function' ? partial(state) : partial
|
||||
if (nextState !== state) {
|
||||
const previousState = state
|
||||
state = replace ? (nextState as TState) : Object.assign({}, state, nextState)
|
||||
listeners.forEach((listener) => listener(state, previousState))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
修改 `state` 也非常简单,唯一重要的是 `listener(state, previousState)`,那么这些 `listeners` 是什么时候注册和声明的呢?其实 `listeners` 就是一个 Set 对象:
|
||||
|
||||
```typescript
|
||||
const listeners: Set<StateListener<TState>> = new Set()
|
||||
```
|
||||
|
||||
注册和销毁时机分别是 `subscribe` 与 `destroy` 函数调用时,这个实现很简单、高效。对应代码就不贴了,很显然,`subscribe` 时注册的监听函数会作为 `listener` 添加到 `listeners` 队列中,当发生 `setState` 时便会被调用。
|
||||
|
||||
最后我们看 `createStore` 的定义与结尾:
|
||||
|
||||
```typescript
|
||||
function createStore(createState) {
|
||||
let state: TState
|
||||
const setState = /** ... */
|
||||
const getState = /** ... */
|
||||
/** ... */
|
||||
const api = { setState, getState, subscribe, destroy }
|
||||
state = createState(setState, getState, api)
|
||||
return api
|
||||
}
|
||||
```
|
||||
|
||||
虽然这个 `state` 是个简单的对象,但回顾使用文档,我们可以在 `create` 创建 store 利用 callback 对 state 赋值,那个时候的 `set`、`get`、`api` 就是上面代码倒数第二行传入的:
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
|
||||
const useStore = create((set, get) => ({
|
||||
bears: 0,
|
||||
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
|
||||
removeAllBears: () => set({ bears: 0 })
|
||||
}))
|
||||
```
|
||||
|
||||
至此,初始化 store 的所有 API 的来龙去脉就梳理清楚了,逻辑简单清晰。
|
||||
|
||||
### create 函数的实现
|
||||
|
||||
上面我们说清楚了如何创建 store 实例,但这个实例是底层 API,使用文档介绍的 `create` 函数在 `react.ts` 文件定义,并调用了 `createStore` 创建框架无关数据流。之所 `create` 定义在 `react.ts`,是因为返回的 `useStore` 是一个 Hooks,所以本身具有 React 环境特性,因此得名。
|
||||
|
||||
该函数第一行就调用 `createStore` 创建基础 store,因为对框架来说是内部 API,所以命名也叫 api:
|
||||
|
||||
```typescript
|
||||
const api: CustomStoreApi = typeof createState === 'function' ? createStore(createState) : createState
|
||||
|
||||
const useStore: any = <StateSlice>(
|
||||
selector: StateSelector<TState, StateSlice> = api.getState as any,
|
||||
equalityFn: EqualityChecker<StateSlice> = Object.is
|
||||
) => /** ... */
|
||||
```
|
||||
|
||||
接下来所有代码都在创建 `useStore` 这个函数,我们看下其内部实现:
|
||||
|
||||
简单来说就是利用 `subscribe` 监听变化,并在需要的时候强制刷新当前组件,并传入最新的 `state` 给到 `useStore`。所以第一步当然是创建 `forceUpdate` 函数:
|
||||
|
||||
```typescript
|
||||
const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void]
|
||||
```
|
||||
|
||||
然后通过调用 API 拿到 `state` 并传给 selector,并调用 `equalityFn`(这个函数可以被定制)判断状态是否发生了变化:
|
||||
|
||||
```typescript
|
||||
const state = api.getState()
|
||||
newStateSlice = selector(state)
|
||||
hasNewStateSlice = !equalityFn(
|
||||
currentSliceRef.current as StateSlice,
|
||||
newStateSlice
|
||||
)
|
||||
```
|
||||
|
||||
如果状态变化了,就更新 `currentSliceRef.current`:
|
||||
|
||||
```typescript
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (hasNewStateSlice) {
|
||||
currentSliceRef.current = newStateSlice as StateSlice
|
||||
}
|
||||
stateRef.current = state
|
||||
selectorRef.current = selector
|
||||
equalityFnRef.current = equalityFn
|
||||
erroredRef.current = false
|
||||
})
|
||||
```
|
||||
|
||||
> `useIsomorphicLayoutEffect` 是同构框架常用 API 套路,在前端环境是 `useLayoutEffect`,在 node 环境是 `useEffect`:
|
||||
|
||||
说明一下 `currentSliceRef` 与 `newStateSlice` 的功能。我们看 `useStore` 最后的返回值:
|
||||
|
||||
```typescript
|
||||
const sliceToReturn = hasNewStateSlice
|
||||
? (newStateSlice as StateSlice)
|
||||
: currentSliceRef.current
|
||||
useDebugValue(sliceToReturn)
|
||||
return sliceToReturn
|
||||
```
|
||||
|
||||
发现逻辑是这样的:如果 state 变化了,则返回新的 state,否则返回旧的,这样可以保证 compare 函数判断相等时,返回对象的引用完全相同,这个是不可变数据的核心实现。另外我们也可以学习到阅读源码的技巧,即要经常跳读。
|
||||
|
||||
那么如何在 selector 变化时更新 store 呢?中间还有一段核心代码,调用了 `subscribe`,相信你已经猜到了,下面是核心代码片段:
|
||||
|
||||
```typescript
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const listener = () => {
|
||||
try {
|
||||
const nextState = api.getState()
|
||||
const nextStateSlice = selectorRef.current(nextState)
|
||||
if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) {
|
||||
stateRef.current = nextState
|
||||
currentSliceRef.current = nextStateSlice
|
||||
forceUpdate()
|
||||
}
|
||||
} catch (error) {
|
||||
erroredRef.current = true
|
||||
forceUpdate()
|
||||
}
|
||||
}
|
||||
const unsubscribe = api.subscribe(listener)
|
||||
if (api.getState() !== stateBeforeSubscriptionRef.current) {
|
||||
listener() // state has changed before subscription
|
||||
}
|
||||
return unsubscribe
|
||||
}, [])
|
||||
```
|
||||
|
||||
这段代码要先从 `api.subscribe(listener)` 看,这使得任何 `setState` 都会触发 `listener` 的执行,而 `listener` 利用 `api.getState()` 拿到最新 `state`,并拿到上一次的 compare 函数 `equalityFnRef` 执行一下判断值前后是否发生了改变,如果改变则更新 `currentSliceRef` 并进行一次强制刷新(调用 `forceUpdate`)。
|
||||
|
||||
### context 的实现
|
||||
|
||||
注意到 context 语法,可以创建多个互不干扰的 store 实例:
|
||||
|
||||
```tsx
|
||||
import create from 'zustand'
|
||||
import createContext from 'zustand/context'
|
||||
|
||||
const { Provider, useStore } = createContext()
|
||||
|
||||
const createStore = () => create(...)
|
||||
|
||||
const App = () => (
|
||||
<Provider createStore={createStore}>
|
||||
...
|
||||
</Provider>
|
||||
)
|
||||
```
|
||||
|
||||
首先我们知道 `create` 创建的 store 是实例间互不干扰的,问题是 `create` 返回的 `useStore` 只有一个实例,也没有 `<Provider>` 声明作用域,那么如何构造上面的 API 呢?
|
||||
|
||||
首先 `Provider` 存储了 `create` 返回的 `useStore`:
|
||||
|
||||
```tsx
|
||||
const storeRef = useRef<TUseBoundStore>()
|
||||
storeRef.current = createStore()
|
||||
```
|
||||
|
||||
那么 `useStore` 本身其实并不实现数据流功能,而是将 `<Provider>` 提供的 `storeRef` 拿到并返回:
|
||||
|
||||
```typescript
|
||||
const useStore: UseContextStore<TState> = <StateSlice>(
|
||||
selector?: StateSelector<TState, StateSlice>,
|
||||
equalityFn = Object.is
|
||||
) => {
|
||||
const useProviderStore = useContext(ZustandContext)
|
||||
return useProviderStore(
|
||||
selector as StateSelector<TState, StateSlice>,
|
||||
equalityFn
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
所以核心逻辑还是是现在 `create` 函数里,`context.ts` 只是利用 ReactContext 将 `useStore` “注入” 到组件,且利用 ReactContext 特性,这个注入可以存在多个实例,且不会相互影响。
|
||||
|
||||
### 中间件
|
||||
|
||||
中间件其实不需要怎么实现。比如看这个 redux 中间件的例子:
|
||||
|
||||
```typescript
|
||||
import { redux } from 'zustand/middleware'
|
||||
const useStore = create(redux(reducer, initialState))
|
||||
```
|
||||
|
||||
可以将 zustand 用法改变为 reducer,实际上是利用了函数式理念,redux 函数本身可以拿到 `set, get, api`,如果想保持 API 不变,则原样返回 callback 就行了,如果想改变用法,则返回特定的结构,就是这么简单。
|
||||
|
||||
为了加深理解,我们看看 redux 中间件源码:
|
||||
|
||||
```typescript
|
||||
export const redux = ( reducer, initial ) => ( set, get, api ) => {
|
||||
api.dispatch = action => {
|
||||
set(state => reducer(state, action), false, action)
|
||||
return action
|
||||
}
|
||||
api.dispatchFromDevtools = true
|
||||
return { dispatch: (...a) => api.dispatch(...a), ...initial }
|
||||
}
|
||||
```
|
||||
|
||||
将 `set, get, api` 封装为 redux API:`dispatch` 本质就是调用 `set`。
|
||||
|
||||
## 总结
|
||||
|
||||
[zustand](https://github.com/pmndrs/zustand) 是一个实现精巧的 React 数据流管理工具,自身框架无关的分层合理,中间件实现巧妙,值得学习。
|
||||
|
||||
> 讨论地址是:[精读《zustand 源码》· Issue #392 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/392)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,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` 个非负整数 `a1,a2,...,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))
|
||||
@@ -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))
|
||||
@@ -103,7 +103,7 @@ select c.$my_custom_symbol$ from ...
|
||||
|
||||
```sql
|
||||
select a |from b;
|
||||
# select a $my_custom_symbol$ b;
|
||||
# select a $my_custom_symbol$ from b;
|
||||
```
|
||||
|
||||
你会发现,“补全光标文字” 法,在关键字位置时,会把原本正确的语句变成错误的语句,根本解析不出语法树。
|
||||
|
||||
@@ -32,7 +32,7 @@ Factory Method(工厂方法)属于创建型模式,利用工厂方法创建
|
||||
|
||||
对卡牌对战的系统来说,**所有卡牌都应该实现同一种接口**,所以卡牌对战系统拿到的卡牌应该就是简单的 Card 类型,这种类型具备基本的卡片操作交互能力,系统就调用这些能力完成基本流程就好了,如果系统直接实例化具体的卡片,那不同的卡片类型会导致系统难以维护,卡片间操作也无法抽象化。
|
||||
|
||||
正式这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。
|
||||
正是这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。
|
||||
|
||||
对图形拖拽系统来说,用到了 “连接平行的类层次” 这个特性,所谓连接平行的类层次,就是指一个图形,与其对应的操作类是一个平行抽象类,而一个具体的图形与具体的操作类则是另一个平行关系,系统只要关注最抽象的 “通用图形类” 与 “通用操作类” 即可,操作时,底层可能是某个具体的 “圆类” 与 “圆操作类” 结合使用,具体的类有不同的实现,但都符合同一种接口,因此操作系统才可以把它们一视同仁,统一操作。
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Prototype(原型模式)属于创建型模式,既不是工厂也不是直
|
||||
|
||||
### 模版组件
|
||||
|
||||
通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意为止,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?
|
||||
通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意位置,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?
|
||||
|
||||
## 意图解释
|
||||
|
||||
|
||||
@@ -42,13 +42,25 @@ State(状态模式)属于行为型模式。
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
abstract class Context {
|
||||
abstract setState(state: State): void;
|
||||
}
|
||||
|
||||
// 定义状态接口
|
||||
interface State {
|
||||
// 模拟台灯点亮
|
||||
show: () => string
|
||||
}
|
||||
|
||||
class Light1 implements State {
|
||||
interface Light {
|
||||
click: () => void
|
||||
}
|
||||
|
||||
type LightState = State & Light
|
||||
|
||||
class TurnOff implements State, Light {
|
||||
context: Context;
|
||||
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
@@ -59,11 +71,13 @@ class Light1 implements State {
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light2(this.context))
|
||||
this.context.setState(new WeakLight(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light2 implements State {
|
||||
class WeakLight implements State, Light {
|
||||
context: Context;
|
||||
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
@@ -74,11 +88,13 @@ class Light2 implements State {
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light3(this.context))
|
||||
this.context.setState(new StandardLight(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light3 implements State {
|
||||
class StandardLight implements State, Light {
|
||||
context: Context;
|
||||
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
@@ -89,11 +105,13 @@ class Light3 implements State {
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light4(this.context))
|
||||
this.context.setState(new StrongLight(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light4 implements State {
|
||||
class StrongLight implements State, Light {
|
||||
context: Context;
|
||||
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
@@ -104,30 +122,37 @@ class Light4 implements State {
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light1(this.context))
|
||||
this.context.setState(new TurnOff(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
// 台灯
|
||||
public class Lamp {
|
||||
class Lamp extends Context {
|
||||
// 当前状态
|
||||
private currentState = new Light1(this)
|
||||
|
||||
protected setState(state: State) {
|
||||
this.currentState = state
|
||||
#currentState: LightState = new TurnOff(this)
|
||||
setState(state: LightState) {
|
||||
this.#currentState = state
|
||||
}
|
||||
getState() {
|
||||
return this.#currentState
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
click() {
|
||||
this.getState().click()
|
||||
}
|
||||
}
|
||||
|
||||
const lamp = new Lamp() // 关闭
|
||||
console.log(lamp.getState().show()) // 关灯
|
||||
lamp.click() // 弱光
|
||||
console.log(lamp.getState().show()) // 弱光
|
||||
lamp.click() // 亮
|
||||
console.log(lamp.getState().show()) // 亮
|
||||
lamp.click() // 强光
|
||||
console.log(lamp.getState().show()) // 强光
|
||||
lamp.click() // 关闭
|
||||
console.log(lamp.getState().show()) // 关闭
|
||||
```
|
||||
|
||||
其实有很多种方式来实现,不必拘泥于形式,大体上只要保证由多个类实现不同状态,每个类实现到下一个状态切换就好了。
|
||||
|
||||
Reference in New Issue
Block a user