1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 10:22:23 +00:00

docs(async): edit async

This commit is contained in:
ruanyf 2017-01-02 23:19:46 +08:00
parent f02924cd5a
commit 6609f64696
2 changed files with 67 additions and 58 deletions

View File

@ -1,6 +1,6 @@
# 异步操作和Async函数
异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的如果没有异步编程根本没法用非卡死不可。
异步编程对 JavaScript 语言太重要。Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。本章主要介绍如何 Generator 函数完成异步操作。
ES6诞生以前异步编程的方法大概有下面四种。
@ -9,13 +9,13 @@ ES6诞生以前异步编程的方法大概有下面四种。
- 发布/订阅
- Promise 对象
ES6将JavaScript异步编程带入了一个全新的阶段ES7的`Async`函数更是提出了异步编程的终极解决方案
Generator 函数将 JavaScript 异步编程带入了一个全新的阶段
## 基本概念
### 异步
所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
@ -23,36 +23,38 @@ ES6将JavaScript异步编程带入了一个全新的阶段ES7的`Async`函数
### 回调函数
JavaScript语言对异步编程的实现就是回调函数。所谓回调函数就是把任务的第二段单独写在一个函数里面等到重新执行这个任务的时候就直接调用这个函数。它的英语名字callback,直译过来就是"重新调用"。
JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数的英语名字`callback`,直译过来就是"重新调用"。
读取文件进行处理,是这样写的。
```javascript
fs.readFile('/etc/passwd', function (err, data) {
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
```
上面代码中,readFile函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了`/etc/passwd`这个文件以后,回调函数才会执行。
上面代码中,`readFile`函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了`/etc/passwd`这个文件以后,回调函数才会执行。
一个有趣的问题是为什么Node.js约定回调函数的第一个参数必须是错误对象err如果没有错误该参数就是null原因是执行分成两段在这两段之间抛出的错误程序无法捕捉只能当作参数传入第二段。
一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对象`err`(如果没有错误,该参数就是`null`
原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。
### Promise
回调函数本身并没有问题它的问题出现在多个回调函数嵌套。假定读取A文件之后再读取B文件代码如下。
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取`A`文件之后,再读取`B`文件,代码如下。
```javascript
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
```
不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为"回调函数地狱"callback hell
不难想象,如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"callback hell
Promise就是为了解决这个问题而提出的。它不是新的语法功能而是一种新的写法允许将回调函数的嵌套改成链式调用。采用Promise连续读取多个文件写法如下。
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise连续读取多个文件写法如下。
```javascript
var readFile = require('fs-readfile-promise');
@ -72,11 +74,11 @@ readFile(fileA)
});
```
上面代码中我使用了fs-readfile-promise模块它的作用就是返回一个Promise版本的readFile函数。Promise提供then方法加载回调函数catch方法捕捉执行过程中抛出的错误。
上面代码中,我使用了`fs-readfile-promise`模块,它的作用就是返回一个 Promise 版本的`readFile`函数。Promise 提供`then`方法加载回调函数,`catch`方法捕捉执行过程中抛出的错误。
可以看到Promise 的写法只是回调函数的改进使用then方法以后异步任务的两段执行看得更清楚了除此以外并无新意。
可以看到Promise 的写法只是回调函数的改进,使用`then`方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆`then`,原来的语义变得很不清楚。
那么,有没有更好的写法呢?
@ -88,12 +90,12 @@ Promise 的最大问题是代码冗余原来的任务被Promise 包装了一
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步协程A开始执行。
- 第二步协程A执行到一半进入暂停执行权转移到协程B。
- 第三步一段时间后协程B交还执行权。
- 第四步协程A恢复执行。
- 第一步,协程`A`开始执行。
- 第二步,协程`A`执行到一半,进入暂停,执行权转移到协程`B`
- 第三步,(一段时间后)协程`B`交还执行权。
- 第四步,协程`A`恢复执行。
上面流程的协程A就是异步任务因为它分成两段或多段执行。
上面流程的协程`A`,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
@ -107,7 +109,7 @@ function *asyncJob() {
上面代码的函数`asyncJob`是一个协程,它的奥妙就在其中的`yield`命令。它表示执行到此处,执行权将交给其他协程。也就是说,`yield`命令是异步两个阶段的分界线。
协程遇到`yield`命令就暂停等到执行权返回再从暂停的地方继续往后执行。它的最大优点就是代码的写法非常像同步操作如果去除yield命令简直一模一样。
协程遇到`yield`命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除`yield`命令,简直一模一样。
### Generator 函数的概念
@ -126,9 +128,9 @@ g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
```
上面代码中调用Generator函数会返回一个内部指针即遍历器g 。这是Generator函数不同于普通函数的另一个地方即执行它不会返回结果返回的是指针对象。调用指针g的next方法会移动内部指针即执行异步任务的第一段指向第一个遇到的yield语句上例是执行到`x + 2`为止。
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)`g`。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针`g``next`方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的`yield`语句,上例是执行到`x + 2`为止。
换言之next方法的作用是分阶段执行Generator函数。每次调用next方法会返回一个对象表示当前阶段的信息value属性和done属性。value属性是yield语句后面表达式的值表示当前阶段的值done属性是一个布尔值表示Generator函数是否执行完毕即是否还有下一个阶段。
换言之,`next`方法的作用是分阶段执行`Generator`函数。每次调用`next`方法,会返回一个对象,表示当前阶段的信息(`value`属性和`done`属性)。`value`属性是`yield`语句后面表达式的值,表示当前阶段的值;`done`属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
### Generator 函数的数据交换和错误处理
@ -558,15 +560,15 @@ co(gen).then(function (){
1回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
2Promise 对象。将异步操作包装成Promise对象用then方法交回执行权。
2Promise 对象。将异步操作包装成 Promise 对象,用`then`方法交回执行权。
co模块其实就是将两种自动执行器Thunk函数和Promise对象包装成一个模块。使用co的前提条件是Generator函数的yield命令后面只能是Thunk函数或Promise对象。
co 模块其实就是将两种自动执行器Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是Generator 函数的`yield`命令后面,只能是 Thunk 函数或 Promise 对象。
上一节已经介绍了基于 Thunk 函数的自动执行器。下面来看,基于 Promise 对象的自动执行器。这是理解 co 模块必须的。
### 基于 Promise 对象的自动执行
还是沿用上面的例子。首先把fs模块的readFile方法包装成一个Promise对象。
还是沿用上面的例子。首先,把`fs`模块的`readFile`方法包装成一个 Promise 对象。
```javascript
var fs = require('fs');
@ -600,7 +602,7 @@ g.next().value.then(function(data){
});
```
手动执行其实就是用then方法层层添加回调函数。理解了这一点就可以写出一个自动执行器。
手动执行其实就是用`then`方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。
```javascript
function run(gen){
@ -620,7 +622,7 @@ function run(gen){
run(gen);
```
上面代码中只要Generator函数还没执行到最后一步next函数就调用自身以此实现自动执行。
上面代码中,只要 Generator 函数还没执行到最后一步,`next`函数就调用自身,以此实现自动执行。
### co 模块的源码
@ -637,7 +639,7 @@ function co(gen) {
}
```
在返回的Promise对象里面co先检查参数gen是否为Generator函数。如果是就执行该函数得到一个内部指针对象如果不是就返回并将Promise对象的状态改为resolved。
在返回的 Promise 对象里面co 先检查参数`gen`是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为`resolved`
```javascript
function co(gen) {
@ -650,7 +652,7 @@ function co(gen) {
}
```
接着co将Generator函数的内部指针对象的next方法包装成onFulfilled函数。这主要是为了能够捕捉抛出的错误。
接着co Generator 函数的内部指针对象的`next`方法,包装成`onFulfilled`函数。这主要是为了能够捕捉抛出的错误。
```javascript
function co(gen) {
@ -674,33 +676,39 @@ function co(gen) {
}
```
最后就是关键的next函数它会反复调用自身。
最后,就是关键的`next`函数,它会反复调用自身。
```javascript
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "'
+ String(ret.value)
+ '"'
)
);
}
```
上面代码中next 函数的内部代码,一共只有四行命令。
上面代码中,`next`函数的内部代码,一共只有四行命令。
第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
第二行,确保每一步的返回值,是 Promise 对象。
第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
第三行,使用`then`方法,为返回值加上回调函数,然后通过`onFulfilled`函数再次调用`next`函数。
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为`rejected`,从而终止执行。
### 处理并发的异步操作
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。
这时要把并发的操作都放在数组或对象里面跟在yield语句后面。
这时,要把并发的操作都放在数组或对象里面,跟在`yield`语句后面。
```javascript
// 数组的写法

View File

@ -181,6 +181,7 @@
- Daniel Brain, [Understand promises before you start using async/await](https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8): 讨论async/await与Promise的关系
- Jake Archibald, [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/getting-started/primers/async-functions)
- Axel Rauschmayer, [ES proposal: asynchronous iteration](http://www.2ality.com/2016/10/asynchronous-iteration.html): 异步遍历器的详细介绍
- Dima Grossman, [How to write async await without try-catch blocks in Javascript](http://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/): 除了 try/catch 以外的 async 函数内部捕捉错误的方法
## Class