Compare commits

..
15 Commits
Author SHA1 Message Date
ascoders e0d8be6916 196 2021-05-24 10:12:55 +08:00
ascoders 7c4d37a428 fix typo 2021-05-17 13:21:06 +08:00
ascoders eeaebad903 195 2021-05-17 09:43:00 +08:00
黄子毅 c624e7e607 Merge pull request #315 from kamilic/patch-1
Update 194.精读《算法基础数据结构》.md
2021-05-12 13:53:55 +08:00
kamilic 0f724a20cf Update 194.精读《算法基础数据结构》.md
chores: fix typo
2021-05-11 21:08:13 +08:00
黄子毅 c7922c4a0e Merge pull request #314 from jihchi/patch-1
Update 194.精读《算法基础数据结构》.md -- 連結正確但文字錯誤
2021-05-10 14:23:45 +08:00
Jihchi Lee 9d98f72fd3 Update 194.精读《算法基础数据结构》.md 2021-05-10 11:25:53 +08:00
ascoders db9a256efa update readme 2021-05-10 08:59:01 +08:00
ascoders aedfdd4b91 Merge branch 'master' of https://github.com/dt-fe/weekly 2021-05-10 08:57:02 +08:00
ascoders 99d5c51d42 194 2021-05-10 08:56:52 +08:00
黄子毅 086b733288 Merge pull request #313 from jihchi/patch-1
96.精读《useEffect%20完全指南》-- 修正錯誤依賴
2021-05-08 10:46:03 +08:00
Jihchi Lee 6f9ccaef9e Update 96.精读《useEffect 完全指南》.md 2021-05-07 12:01:13 +08:00
ascoders dba92f52b5 update 2021-04-25 10:10:15 +08:00
ascoders 61512fba55 193 2021-04-25 10:06:54 +08:00
ascoders 542faafe04 fix: 修复算法错误 2021-04-22 17:18:24 +08:00
9 changed files with 842 additions and 14 deletions
+6 -2
View File
@@ -6,7 +6,7 @@
前端界的好文精读,每周更新!
最新精读:<a href="./前沿技术/192.精读《DOM diff 最长上升子序列》.md">192.精读《DOM diff 最长上升子序列》</a>
最新精读:<a href="./前沿技术/196.精读《前端职业规划 - 2021 年》.md">196.精读《前端职业规划 - 2021 年》</a>
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
@@ -155,6 +155,10 @@
- <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>
### 设计模式
@@ -205,7 +209,7 @@
- <a href="./源码解读/110.精读《Inject Instance 源码》.md">110.精读《Inject Instance 源码》</a>
- <a href="./源码解读/122.精读《robot 源码 - 有限状态机》.md">122.精读《robot 源码 - 有限状态机》</a>
- <a href="./源码解读/128.精读《Hooks 取数 - swr 源码》.md">128.精读《Hooks 取数 - swr 源码》</a>
- <a href="./源码解读/130.精读《unstated 与 unstated-next 源码》 copy.md">130.精读《unstated 与 unstated-next 源码》 copy</a>
- <a href="./源码解读/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>
@@ -126,23 +126,19 @@
最后我们看看,如何在找到答案的同时,还能找到正确的序列呢?
其实读到这里,不用说你应该也能猜出来,前面已经说过了,**只要替换了最后一个或者插入的时候,栈顺序就是正确的**。所以我们可以在替换最后一个或者插入的时候,存储下当前栈的拷贝,这样最后留下来的拷贝就是最终正确的顺序。
### 找出正确的序列
那为什么是这样呢?我们最后用一个例子强化一下理解,因为已经很熟练了,因此前几步合并了一下
找出正确的序列并不容易,让我们看下面这个情况
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01Mi7fPY1FLlDhiuGSC_!!6000000000471-2-tps-1200-344.png">
<img width=450 src="https://img.alicdn.com/imgextra/i2/O1CN01DXI8Cm1Uh1qjYGtEM_!!6000000002548-2-tps-1208-486.png">
到目前为止,`7, 8, 9, 13` 是不存在的,但实际上它指代的是 `10, 11, 12, 13`,这个前面已经解释过,就不再赘述。我们此时已经存了队列 `10, 11, 12, 13`,因此此时结束的话,这个队列输出是正确的。我们看下一步
贪心算法结束后,总长度是对的,但很明显顺序还是错的。为了方便计算,我们存储时转化为下标
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01ZtAMAR1V30rKrchB2_!!6000000002596-2-tps-1204-444.png">
<img width=450 src="https://img.alicdn.com/imgextra/i3/O1CN01tflKTQ1soP0NpnIiu_!!6000000005813-2-tps-1216-716.png">
为了方便识别,我给不同分组数字加了背景色,这样更容易观察:我们发现,由于每次替换的都是比它稍大的数字,一旦遇到了一个更小的开始 `1, 2, 3, 4, 5`,即便上一轮 `7, 8, 9` 还没有完全替换完 `10, 11, 12, 13`,更小的也一定从最左边开始替换,因为栈内数字是单调递增的。那么全部替换完,或者从某个数字开始,向右替换完,此时队列中的数字一定都是相对顺序正确的。从这里例子来看,`2, 3` 一定会优先替换掉 `8, 9`,等 `13` 被替换的时候,栈的相对顺序一定符合原数组的相对顺序。
并且使用二维数组存储,这样被替换的数字可以被保留下来。当计算完毕后,我们从最后一位开始向前查找,**一旦发现一个值不是单调递减的,就向数组上方继续查找,直到首节点。**
最后看一个更复杂的例子加深印象:
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01GXWX6G1jaoiMJWC9h_!!6000000004565-2-tps-1102-768.png">
读到这里,恭喜你已经大功告成,完全理解这个 DOM diff 算法啦。
因此上面的例子,最终顺序下标是 `[0, 1, 2, 3, 4, 5, 9]`,对应数字为 `[10, 20, 30, 40, 50, 60, 61]`,**而且这个数字是潜力最大的最长子序列。**
## 总结
@@ -0,0 +1,324 @@
截止目前,React Server Component 还在开发与研究中,因此不适合投入生产环境使用。但其概念非常有趣,值得技术人学习。
目前除了国内各种博客、知乎解读外,最一手的学习资料有下面两处:
1. [Dan 的 Server Component 介绍视频](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
2. [Server Component RFC 草案](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md)
我会结合这些一手资料,与一些业界大牛的解读,系统的讲清楚 React Server Component 的概念,以及我对它的一些理解。
首先我们来看,为什么需要提出 Server Component 这个概念:
Server Component 概念的提出,是为了解决 "用户体验、可维护性、性能" 这个不可能三角,所谓不可能三角就是,最多同时满足两条,而无法三条都同时满足。
简单解释一下,用户体验体现在页面更快的响应、可维护性体现在代码应该高内聚低耦合、性能体现在请求速度。
- 保障 **用户体验、可维护性**,用一个请求拉取全部数据,所有组件一次性渲染。但当模块不断增多,无用模块信息不敢随意删除,请求会越来越大,越来越冗余,导致瓶颈卡在取数这块,也就是 **性能不好**
- 保障 **用户体验、性能**,考虑并行取数,之后流程不变,那么以后业务逻辑新增或减少一个模块,我们就要同时修改并行取数公共逻辑与对应业务模块,**可维护性不好**。
- 保障 **可维护性、性能**,可以每个模块独立取数,但在父级渲染完才渲染子元素的情况下,父子取数就变成了串行,页面加载被阻塞,**用户体验不好**。
一言蔽之,在前后端解耦的模式下,唯一连接的桥梁就是取数请求。要把用户体验做好,取数就要提前并行发起,而前端模块是独立维护的,所以在前端做取数聚合这件事,必然会破坏前端可维护性,而这并行这件事放在后端的话,会因为后端不能解析前端模块,导致给出的聚合信息滞后,久而久之变得冗余。
要解决这个问题,就必须加深前端与后端的联系,所以像 GraphQL 这种前后端约定方案是可行的,但因为其部署成本高,收益又仅在前端,所以难以在后端推广。
Server Component 是另一种方案,通过启动一个 Node 服务辅助前端,但做的不是 API 对接,而是运行前端同构 js 代码,直接解析前端渲染模块,从中自动提取请求并在 Node 端直接与服务器通信,因为服务端间通信成本极低、前端代码又不需要做调整,请求数据也是动态按需聚合的,因此同时解决了 "用户体验、可维护性、性能" 这三个问题。
其核心改进点如下图所示:
<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN01NttXOI21kaFJgNDx1_!!6000000007023-2-tps-720-466.png">
如上图所示,这是前后端正常交互模式,可以看到,`Root``Child` 串行发了两个请求,因为网络耗时与串行都是严重阻塞部分,因此用红线标记。
Server Component 可以理解为下图,不仅减少了一次网络损耗,请求也变成了并行,请求返回结果也从纯数据变成了一个同时描述 UI DSL 与数据的特殊结构:
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN01MDYxZ71K0IkACLmFJ_!!6000000001101-2-tps-1142-468.png">
到此,恭喜你已经理解了 Server Component 核心概念,如果你只想泛泛了解一下,读到这里就可以结束了。如果你还想深入了解其实现细节,请继续阅读。
## 概述
概括的说,Server Component 就是让组件拥有在服务端渲染的能力,从而解决不可能三角问题。也正因为这个特性,使得 Server Component 拥有几种让人眼前一亮的特性,都是纯客户端组件所不具备的:
- **运行在服务端的组件只会返回 DSL 信息,而不包含其他任何依赖**,因此 Server Component 的所有依赖 npm 包都不会被打包到客户端。
- **可以访问服务端任何 API**,也就是让组件拥有了 Nodejs 能拥有的能力,你理论上可以在前端组件里干任何服务端才能干的事情。
- **Server Component 与 Client Component 无缝集成**,可以通过 Server Component 无缝调用 Client Component。
- **Server Component 会按需返回信息**,在当前逻辑下,走不到的分支逻辑的所有引用都不会被客户端引入。比如 Server Component 虽然引用了一个巨大的 npm 包,但某个分支下没有用到这个包提供的函数,那客户端也不会下载这个巨大的 npm 包到本地。
- **由于返回的不是 HTML,而是一个 DSL,所以服务端组件即便重新拉取,已经产生的 State 也会被维持住**。比如说 A 是 ServerComponent,其子元素 B 是 Client Component,此时对 B 组件做了状态修改比如输入一些文字,此时触发 A 重新拉取 DSL 后,B 已经输入的文字还会保留。
- **可以无缝与 Suspense 结合**,并不会因为网络原因导致连 Suspense 的 loading 都不能及时展示。
- **共享组件可以同时在服务端与客户端运行**。
### 三种组件
Server Component 将组件分为三种:Server Component、Client Component、Shared Component,分别以 `.server.js``.client.js``.js` 后缀结尾。
其中 `.client.js` 与普通组件一样,但 `.server.js``.js` 都可能在服务端运行,其中:
- `.server.js` 必然在服务端执行。
- `.js` 在哪执行要看谁调用它,如果是 `.server.js` 调用则在服务端执行,如果是 `.client.js` 调用则在客户端执行,因此其本质还要接收服务端组件的约束。
下面是 RFC 中展示的 Server Component 例子:
```typescript
// Note.server.js - Server Component
import db from 'db.server';
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
// (B) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
```
可以看到,**这就是 Node 与 React 混合语法**。服务端组件有着苛刻的限制条件:**不能有状态,且 `props` 必须能被序列化**。
很容易理解,因为服务端组件要被传输到客户端,就必须经过序列化、反序列化的过程,JSX 是可以被序列化的,props 也必须遵循这个规则。另外服务端不能帮客户端存储状态,因此服务端组件不能用任何 `useState` 等状态相关 API。
但这两个问题都可以绕过去,即将状态转化为组件的 `props` 入参,由 `.client.js` 存储,见下图:
<img width=250 src="https://img.alicdn.com/imgextra/i4/O1CN01ChPZdO1ky0Nsu2ygV_!!6000000004751-2-tps-514-278.png">
或者利用 Server Component 与 Client Component 无缝集成的能力,将状态与无法序列化的 `props` 参数都放在 Client Component,由 Server Component 调用。
### 优点
#### 零客户端体积
这句话听起来有点夸张,但其实在 Server Component 限定条件下还真的是。看下面代码:
```typescript
// NoteWithMarkdown.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
```
`marked``sanitize-html` 都不会被下载到本地,所以如果只有这一个文件传输,客户端的理论增加体积就是 `render` 函数序列化后字符串大小,可能不到 1KB。
当然这背后也是限制换来的,首先这个组件没有状态,无法在客户端实时执行,而且在服务端运行也可能消耗额外计算资源,如果某些 npm 包计算复杂度较高的话。
这个好处可以理解为,`marked` 这个包仅在服务端读取到内存一次,以后只要后客户端想用,只需要在服务端执行 `marked` API 并把输出结果返回给客户端,而不需要客户端下载 `marked` 这个包了。
#### 拥有完整服务端能力
由于 Server Component 在服务端执行,因此可以执行 Nodejs 的任何代码。
```typescript
// Note.server.js - Server Component
import fs from 'react-fs';
function Note({id}) {
const note = JSON.parse(fs.readFile(`${id}.json`));
return <NoteWithMarkdown note={note} />;
}
```
我们可以把对请求的理解拔高一个层次,即 `request` 只是客户端发起的一个 Http 请求,其本质是访问一个资源,在服务端就是个 IO 行为。对于 IO,我们还可以通过 `file` 文件系统写入删除资源、`db` 通过 sql 语法直接访问数据库,或者 `request` 直接在服务器本地发出请求。
#### 运行时 Code Split
我们都知道 webpack 可以通过静态分析,将没有使用到的 import 移出打包,而 Server Component 可以在运行时动态分析,将当前分支逻辑下没有用到的 import 移出打包:
```typescript
// PhotoRenderer.js
import React from 'react';
// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (props.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
```
这是因为 Server Component 构建时会进行预打包,运行时就是一个动态的包分发器,完全可以通过当前运行状态比如 `props.xxx` 来区分当前运行到哪些分支逻辑,而没有运行到哪些分支逻辑,并且仅告诉客户端拉取当前运行到的分支逻辑的缺失包。
纯前端模式与之类似的写法是:
```typescript
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
```
只是这种写法不够原生,且实际场景往往只有前端框架把路由自动包一层 Lazy Load,而普通代码里很少出现这种写法。
#### 无客户端往返的数据端取数
一般考虑到取数网络消耗,我们往往会将其处理成异步,然后在数据返回前展示 Loading:
```typescript
// Note.js
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
// NOTE: loads *after* rendering, triggering waterfalls in children
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return (/* render note here... */);
}
}
```
这是因为单页模式下,我们可以快速从 CDN 拿到这个 DOM 结构,但如果再等待取数,整体渲染就变慢了。而 Server Component 因为本身就在服务端执行,因此可以将拿 DOM 结构与取数同时进行:
```typescript
// Note.server.js - Server Component
function Note(props) {
// NOTE: loads *during* render, w low-latency data access on the server
const note = db.notes.get(props.id);
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
```
当然这个前提是网络消耗敏感的情况,如果本身就是一个慢 SQL 查询,耗时几秒的情况下,这样做反而适得其反。
#### 减少 Component 层次
看下面的例子:
```js
// Note.server.js
// ...imports...
function Note({id}) {
const note = db.notes.get(id);
return <NoteWithMarkdown note={note} />;
}
// NoteWithMarkdown.server.js
// ...imports...
function NoteWithMarkdown({note}) {
const html = sanitizeHtml(marked(note.text));
return <div ... />;
}
// client sees:
<div>
<!-- markdown output here -->
</div>
```
虽然在组件层面抽象了 `Note``NoteWithMarkdown` 两个组件,但由于真正 DOM 内容实体只有一个简单的 `div`,所以在 Server Component 模式下,返回内容就会简化为这个 `div`,而无需包含那两个抽象的组件。
### 限制
Server Component 模式下有三种组件,分别是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,如下:
**Server Component**
- ❌ 不能用 `useState``useReducer` 等状态存储 API。
- ❌ 不能用 `useEffect` 等生命周期 API。
- ❌ 不能用 `window` 等仅浏览器支持的 API。
- ❌ 不能用包含了上面情况的自定义 Hooks。
- ✅ 可无缝访问服务端数据、API。
- ✅ 可渲染其他 Server/Client Component
**Client Component**
- ❌ 不能引用 Server Component。
- ✅ 但可以在 Server Component 中出现 Client Component 调用 Server Component 的情况,比如 `<ClientTabBar><ServerTabContent /></ClientTabBar>`
- ❌ 不能调用服务端 API 获取数据。
- ✅ 可以用一切 React 与浏览器完整能力。
**Shared Component**
- ❌ 不能用 `useState``useReducer` 等状态存储 API。
- ❌ 不能用 `useEffect` 等生命周期 API。
- ❌ 不能用 `window` 等仅浏览器支持的 API。
- ❌ 不能用包含了上面情况的自定义 Hooks。
- ❌ 不能引用 Server Component。
- ❌ 不能调用服务端 API 获取数据。
- ✅ 可以同时在服务器与客户端使用。
其实不难理解,因为 Shared Component 同时在服务器与客户端使用,因此兼具它们的劣势,带来的好处就是更强的复用性。
## 精读
要快速理解 Server Component,我觉得最好也是最快的方式,就是找到其与十年前 PHP + HTML 的区别。看下面代码:
```php
$link = mysqli_connect('localhost', 'root', 'root');
mysql_select_db('test', $link);
$result = mysql_query('select * from table');
while($row=mysql_fetch_assoc($result)){
echo "<span>".$row["id"]."</span>";
}
```
其实 PHP 早就是一套 "Server Component" 方案了,在服务端直接访问 DB、并返回给客户端 DOM 片段。
React Server Component 在折腾了这么久后,可以发现,最大的区别是将返回的 HTML 片段改为了 DSL 结构,这其实是浏览器端有一个强大的 React 框架在背后撑腰的结果。而这个带来的好处除了可以让我们在服务端能继续写 React 语法,而不用退化到 "PHP 语法" 以外,更重要的是组件状态得以维持。
另一个重要不同是,PHP 无法解析现在前端生态下任何 npm 包,所以无从解析模块化的前端代码,所以虽然直觉上感觉 PHP 效率与 Server Component 并无区别,但背后的成本是得写另一套不依赖任何 npm 包、JSX 的语法来返回 HTML 片段,Server Component 大部分特性都无法享受到,而且代码也无法复用。
所以,本质上还是 HTML 太简单了,无法适应如今前端的复杂度,而普通后端框架虽然后端能力强大,但在前端能力上还停留在 20 年前(直接返回 DOM),唯有 Node 中间层方案作为桥梁,才能较好的衔接现代后端代码与现代前端代码。
### PHP VS Server Component
其实在 PHP 时代,前后端都可以做模块化。后端模块化显而易见,因为可以将后端代码模块化的开发,最后打包至服务器运行。前端也可以在服务端模块化开发,只要我们将前后端代码剥离出来即可,下图青色是后端部分,红色是前端部分:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01jsKjLq1iWPHi9C4pQ_!!6000000004420-2-tps-894-642.png">
但这有个问题,因为后端服务对浏览器来说是无状态的,所以后端模块化本身就符合其功能特征,但前端页面显示在用户浏览器,每次都通过路由跳转到新页面,显然不能最大程度发挥客户端持续运行的优势,我们希望在保持前端模块化的基础上,在浏览器端有一个持续运行的框架优化用户体验,因此 Server Component 其实做了下面的事情:
<img width=550 src="https://img.alicdn.com/imgextra/i3/O1CN01gzaZNY1lBkGbGJKUy_!!6000000004781-2-tps-1332-760.png">
这样做有两大好处:
1. 兼顾了 PHP 模式下优势,即前后端代码无缝混合,带来一系列体验和能力增强。
2. 前后端还是各自模块化编写,图中红色部分是随前端项目整体打包的,因此开发还是保留了模块化特点,且在浏览器上还保持了 React 现代框架运行,无论是单页还是数据驱动等特性都能继续使用。
## 总结
Server Component 还没有成熟,但其理念还是很靠谱的。
想要同时实现 "用户体验、可维护性、性能",重后端,或者重前端的方案都不可行,只有在前后端取得一种平衡才能达到。Server Component 表达了一种职业发展理念,即未来前后端还是会走向全栈,这种全栈是前后端同时做深,从而让程序开发达到纯前端或纯后端无法达到的高度。
2021 年国内开发环境依然比较落后,所谓全栈,往往指的是 “前后端都懂一点”,各端都做不深,难以孵化出 Server Component 这种概念。当然,这也是我们继续向世界学习的动力。
也许 PHP 与 Server Component 的区别,就是检验一个人是真全栈还是伪全栈的试金石,快去问问你的同事吧!
> 讨论地址是:[精读《React Server Component》· Issue #311 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/311)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,150 @@
掌握了不同数据结构的特点,可以让你在面对不同问题时,采用合适的数据结构处理,达到事半功倍的效果。
所以这次我们详细介绍各类数据结构的特点,希望你可以融会贯通。
## 精读
### 数组
<img width=200 src="https://img.alicdn.com/imgextra/i2/O1CN01noho9m1Vltg5ISaq2_!!6000000002694-2-tps-418-110.png">
数组非常常用,它是一块连续的内存空间,因此可以根据下标直接访问,其查找效率为 O(1)。
但数组的插入、删除效率较低,只有 O(n),原因是为了保持数组的连续性,必须在插入或删除后对数组进行一些操作:比如插入第 K 个元素,需要将后面元素后移;而删除第 K 个元素,需要将后面元素前移。
### 链表
<img width=280 src="https://img.alicdn.com/imgextra/i3/O1CN010JfUOo1b0A5muE4sE_!!6000000003402-2-tps-584-112.png">
链表是为了解决数组问题而发明出来的,它提升了插入、删除效率,而牺牲了查找效率。
链表的插入、删除效率是 O(1),因为只要将对应位置元素断链、重连就可以完成插入、删除,而无需关心其他节点。
相应的查找效率就低了,因为存储空间不是连续的,所以无法像数组一样通过下标直接查找,而需要通过指针不断搜索,所以查找效率为 O(n)。
顺带一提,链表可以通过增加 `.prev` 属性改造为双向链表,也可以通过定义两个 `.next` 形成二叉树(`.left` `.right`)或者多叉树(N 个 `.next`)。
<img width=160 src="https://img.alicdn.com/imgextra/i2/O1CN01IqNVQI1m5ABrZXV4i_!!6000000004902-2-tps-318-316.png">
### 栈
<img width=240 src="https://img.alicdn.com/imgextra/i3/O1CN01xSS8e21xbe3LP1khU_!!6000000006462-2-tps-466-122.png">
栈是一种先入后出的结构,可以用数组模拟。
```typescript
const stack: number[] = []
// 入栈
stack.push(1)
// 出栈
stack.pop()
```
### 堆
<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN019O42yy1qE6NlA8w6V_!!6000000005463-2-tps-1154-952.png">
堆是一种特殊的完全二叉树,分为大顶堆与小顶堆。
大顶堆指二叉树根节点是最大的数,小顶堆指二叉树根节点是最小的数。为了方便说明,以下以大顶堆举例,小顶堆的逻辑与之相反即可。
大顶堆中,任意节点都比其叶子结点大,所以根节点是最大的节点。这种数据结构的优势是可以以 O(1) 效率找到最大值(小顶堆找最小值),因为直接取 `stack[0]` 就是根节点。
这里稍微提一下二叉树与数组结构的映射,因为采用数组方式操作二叉数,无论操作还是空间都有优势:第一项存储的是节点总数,对于下标为 K 的节点,其父节点下标是 `floor(K / 2)`,其子节点下标分别是 `K * 2``K * 2 + 1`,所以可以快速定位父子位置。
而利用这个特性,可以将插入、删除的效率达到 `O(logn)`,因为可以通过上下移动的方式调整其他节点顺序,而对于一个拥有 n 个节点的完全二叉树,树的深度为 `logn`
### 哈希表
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01u3J1JF1Sl25HB6Q0I_!!6000000002286-2-tps-740-598.png">
哈希表就是所谓的 Map,不同 Map 实现方式不同,常见的有 HashMap、TreeMap、HashSet、TreeSet。
其中 Map 和 Set 实现类似,所以以 Map 为例讲解。
首先将要存储的字符求出其 ASCII 码值,再根据比如余数等方法,定位到一个数组的下标,同一个下标可能对应多个值,因此这个下标可能对应一个链表,根据链表进一步查找,这种方法称为拉链法。
如果存储的值超过一定数量,链表的查询效率就会降低,可能会升级为红黑树存储,总之这样的增、删、查效率为 `O(1)`,但缺点是其内容是无序的。
为了保证内容有序,可以使用树状结构存储,这种数据结构称为 HashTree,这样时间复杂度退化为 `O(logn)`,但好处是内容可以是有序的。
### 树 & 二叉搜索树
<img width=380 src="https://img.alicdn.com/imgextra/i4/O1CN01vOCoG91w82pzSITaQ_!!6000000006262-2-tps-800-504.png">
二叉搜索树是一种特殊二叉树,更复杂的还有红黑树,但这里就不深入了,只介绍二叉搜索树。
二叉搜索树满足对于任意节点,`left 的所有节点 < 根节点 < right 的所有节点`,注意这里是所有节点,因此在判断时需要递归考虑所有情况。
二叉搜索树的好处在于,访问、查找、插入、删除的时间复杂度均为 O(logn),因为无论何种操作都可以通过二分方式进行。但在最坏的情况会降级为 O(n),原因是多次操作后,二叉搜索树可能不再平衡,最后退化为一个链表,就变成了链表的时间复杂度。
更好的方案有 AVL 树、红黑树等,像 JAVA、C++ 标准库实现的二叉搜索树都是红黑树。
### 字典树
<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN01TqDeaL1ll0lTX75y3_!!6000000004858-2-tps-872-510.png">
字典树多用于单词搜索场景,只要给定一个单独开头,就可以快速查找到后面有几种推荐词。
比如上面的例子,输入 "o",就可以快速查找到后面有 "ok" 与 "ol" 两个单词。要注意的是,每个节点都要有一个属性 `isEndOfWord` 表示到当前为止是否为一个完整的单词:比如 `go``good` 两个都是完整的单词,但 `goo` 不是,因此第二个 `o` 与第四个 `d` 都有 `isEndOfWord` 标记,表示读到这里就查到一个完整的单词了,叶子结点的标记也可以省略。
### 并查集
<img width=300 src="https://img.alicdn.com/imgextra/i4/O1CN01B5xA5r21rSBj442z3_!!6000000007038-2-tps-622-172.png">
并查集用来解决团伙问题,或者岛屿问题,即判断多个元素之间是属于某个集合。并查集的英文是 Union and Find,即归并与查找,因此并查集数据结构可以写成一个类,提供两个最基础的方法 `union``find`
其中 `union` 可以将任意两个元素放在一个集合,而 `find` 可以查找任意元素属于哪个根集合。
并查集使用数组的数据结构,只是有以下特殊含义,设下标为 k:
- `nums[k]` 表示其所属的集合,如果 `nums[k] === k` 表示它是这个集合的根节点。
如果要数一共有几个集合,只要数有多少满足 `nums[k] === k` 条件的数目即可,就像数有几个团伙,只要数有几个老大即可。
并查集的实现不同,数据也会有微妙的不同,高效的并查集在插入时,会递归将元素的值尽量指向根老大,这样查找判断时计算的快一些,但即便指向的不是根老大,也可以通过递归的方式找到根老大。
### 布隆过滤器
<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01CWabYX26RPkR0T3zs_!!6000000007658-2-tps-650-334.png">
Bloom Filter 只是一个过滤器,可以用远远超过其他算法的速度把未命中的数据排除掉,但未排除的也可能实际不存在,所以需要进一步查询。
布隆过滤器是如何做到这一点的呢?就是通过二进制判断。
如上图所示,我们先存储了 a、b 两个数据,将其转化为二进制,将对应位置改为 1,那么当我们再查询 a 或 b 时,因为映射关系相同,所以查到的结果肯定存在。
但查询 c 时,发现有一项是 0,说明 c 一定不存在;但查询 d 时,恰好两个都查到是 1,但实际 d 是不存在的,这就是其产生误差的原因。
布隆过滤器在比特币与分布式系统中使用广泛,比如比特币查询交易是否在某个节点上,就先利用布隆过滤器挡一下,以快速跳过不必要的搜索,而分布式系统计算比如 Map Reduce,也通过布隆过滤器快速过滤掉不在某个节点的计算。
## 总结
最后给出各数据结构 “访问、查询、插入、删除” 的平均、最差时间复杂度图:
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01LV4sSl20vkHWdZ7nr_!!6000000006912-2-tps-2398-1272.png">
这个图来自 [bigocheatsheet](https://www.bigocheatsheet.com/#graphs),你也可以点开链接直接访问。
学习了这些基础数据结构之后,希望你可以融会贯通,善于组合这些数据结构解决实际的问题,同时还要意识到没有任何一个数据结构是万能的,否则就不会有这么多数据结构需要学习了,只用一个万能的数据结构就行了。
对于数据结构的组合,我举两个例子:
第一个例子是如何以 O(1) 平均时间复杂度查询一个栈的最大或最小值。此时一个栈是不够的,需要另一个栈 B 辅助,遇到更大或更小值的时候才入栈 B,这样栈 B 的第一个数就是当前栈内最大或最小的值,查询效率是 O(1),而且只有在出栈时才需要更新,所以平均时间复杂度整体是 O(1)。
第二个例子是如何提升链表查找效率,可以通过哈希表与链表结合的思路,通过空间换时间的方式,用哈希表快速定位任意值在链表中的位置,就可以通过空间翻倍的牺牲换来插入、删除、查询时间复杂度均为 O(1)。虽然哈希表就能达到这个时间复杂度,但哈希表是无序的;虽然 HashTree 是有序的,但时间复杂度是 O(logn),所以只有通过组合 HashMap 与链表才能达到有序且时间复杂度更优,但牺牲了空间复杂度。
包括最后说的布隆过滤器也不是单独使用的,它只是一个防火墙,用极高的效率阻挡一些非法数据,但没有阻挡住的不一定就是合法的,需要进一步查询。
所以希望你能了解到各个数据结构的特征、局限以及组合的用法,相信你可以在实际场景中灵活使用不同的数据结构,以实现当前业务场景的最优解。
> 讨论地址是:[精读《算法基础数据结构》· Issue #312 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/312)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,121 @@
本周精读的文章是 [Comparing the New Generation of Build Tools](https://css-tricks.com/comparing-the-new-generation-of-build-tools/)。
前端工程领域近期出了不少新工具,这些新工具都运用了一些新技术或者跨领域技术,实现了一些突破,因此有必要了解一下这些工具都有什么特性,以及是否可以投入生产环境。
由于原文比较啰嗦,所以具体用法和支持细节不在这里展开,如果想进一步了解细节,可以直接阅读 [原文]((https://css-tricks.com/comparing-the-new-generation-of-build-tools/))。
## 精读
按照从底层到上层的封装粒度,以 esbuild、snowpack、vite、wmr 的顺序介绍。
### esbuild
esbuild 使用 go 语言编写,由于相对 node 更为底层,且不提供 AST 操作能力,所以代码执行效率更高,根据其官方 benchmark 介绍提速有 10100 倍:
<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01hzHuDP1JXuBvRgX7x_!!6000000001039-2-tps-800-170.png">
esbuild 有两大功能,分别是 bundler 与 minifier,其中 bundler 用于代码编译,类似 babel-loader、ts-loaderminifier 用于代码压缩,类似 terser。
使用 esbuild 编译代码方法如下:
```typescript
esbuild.build({
entryPoints: ["src/app.jsx"],
outdir: "dist",
define: { "process.env.NODE_ENV": '"production"' },
watch: true,
});
```
但由于 esbuild 无法操作 AST,所以一些需要操作 AST 的 babel 插件无法与之兼容,导致生产环境很少直接使用 esbuild 的 bundler 模块。
幸运的是 minifier 模块可以直接替换 terser 使用,可以用于生产环境:
```typescript
esbuild.transform(code, {
minify: true,
});
```
由于 esbuild 牺牲了一些包大小换取了更高的执行效率,因此压缩后包体积会稍微大一些,不过也就是 177KB 与 165KB 的区别,几乎可以忽略。
esbuild 比较底层,所以可以与后续介绍的上层构建工具结合使用,当然根据工具设计理念,是否内置,内置到什么程度,以及是否允许通过插件替换就是另一回事了。
### snowpack
snowpack 是一个相对轻量的 bundless 方案,之前也写过一篇 [精读 snowpack](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md),其实 bundless 就是利用浏览器支持的 [ESM import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 特性,利用浏览器进行模块间依赖加载,而不需要在编译时进行。
跳过编译时依赖加载可以省很多事,比如不用考虑 tree shaking 问题,也不用为了最终产物加速而使用缓存,相当于这些工作交给最终执行的浏览器了,而浏览器作为最终运行时容器,比编译时工具更了解应该如何按需加载。
仅从编译时来看,修改单个文件的编译速度与项目整体大小有关,而若不考虑整体项目,仅编译单个文件(最多递归一下有限的依赖模块,解决比如 TS 类型变量判断问题)时间复杂度一定是 O(1) 的。
实际上我们很少单独使用 snowpack,因为其编译使用的 esbuild 还未达到 1.0 稳定版本,在生态兼容与产物稳定性上存在风险,所以编译打包时往往采用 rollup 或 webpack,但这种割裂也导致了开发与生产环境不一致,这往往代表着更大的风险,因此在 vite 框架可以看到这块的取舍。
snowpack 是开箱即用的:
```json
// package.json
"scripts": {
"start": "snowpack dev",
"build": "snowpack build"
},
```
我们还可以增加 `snowpack.config.js` 配置文件开启 `remote` 模式:
```js
// snowpack.config.js
module.exports = {
packageOptions: {
"source": "remote",
}
};
```
`remote` 模式是 [Streaming Imports](https://www.snowpack.dev/guides/streaming-imports#how-streaming-imports-work),即不用安装对应的 npm 包到本地,snowpack 自动从 [skypack](https://www.skypack.dev/) 读取文件并缓存起来。
snowpack 看起来更多是对 bundless 纯粹的尝试,而不是一个适合满足日常开发的工具,因为日常开发需要一个一站式工具,这就是后面说的 vite 与 wmr。
### vite
可以理解为结合了 snowpack 特色的一站式构建工具,从开发到发布全套流程都帮你搞定。
涉及的用法非常多,具体内容可以看 [官方文档](https://vitejs.dev/)。
与 snowpack 不同的是,snowpack 生产打包的产物是独立的文件,而 vite 没有采用 esbuild 而是 rollup 打包,目的是为了打包为一个整体,并规避 esbuild 不稳定的风险。
另外由于 vite 集成化更高,比 snowpack 多了许多功能,比如 css 拆分、多页、使用 esbuild 进行依赖预构建、monorepo 支持、对多框架支持、SSR 等等。具体可以看 [文档介绍](https://vitejs.dev/guide/comparisons.html#snowpack)。然而原文说这有利有弊,好处是开箱即用,弊端是缺乏定制的灵活性。
其实革命性突破主要是 bundless,在这基础上发展出一系列便捷的功能,这值得每一个工程化团队学习。其实就算决定再造一个轮子,也是维持 90% 功能不变的基础上,在默认的偏好设置做一些微调,而这些大多可以用 [插件](https://vitejs.dev/guide/api-plugin.html) 解决。
总结下来,Vite 是一个既积极拥抱新特性,又为生产环境考虑的工程化全家桶,相比之下,技术栈过于前沿的工具只能称为玩具,而 Vite 是真的可以用一用的。
### wmr
由 preact 作者开发,可以理解为 preact 版的 vite。所以对于 preact 技术栈的开发者更加友好,集成度更高。
原文提到的另一个特色是,wmr 使用了 [htm](https://github.com/developit/htm) 转换 JSX,使其获得了更加精确的报错体验,即可以精确到源码行的同时指定到具体列。
综合功能和 vite 差不多,单页 + ssr 都支持,如果你平时使用 preact,或者想开发一个体积极小的项目,可以考虑用 wmr 全家桶。
## 总结
新一代前端构建工具最大特色有两个:更底层的语言编写、bundless,如果用一个词描述就是高性能。积极拥抱浏览器新特性或者知识跨界都可以帮助前端领域取得新的突破。
另外构建工具已经变得越来越集成化,从仅用于编译的 esbuild,到支持开发的 snowpack,再到内置了最佳实践、甚至支持比如 ssr 等后端能力、最后到垂直场景的 [vitePress](https://github.com/vuejs/vitepress),每抽象一次,都更开箱即用,但带来的灵活性降低也成为各团队自己造轮子的理由,越上层越是有自己造轮子的冲动。
这和可视化领域很像,可视化从最底层的 svg、canvas、webgl 到基于其封装的命令式框架,再到数据驱动开发框架、完全 JSON 配置化的图表库、甚至到零配置,根据数据猜配置的智能化项目,也是配置越来越少,但灵活度越来越低,使用什么层次的完全看项目对细节的要求。
不过工程化相对还是标准化的,因为可视化面向的是用户,而工程化面向的是程序员,我们不能控制用户需求,但可以控制程序员的开发习惯 :P。
最后,除了升级你的构建工具外,换一台 M1 芯片电脑也可以极大提升开发效率,笔者亲测 webpack 构建速度提升 3 倍!
> 讨论地址是:[精读《新一代前端构建工具对比》· Issue #316 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/316)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,125 @@
不知道你上次思考前端职业规划是什么时候?
如果你是一位学生,你肯定对前端这个职业感到陌生,你虽然没有经验,但却对未来充满好奇,你有大把时间来思考,但可能摸不着方向,有种拳头打在棉花上的无力感。
如果你已经参加了工作,不论是刚开始实习,还是工作了 3 年、5 年甚至 10 年,一定觉得非常充实,但真正用于思考的时间足够吗?如果维持现状,再过 5 年自己的提升点在哪里?如果你对这些结论不清晰,很可能是缺乏了对职业规划的思考。
这种缺乏职业规划的焦虑已经发展成为了商机。当你没有清晰职业规划,正在迷茫的时候,培训机构站出来说,是不是对职业规划充满焦虑?如果是,可以订购我们的课程,名牌大厂 P10 带你跑赢职场。其实课程确实是干货,但一个具体课程并不能代替你自己的思考,你需要自己想明白自己想要的,而不是被别人灌输思想,因为职场没有标准路线,但培训机构的文案确实有标准写法。
所以这篇前端职业规划是站在我自己角度写的,你如果也在思考长线发展问题,可以作为参考。
我总结出三个主要思考方向,分别是 **知识分类**、**领域深耕**、**经济视角**。
**知识分类** 指的是你对知识的理解是否成体系。现在全球每天新增的知识,一个人穷尽一生也学不完,如果不建立一套你自己的知识筛选标准,长期发展就无从谈起。
**领域深耕** 是实践,天天学习也是没有用的,你必须要做出什么有价值的事情,才能为行业带来贡献,或者说将知识转化为财富。当然不同职业学习与实践的比例是不同的,比如理论物理可能模糊了学习与实践的边界,而在职场环境的工程师,更容易区分什么是学习,什么是实践。
**经济视角** 是说你要能够带着经济视角看问题。可以说没有经济活动,我们一切学习、生产、职业都没有任何意义,因为推动我们学习、推动社会生产的动力是交易,没有经济活动就没有需求,需求是推动一切活动的基础。稍微理解了经济和生产的关系,就能理解为什么技术要为商业服务,因为任何技术都要有转化为商业价值的潜力才值得被研究,大到社会价值,小到产品价值,都一样。
下面我分别讲讲自己对每个方向的理解。
## 知识分类
作为前端,为了保持技术敏锐度,我们会订阅许多专栏了解新知识。仅我知道的周更专栏就有 30 个,其实根据一些专门整理好的专栏检索网站,每周甚至可以看到超过 100 种不同的前端专栏。大部分专栏都在做文章聚合,每篇专栏聚合的文章一般有 5 篇到 30 篇不等,这样即便去除重复,一周至少有几百篇新的前端技术文章等你去读,所以有些同学会觉得焦虑,甚至喊出学不动了。
我每周写前端精读恰好也要找一些文章阅读,但几年下来,我恰恰觉得每周根本找不到有用的素材。就以本周的 [javascript weekly](https://javascriptweekly.com/issues/539) 为例,我摘了一些文章标题:
- [DOM Events: A Way to Visualize and Experiment with the DOM Event System](https://javascriptweekly.com/link/108484/web)。
- [Introducing WebContainers: Run Node.js Natively in the Browser](https://blog.stackblitz.com/posts/introducing-webcontainers/)。
- [New & Updated Course: Complete Intro to React v6 with Brian Holt](https://javascriptweekly.com/link/108483/web)
- [Parcel 2 Beta 3: A Wild Rust Appears!](https://v2.parceljs.org/blog/beta3/)
- [2D Optics Demos in JavaScript](https://javascriptweekly.com/link/108493/web)
- [A Complete Beginner's Guide to Next.js](https://www.youtube.com/watch?v=nBkRxwHMrto)
- [How to Create Reusable Web Components with Lit and Vue](https://javascriptweekly.com/link/108496/web)
第二篇是通过可视化帮你理解 DOM 事件的文章,UI 很有意思,但 DOM 事件作为前端基础,精读实在不适合拿过来炒冷饭,这个知识点讲一遍就行了,没必要做成 UI 后再讲一遍。
第二篇是讲一项技术可以让 Node 运行在浏览器的,这确实是一个新技术,但现阶段我们没必要为这项技术找场景,只要知道有这个东西就行了,没必要仔细阅读。第三篇是对 React 的完整教程,非常体系化,但没有新东西,适合前端新人读,所以也不需要看。
再后面几篇分别是框架升级带来的特性介绍、一个有趣的可视化效果、Next.js 新手入门、如何用 [Lit](https://lit.dev/) 框架开发组件。这些知识从直觉来看属于可读可不读的,读了吧觉得好像对自己没什么成长,不读又觉得错过了什么,真的像鸡肋。
如果你看到这些 Feed 流也有犹豫的感觉,我建议你建立一套前端知识分类体系。就像学习武功,如果你不了解什么是基本功,什么是花拳绣腿,那么每天面临几百本推送过来的 “武学新闻” 确实是无从学起,而且也学不过来。
在技术领域,知识分类体系是有规可循的,大致可以讲知识分为两种类型:通用、行业知识。
通用知识是指最为基础、适用面也最大的知识,比如数理化,这些知识我们上学时都学过,工作中用到的知识都是建立在这些通用知识基础之上的,比如没有一定数学基础就难以学习计算机可视化领域,因为其中会大量运用数学知识。
通用知识最有用,也最保值,所以学校时就安排给我们了,那么大学其实就在教通用行业知识,所以这个阶段如果没有打牢的基础,想要弥补也很简单,只要按照大学教材温习一遍就好了,对于计算机领域的通用知识一般有计算机原理、操作系统、设计模式、编译原理、数据结构、算法等。
领域通用知识看上去比较死板,而初入工作的同学一般都在做拧螺丝钉的事,往往会忽略行业通用知识的重要性,但当你不断深入接触公司核心技术时,会发现大量运用了大学里教的那些通用知识,等用到的时候再学就迟了。
如果说行业通用知识的保值时间是 30 年,那接下来提到的行业专用知识的保值时间只有 1 年。行业专用知识就是我们在 Weekly 上看到的大部分内容,也包括培训班帮我们速成的前端框架、API 等知识。这些知识非常有用,接地气,而且刚接触工作时第一时间就要用到,但这些知识最大的问题就是太过于上层,以至于同类产品过多,可替代性强,知识点可以随着新版本发布全变了样。
就像项目脚手架工具,现在每天都会出一个基于 webpack 或者 rollup 包装的新品牌,这种脚手架就不值得学习,你也不需要把新出的脚手架当作新知识,因为这些知识的生命周期大部分不到一年,大多没有人用,最重要的是除了名字以外,组成要素里没有任何新知识,所以读完源码也学不到新知识。更最重要的是,你无法根据这些知识生产同类产品,所以如果你真的想学脚手架相关知识,认真读好一个主流脚手架源码就行了,以后除了工作中用到,不需要看任何使用文档。
对于架构能力也一样,我们在工作中通过踩坑甚至把一个项目做失败得出的经验,可能只是设计模式这本书里提到的一个常见误区;我们在设计一个非常复杂的系统时,用到的模块通信设计,可能只是操作系统设计里的一种常见通信方法。一个能理解操作系统复杂度的人,基本上可以处理与其等价复杂度的软件工程问题,而软件工程的复杂度其实很难超越操作系统,所以与其在项目里试错,不如从这些基础知识里找答案。
所以如果你想在职业规划上更进一步,检查一下自己的基础是否牢固。如果你通用知识特别扎实,就可以快速学会行业基础知识,根据行业基础知识,你甚至可以独立创造任何一个新的框架,这些框架都会成为别人学习到的行业专用知识,如果另一位同学没有打基础,把时间都用在学习你做的框架上,那么他的职业发展一定程度会被你左右,而他如果只停留在用的阶段,而不了解实现原理,从长期来看,你的职业天花板一定会更高。
关于哪些是通用基础知识、行业基础知识、行业专用知识,这里不给出具体的建议,相信每个人都会有自己的判断。
## 领域深耕
> 这段思考 **不适用于** 刚参加工作的前端同学。
前端有一句有名的鸡汤 “前端不是因为做交互界面,而是因为站在业务的最前端”,其实这句话是有问题的,我觉得每一位工作经验超过三年的前端同学都有一种在业务领域的无力感。
其实最核心的业务模型天然在后端,这是因为前端只是一个用户与业务系统交互的窗口,没有前端,用户也可以和接口直接交互,只是这么做成本很大,所以为了降低用户上手难度,或者带来更好的用户体验,才需要不断升级 UI 界面,所以 UI 界面和后端往往是多对一的关系,移动端、小程序、网页对应的接口都是一套,目的就是为了方便任何场景用户都能轻松触达业务,所以作为前端,首先要对前端存在的原因有正确的认识。
注意这里说的是业务模型,没有提到体验深度,如果讲究体验深度,自然只有前端能做到。然而前端本质还是景上添花的部分,因为在任何行业耕耘久了,如果仅仅只考虑前端,那么目标永远是体验度量、研发提效的事情,很少触及到业务层,以至于前端在业务价值的体现不直接,比较难解释体验度量、研发提效与最终业务增长之间的关系。
所以对于有一定工作经验的前端同学,想要更进一步,一定要在业务领域深耕。
那么如何在业务领域深耕呢?首先你要抛开前端视角,用业务眼光看问题,否则还是会陷入无尽的交互细节。首先要了解你所在的领域,比如笔者在的数据领域,要知道行业的历史、现状和未来,有哪些产品,每种产品的商业模式是什么,产品之间有什么关联,现在的产品距离头部产品还有哪些差距,今年产品目标主要解决什么问题,三年目标是什么等等。每个同学首先都应该理解产品,其次再产生研发、产品经理的分工。
然后审视一下自己的工作,在产品核心能力里扮演者什么角色?比如做 BI 工具,其核心是数据分析能力与报表可视化分析能力,如果你总在做类似报表列表页、个人中心这种通用中后台的工作,你就要想想,这些工作是不是可以外包出去,如果不行,那就想办法做一些领域搭建,往通用领域转吧。
当你审视了自己工作,发现核心产品能力与你工作内容不相符,而你又不想转到前端中后台通用领域一直做研发提效的事情,这时候你就要想办法和老板沟通改变一下工作内容了,你可以找一些前端也能接触强业务模型的领域,比如 BI 分析,数据可视化等等。其实通用领域也有不少深水区,比如语雀背后的富文本编辑器、流程图、研发工作台、业务组件库等等都是可以做深的通用领域,当你想再上一层楼时,就要像玉伯一样成为语雀整个产品的引领者,这样你其实又进入了知识协作、生产力工具这个专业领域。
如果你既不想往通用技术领域发展,又无法改变工作内容,就尝试承担更多职责吧,如果可能的话,尝试参与后端业务逻辑的开发,这样可以帮助你深入、全面理解业务逻辑。其实前端 + 产品的路线也可以很好在专业领域做深,前端 + 后端路线也可以,你需要根据自己团队实际情况做出调整。
任何产品的研发团队都要有产品全局观,这就是刚才说的在技术之外,你对你所在业务领域的理解程度,理解程度越高,技术方向就越明确,但如果你的职业规划是再继续攀爬,就要成为整个产品负责人了。现在的年轻人非常上进,许多公司都在尝试采取活水政策,让想更进一步的年轻人尝试新方向开疆拓土,而不是留在一个成熟的团队里内卷。
## 经济视角
做职业规划的另一个目的当然是升职加薪了,但是你的薪资并不能无限膨胀,其增长大致还是符合市场规律的。另外任何工作都是一笔经济账,我们要带着技术、产品和经济视角看业务,才能做出合理的判断。
因为去年疫情原因,全球远程办公得到了积极实践,并且在未来依然有增长潜力,因此作为用人单位方,必定会逐渐放眼全球去看人力成本问题,因为在哪都能办公。从全球软件开发数据来看,美国的工资水平最高,中国软件工程师的工资也紧随其后,所以在软件领域中国已经不存在劳动力成本低廉的优势了,尤其当你工作经验丰富后,要竞争中高级岗位,中国软件公司开的薪资放眼全球都不低。
然而国家之间技术发展阶段、教育水平仍然存在差距,如果同样的资深技术专家岗位,国内与国外开的薪资持平,但中国的软件工程师架构水平完全不及美国的软件工程师,那么长期来看,这种错配会造成企业用人成本浪费,企业会在一定程度想办法优化一下人员构成的。因此作为前端,或者软件工程师,你必须清楚长期而言,你要和全球的软件工程师竞争,所以你还要充分了解你的领域在全球范围的发展阶段,人才水平如何。
以上是个人的经济账,接下来谈谈业务的经济账。
首先你要了解自己的技术是怎样转化为收入,覆盖自己工资的。我们首先看市场竞争,市场竞争通过价格调节供需关系,我们做的产品成本、售价很清楚,是否值得做一目了然。然而对于复杂产品需要多人协作,如果人与人之间再通过市场化机制合作,往往容易产生低效的结果,比如我做的按钮按照 3 元一个的价格卖给后端,那为了提升我的价值,我会提价到 5 元一个,然而倾向于给产品加更多的按钮,这样都在看短期利益,谁也不会为产品长期发展负责。
所以公司是一个相对大锅饭的组织,谁也不要给自己工作定价,大家都尽可能的打磨产品,月底按照合同约定给固定薪酬。这样做确实解决了产品长期发展的问题,但这套机制成熟后,尤其在大公司,刚毕业就去拧螺丝钉的同学很可能永远没有机会了解何为成本,没有成本概念,就难以想清楚为什么做事要考虑投入产出比,或者觉得 ROI 这个词很高级,其实这个词一点不高级,只是公司将它屏蔽了,但如果这导致你做技术完全不考虑成本,只追求让你激动的技术细节,或者只做你感兴趣的技术方向,那其实是不成熟的表现,你做的事情可能也难以被业务认可。
如果你想往更高层次发展,成本意识是一定要培养的,可以了解一下人力成本、机器成本、以及接入二方、三方服务的外部成本,了解这些成本后,再算算产品年营收是否能覆盖这些成本,如果想继续加人,那明年产品营收相应要翻多少,现在市场空间允许产品翻这么多吗?如果想提供更好的服务,要加机器,那么你的业务方是否会因为服务变好变得更多?衡量业务方增多带来的价值一般从订单价格,MAU 来看,如果服务外部,直接看价格是否覆盖成本就行了,如果服务内部,就看 MAU 是否值得投入这些机器成本。
然而也不能只看钱,市场份额也很重要。如果 Chrome 对研发投入只看年营收,那现在 IE 估计还是主流浏览器。其实 Chrome 在确立霸主地位后,对谷歌产品生态的打通、W3C 的话语权、开发者吸引力有很大提升,这些看不见的影响面难以直接转化为金钱来统计,所以如果你认为产品市场份额的提升可以带来长线价值,那么也可以把市场份额作为目标之一。
最后经济视角也不仅仅让我们停留在算业务帐上,经济学的边际收益理论可以指导我们优先做边际收益更大的事。当前业务产品矩阵中,拓展哪些产品可以快速弥补不足,如果做技术优化,优化哪些模块带来产品收益、可维护性收益最大,如果时刻能想清楚这些问题,那每年的产品、技术方向就不会跑偏。
## 总结
总结一下文中提到的三个思考方向,其实是职业生涯发展中可能遇到的三种问题。
工作时间久了就会发现,哪怕依然有学习的激情,但保持刚毕业那会的学习方式已经难有突破了,你会发现:工作实践用到的知识不会很多,反复读或者写入门技术文章,只会让自己停留在校招生的技术水平;自己所处的职业也限制了进一步发展,你需要思考怎么打破职业天花板;甚至只钻研技术领域都是不够的,大家都在谈成本,你在谈技术,天然就不在一个频道上。
本文也给出了对应的三个解决方案,**知识分类** 帮助你解决反复学习无用的、入门知识的问题;**领域深耕** 帮助你解决职业天花板的问题;**经济视角** 帮助你解决技术单一视角的问题。
其实职业有天花板很正常,没有哪个职业上升通道是一路无阻的,但人是活的,你可以逐渐改变自己,在适当的时候多看看业务、经济问题,学习知识也不要仅停留在表面,虽然这些你工作中可能根本用不到,但这其实是悖论,因为你没掌握某些知识,所以也没机会接触那些工作,想打破悖论只能从痛苦的自我打破边界开始。
与一般前端职业规划不同,我并没有说很多前端领域专有名词,或者点名要学哪些框架,因为我觉得人之间智商差距并不大,必须掌握的知识工作几年都能学会,而真正能拉开人之间差距的,不是智商,而是学习方法,或者学习路线,如果你把时间用在错误的地方,或者错误的阶段,终将积累成巨大差距。
希望我的思考可以对你有帮助。
> 讨论地址是:[精读《前端职业规划 - 2021 年》· Issue #317 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/317)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -422,7 +422,7 @@ function Article({ id }) {
return () => {
didCancel = true;
};
}, [API.fetchArticle]);
}, [id]);
// ...
}
+108
View File
@@ -0,0 +1,108 @@
我站在前端角度梳理出数据领域技术专家应该学习的技术点,以下是能力要点:
- 同时具备产品、数据、技术能力。
- 以前端为切入点,深入各技术领域,包括前后端、数据技术。
- 打牢基础知识,熟悉领域知识。
对于知识,可以分为通用、领域的知识,其中越通用、越基础的知识越保值,比如数学知识的保质期可能持续到地球毁灭,而 Webpack5 的 API 可能保质期只有三个月。
所以在学习前,要对各类知识的保质期有个大概的了解:
- **通用基础知识**:100 年以上。
- **行业基础知识**,如产品、计算机基础知识:30 年以上。
- **行业通用领域知识**:10 年以内。
- **行业专用领域知识**:1 年左右。
### 能力模型
- **通用基础知识**
- 数学
- 坐标系
- 参数方程
- 向量
- 点乘
- 叉乘
- 矩阵
- 矩阵乘法
- 线性变换
- 仿射变换
- 计算机 **行业基础知识**
- 数据结构
- 数据
- 链表
-
-
- 哈希表
- 树 & 二叉树 & 二叉搜索树
- 字典树
- 并查集
- 布隆过滤器
- 算法
- 位运算
- 滑动窗口
- 双指针
- 贪心
- 回溯
- 递归
- 分治
- 动态规划
- 编译原理
- 设计模式
- 产品
- **行业基础知识**
- 经济学
- 商学
- 心理学
- 数据
- **行业基础知识**
- 数据明细与聚合
- SQL
- **行业通用领域知识**
- 计算字段
- **行业专用领域知识**
- 数据分析表达式
- 前端
- **行业基础知识**
- 编程范式
- 命令式
- 过程式
- 面向对象
- 声明式
- 逻辑式
- 函数式
- 数据修改
- 可变数据
- 不可变数据
- 图形学
- **行业通用领域知识**
- javascript、typescript
- css
- dom、svg、canvas
- webgl
- **行业专用领域知识**
- 前端框架
- React
- Vue
- 数据流框架
- Redux
- Mobx
- 构建工具
- Webpack
- Snowpack
- 脚手架
- Vite
- 全栈框架
- Next.js
- 后端
- **行业基础知识**
- 架构模式
- 一主多从
- **行业专用领域知识**
- 后端框架
- Spring
### 前端精读与能力模型
前端精读不可能覆盖上述所有知识,有的知识是课本学的,有的知识是步入社会后,看书或者通过专门渠道学习的,所以本篇只对能力模型做一个指导,而学海无涯,前端精读即便持续创作一百年,也仍然挂一漏万。
不过这个能力模型会对前端精读选材起到主导作用,我会尽量挑选重要的,保质期长的基础知识解读,而保质期较短的行业专用领域知识则尽量少讲,希望前端精读能形成一个正金字塔,底座是厚厚的基础知识,金字塔尖是重要的行业知识,风沙会经常侵蚀金字塔尖,所以金字塔尖需要不断的更新迭代,但我相信,只要有一个稳定的底座,上层的修缮总不是难事。