前言

异步操作是非常常见的操作,也是其中一题常见的前端工程师面试题目。在日常开发中,我们会和伺服器交互,或者是和用户的行动作出反应,比如说监听某些点击事件。这个时候,其实我们是执行了异步操作,我们需要等待对方若干时间才能收到返回值,甚至是一个错误的值。。。因此,异步操作很容易产生一些误会。这裡,我会说一下异步操作,及其返回的处理方法。

同步与异步的分别

在同步的世界裡面,我们希望执行某些操作之后,就能够马上拿到返回的值,然后执行下一步。然而,当我们发出 HTTP 请求的时候,我们可能需要等待世界的另外一端返回信息,这需要时间,便不是同步了。。。

如果没有异步的话,当你发出 HTTP 请求的时候,浏览器需要等待伺服器返回才执行下一步。这就是代表浏览器会卡住。。。

异步处理,简单来说就是我们发出了一个行动,但是不是马上得到结果,我们会继续执行后面的指令,等到函数裡面有一个返回,我们才拿那个返回值来使用。最简单的说法,就是我们向服务器发出请求,但是服务器需要时间处理,并且返回处理过的内容。

异步操作处理方法

  1. Callback
  2. Promise (思想)
  3. Generator
  4. Async await (推荐)

Callback

我们看一下维基百科上面对于 Callback 的说明:
在电脑程式设计中,回呼函式,或简称回呼(Callback 即call then back 被主函数呼叫运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的参照。这一设计允许了底层代码呼叫在高层定义的子程式。

这样好像说得蛮複杂的,但其实很简单,你把一个函数 cb (Function) 作为参数 (Argument)传进这个函数 B 裡面,然后在函数 B 使用函数 cb。

1
2
3
function func(x, cb) {
cb(x)
}

举个例子,比如我们要在 Node.js 裡面读取一个档案,我们的代码会是下面这样的:

hello.txt:

1
Hello World, I'm Calpa Liu.

index.js:

1
2
3
4
5
6
7
8
var fs = require('fs');
fs.readFile('hello.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log(data);
});
console.log('Finished');

我们会在 callback 裡面处理 callback,也就是一个回调裡面做另外一件事情,那可能是另外一个 callback,那麽你可以想象一下,我们越写越深。。。这样有两个问题,第一:代码的藕合性会很高,不容易去拆分代码;第二:代码的维护性很差。

bjHDvVN.jpg

Promise

Promise 是说如果你做了 A 的事情,成功了就做 B,不成功就做 C,你还可以继续做 D 的事情,然后进行成功和不成功的处理。这样说可能比较虚,但是你看一下 MDN 上面的图就会明白了。

w9BxjmL.png

一个发射子弹的动作可以这样写:

1
2
3
4
5
6
7
8
var fire = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('已命中三千里外的目标');
}, 3000)
});
fire.then(function(result) {
console.log(result);
});

你可以在 Windows 平台按 F12 或 Ctrl + Shift + I,或在 Mac 上 按 Cmd + Opt + I,打开 Google Chrome Developer Tools ,然后在 Console 贴上上面的代码,三秒后就会输出已命中三千里外的目标。

你也可以在 then() 裡面写 1个到 N个的 Promise。

1
2
3
4
5
6
7
8
9
10
11
12
var fire = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('已命中三千里外的目标');
}, 3000)
});
fire.then(function(result) {
console.log(result);
console.log('正在返回司令部');
return fire.then(function (result) {
console.log(result);
});
});

Generator

Generator 其实是一个状态机,内部保存机器的运行状态。我们透过获取机器的完成状态 (done),我们能够重複调用机器。我们可以使用 yield 暂停一个函数,并跳出函数。从外面的角度来看,我们可以从上而下去写代码,但是代码会複杂,难以理解。

虽然它已经写进 ECMAScript 2015 的正式规范裡面,但是我不太喜欢使用 Generator。我们简单看一下 Generator 就好了,因为现在是 2017 年,异步操作的有更加好的处理方法。

1
2
3
4
5
6
7
8
9
10
11
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"
g; //
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: false}
g.next() // {value: undefined, done: true}

Async/Await

对于技术的要求,是无止境的。为了写出更加优美的代码,你又可以付出什麽的代价呢?

如果你没有试过 Async/Await 的话,那麽你就应该试一下,因为实在是太优雅了。

这里我就放出一段现在博客在用的代码:

1
2
3
4
async const getPosts = () => {
await res = axios.get('https://calpa.me/posts');
return res.data;
}

我们简单的读一下这段代码:

一个异步的不变量 getPosts 是一个箭头函数,内部操作为等待 axios 的 GET 请求到地址:http://calpa.me,并返回伺服器返回的资料。

这是一个非常简单的异步操作吧,但是如果是这样的呢?

1
2
3
4
5
async const getUserData = () => {
await posts = axios.get('https://calpa.me/posts');
await accountInfo = axios.get('https://calpa.me/about');
/* ... */
}

如果你想要使用 Async / Await 的话,可以使用
Node.js 7.6 或以上的版本。例如使用 nvm 安装 v8: nvm install v8,然后 nvm use v8。

另外,如果你不想更新 Node 版本的话,你可以安装 async 工具库。