Skip to content

Commit 5f3c034

Browse files
committed
继续写问题部分
1 parent b68f399 commit 5f3c034

File tree

4 files changed

+195
-40
lines changed

4 files changed

+195
-40
lines changed

01-1-start.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
异步的起源
2+
========
3+
4+
故事必须从头说起,在很久很久以前……
5+
6+
## 为校验表单,JavaScript 诞生了
7+
8+
在那个拨号上网的洪荒年代,浏览器还非常初级,与服务器进行数据交互的唯一方式就是提交表单。用户填写完成之后,交给服务器处理,如果内容合规当然好,如果不合规就麻烦了,必须打回来重填。那会儿网速还是论 Kb 的,比如我刚上网那会儿开始升级到 33.6Kb,主流还是 22.4Kb……
9+
10+
所以很容易想象:当用户填完100+选项,按下提交按钮,等待几十秒甚至几分钟之后,反馈回来的信息却是:“您的用户名不能包含大写”,他会有多么的崩溃多么的想杀人。为了提升用户体验,网景公司的[布兰登·艾克](https://zh.wikipedia.org/wiki/%E5%B8%83%E8%98%AD%E7%99%BB%C2%B7%E8%89%BE%E5%85%8B)大约用10天时间,开发出 JavaScript 的原型,从此,这门注定改变世界的语言就诞生了。
11+
12+
只是当时大家都还不知道,发明它的目的,只是为校验表单。
13+
14+
## JavaScript 中存在大量异步计算
15+
16+
同样为了提升用户体验,HTML DOM 也选择了边加载,边生成,边渲染的策略。再加上要等待用户操作,大量交互都以事件来驱动。于是,JavaScript 就存在大量的异步计算。
17+
18+
这也带来一个好处,作为一门 UI 语言,异步操作帮 JavaScript 避免了页面冻结。
19+
20+
为什么异步操作可以避免界面冻结呢?
21+
22+
### 同步的利弊
23+
24+
> 假设你去到一家饭店,自己找座坐下了,然后招呼服务员拿菜单来。
25+
26+
> 服务员说:“对不起,我是‘同步’服务员,我要服务完这张桌子才能招呼你。”
27+
28+
> 那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等待别人的一个大动作完成。你是不是很想抽ta?
29+
30+
这就是“同步”的问题:顺序交付的工作1234,必须按照1234的顺序完成。
31+
32+
不过它的也有好处:逻辑非常简单。你不用担心每步操作会消耗多少时间,反正每步操作都会在上一步完成之后才进行,只管往后写就是了。
33+
34+
### 异步的利弊
35+
36+
与之相反,异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。
37+
38+
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫"异步"。
39+
40+
所以,那些需要大量计算(比如 Service Worker),或者复杂查询(比如 Ajax)的工作,JS 引擎把它们交给系统之后,就回来继续待机了,于是我们总能看到浏览器第一时间响应我们的操作,感觉非常好。
41+
42+
有利必有弊,异步的缺点就是:必须通过特殊的语法才能实现,而这些语法看起来就不如同步那样清晰明确了。
43+
44+
## 异步计算的实现
45+
46+
异步计算有两种常见的实现形式。
47+
48+
### 事件侦听
49+
50+
这种形式在浏览器里比较常见,比如,我们可以对一个 `<button>` 的用户点击行为增加侦听,在点击事件触发后调用函数进行处理。
51+
52+
```javascript
53+
document.getElementById('#button').addEventListener('click', function (event) {
54+
// do something
55+
}, false);
56+
```
57+
58+
也可以使用 DOM 节点的 `onclick` 属性绑定侦听函数:
59+
60+
```javascript
61+
document.getElementById('#button').onclick = function (event) {
62+
// do something
63+
}
64+
```
65+
66+
### 回调
67+
68+
到了 Node.js(以及其它 Hybrid 环境),由于要和 JS 引擎进行交互,大部分操作都变成回调。比如用 `fs.readFile()` 读取文件内容:
69+
70+
```javascript
71+
const fs = require('fs');
72+
73+
fs.readFile('path/to/file.txt', 'utf8', function (err, content) {
74+
if (err) {
75+
throw err;
76+
}
77+
console.log(content);
78+
});
79+
```
80+
81+
如果你不熟悉 Node.js 也没关系,jQuery 里也有类似的操作,最常见的就是侦听页面加载状态,加载完成后启动回调函数:
82+
83+
```javascript
84+
$(function () {
85+
// 绑定事件
86+
// 创建组件
87+
// 以及其它操作
88+
});
89+
```

01-2-issue.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
异步的问题
2+
========
3+
4+
无论是事件还是回调,基本原理是一致的:
5+
6+
> 把当前语句执行完;把不确定完成时间的计算交给系统;等待系统唤起回调。
7+
8+
于是带来第一个问题:**栈被破坏了,无法进行常规的 `try/catch`**
9+
10+
## 断开的栈与 `try/catch`
11+
12+
我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,运行时就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
13+
14+
可是异步的回调函数(包括事件处理函数,下同)不完全如此,比如下面这段代码:
15+
16+
```javascript
17+
function callback (err, content) { // [callback]
18+
// 处理
19+
}
20+
fs.readFile('path/to/file.txt', 'utf8', callback); // [A]
21+
let foo = 123;
22+
// 继续执行其它代码
23+
```
24+
25+
A 函数执行后,并不直接调用 callback,而是继续执行其它代码,直至完成,出栈。真正调用 callback 的是运行时,启动一个新的栈,callback 作为这个栈的第一个函数。所以当函数报错的时候,我们无法获取之前栈里的信息,不容易判定是什么导致的错误。并且,如果我们在外层套一个 `try/catch`,也捕获不到错误。关于这一点,等下还会有说明。
26+
27+
## 回调陷阱
28+
29+
这个问题其实是最直观的问题,也是大家谈的最多的问题。比如下面这段代码:
30+
31+
```javascript
32+
a(function (resultA) {
33+
b(resultA, function (resultB) {
34+
c(resultB, function (resultC) {
35+
d(resultC, function (resultD) {
36+
e(resultD, function (resultE) {
37+
f(resultE, function (resultF) {
38+
// 子子孙孙无穷尽也
39+
console.log(resultF);
40+
});
41+
});
42+
});
43+
});
44+
});
45+
});
46+
```
47+
48+
嵌套层次之深令人发指。这种代码很难维护,有人称之为“回调地狱”,有人称之为“回调陷阱”,还有人称之为“回调金字塔”,其实都无所谓,带来的问题很明显:
49+
50+
1. **难以维护。** 上面这段只是为演示写的示范代码,还算好懂;实际开发中,混杂了业务逻辑的代码更多更长,那才真的没法动。
51+
2. **难以复用。** 回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全局,可能只有全靠手写,结果就会越搞越长。
52+
53+
## 更严重的问题
54+
55+
面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里是不功不过不加分的。要想得到满分必须能答出更深层次的问题。
56+
57+
为了说明这些问题,我们先来看一段代码。假设有这样一个需求:
58+
59+
> 遍历目录,找出最大的一个文件。
60+
61+
```javascript
62+
/**
63+
* @param dir 目标文件夹
64+
* @param callback 完成后的回调
65+
*/
66+
function findLargest(dir, callback) {
67+
fs.readdir(dir, function (err, files) {
68+
if (err) return callback(err); // [1]
69+
let count = files.length; // [2]
70+
let errored = false;
71+
let stats = [];
72+
files.forEach( file => {
73+
fs.stat(path.join(dir, file), (err, stat) => {
74+
if (errored) return; // [1]
75+
if (err) {
76+
errored = true;
77+
return callback(err);
78+
}
79+
stats.push(stat); // [2]
80+
81+
if (--count === 0) {
82+
let largest = stats
83+
.filter(function (stat) { return stat.isFile(); })
84+
.reduce(function (prev, next) {
85+
if (prev.size > next.size) return prev;
86+
return next;
87+
});
88+
callback(null, files[stats.indexOf(largest)]);
89+
}
90+
});
91+
});
92+
});
93+
}
94+
95+
findLargest('./path/to/dir', function (err, filename) {
96+
if (err) return console.error(err);
97+
console.log('largest file was:', filename);
98+
});
99+
```
100+
101+
这里我声明了一个函数 `findLargest()`,用来查找某一个目录下体积最大的文件。大家先请看代码中标记 `[1]` 的地方

01-issues-of-async.md

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,6 @@
11
异步的问题
22
========
33

4-
故事必须从头说起,在很久很久以前……
4+
之所以会出现这样那样的解决方案,我之所以写这样的文章介绍这些解决方案,肯定是异步本身有问题。
55

6-
## 为校验表单,JavaScript 诞生了
7-
8-
在那个拨号上网的洪荒年代,浏览器还非常初级,与服务器进行数据交互的唯一方式就是提交表单。用户填写完成之后,交给服务器处理,如果内容合规当然好,如果不合规就麻烦了,必须打回来重填。那会儿网速还是论 Kb 的,比如我刚上网那会儿开始升级到 33.6Kb,主流还是 22.4Kb……
9-
10-
所以很容易想象:当用户填完100+选项,按下提交按钮,等待几十秒甚至几分钟之后,反馈回来的信息却是:“您的用户名不能包含大写”,他会有多么的崩溃多么的想杀人。为了提升用户体验,网景公司的[布兰登·艾克](https://zh.wikipedia.org/wiki/%E5%B8%83%E8%98%AD%E7%99%BB%C2%B7%E8%89%BE%E5%85%8B)大约用10天时间,开发出 JavaScript 的原型,从此,这门注定改变世界的语言就诞生了。
11-
12-
只是当时大家都还不知道,发明它的目的,只是为校验表单。
13-
14-
## JavaScript 中存在大量异步计算
15-
16-
同样为了提升用户体验,HTML DOM 也选择了边加载,边生成,边渲染的策略。再加上要等待用户操作,大量交互都以事件来驱动。于是,JavaScript 就存在大量的异步计算。
17-
18-
这也带来一个好处,作为一门 UI 语言,异步操作帮 JavaScript 避免了页面冻结。
19-
20-
为什么异步操作可以避免界面冻结呢?
21-
22-
### 同步的利弊
23-
24-
> 假设你去到一家饭店,自己找座坐下了,然后招呼服务员拿菜单来。
25-
26-
> 服务员说:“对不起,我是‘同步’服务员,我要服务完这张桌子才能招呼你。”
27-
28-
> 那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等待别人的一个大动作完成。你是不是很想抽ta?
29-
30-
这就是“同步”的问题:顺序交付的工作1234,必须按照1234的顺序完成。
31-
32-
不过它的也有好处:逻辑非常简单。你不用担心每步操作会消耗多少时间,反正每步操作都会在上一步完成之后才进行,只管往后写就是了。
33-
34-
### 异步的利弊
35-
36-
与之相反,异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。
37-
38-
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫"异步"。
39-
40-
所以,那些需要大量计算(比如 Service Worker),或者复杂查询(比如 Ajax)的工作,JS 引擎把它们交给系统之后,就回来继续待机了,于是我们总能看到浏览器第一时间响应我们的操作,感觉非常好。
41-
42-
有利必有弊,异步的缺点就是:必须通过特殊的语法才能实现,而这些语法看起来就不如同步那样清晰明确了。
43-
44-
## 异步计算的实现
6+
是的,异步就是有那样让人难以割舍,又让人不易亲近的特质。

SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
* [前言](README.md)
44
* [异步的问题](01-issues-of-async.md)
5+
* [异步的起源](01-1-start.md)
6+
* [异步的问题](01-2-issue.md)
7+
* [异步问题的发展](01-3-growing.md)
58
* [Promise 方案](02-promise-intro.md)
69
* [Promise 入门](02-1-promise-basic.md)
710
* [Promise 进阶](02-2-promise-advanced.md)

0 commit comments

Comments
 (0)