js中的异步处理:你想知道的都在这了
假如你已经知道了什么是异步,并且已经写过很多的异步代码。这篇文章主要介绍几种对异步代码的处理,即异步编码姿势:
- 回调函数;
- Promise;
- 迭代器、生成器;
- async/await。
重点在第3、4部分。
回调函数
这个没什么好说的,直接看一段代码:
1 |
|
后面部分都以该读取文件操作为例来讲解。
Promise
Promise
就是为异步而生的,主要是为了解决所谓的回调地狱问题。Promise
的三个状态:pending
,fulfilled
和rejected
。
通常的写法:
1 |
|
需要注意一点的是:new Promise()
传入的函数会立即执行,then
跟catch
中传入的函数才是异步执行的。
then
方法何时执行?取决于两点:
promise
何时变成完成状态(fulfilled
);- 在异步队列中的位置。
迭代器、生成器
概念的理解
先理解两个概念:生成器是一个返回迭代器的函数;那么迭代器就是生成器执行后返回的结果(对象)。所以,生成器是函数,迭代器是对象(很容易弄混的两个概念)。
首先,生成器是一个函数,这是一个特殊的函数,函数定义如下:
1 |
|
yield 返回值取决于 next 方法传进去的值,不是 yield 后面表达式的值
异步的实现
看下面这段代码:
1 |
|
这是node.js中一个简单的读取文件的异步操作,因为用了Promise
,所以正常的使用应该是这样的:
1 |
|
其实这就是上面介绍的Promise
对异步的处理。假如我们有这样一个想法,希望代码是这样的:
1 |
|
我们知道,正常情况下,这段代码肯定不会如期执行,因为我们的data
其实是一个promise
对象。但是假如有这样一个容器,它能如期的执行我们上面的这段代码,我们只需要把代码丢进这个特殊的容器里。注意到没有,上面这段代码其实是一段同步的代码,通过同步的代码实现异步的操作,这似乎是一个很完美的想法,只是首先我们需要有这样的一个容器。
运行容器
运行异步代码的容器:
1 |
|
现在,我们有了这样的一个容器run
,把读取文件的那段“同步”代码丢进这个容器里:
1 |
|
现在,我们的代码便能如期的执行了!
简单的解释一下,我们将读取文件的这段“同步”代码包装成了一个生成器函数,然后传给run
函数去处理。在run
函数内部首先执行这个生成器函数并返回了一个迭代器对象,当第一次执行let result = task.next()
的时候,执行的就是readFile('config.json')
这句,而这个函数会异步去读取文件并立马返回一个promise
对象。所以result
的值就是{value: promise, done: false}
。由于result.value
本身是一个promise
对象,所以执行const promise = Promise.resolve(result.value)
这句的时候返回的仍然是传入的那个promise
对象(也就是result.value
)。当读取文件操作完成之后,才会执行then
或catch
中的代码,在then
中result = task.next(value)
这句代码就会让之前卡住的yield readFile('config.json')
往后执行,也就是data
接收到value
的值,然后打印出来。
如果你对迭代器/生成器这块不熟的话,理解起来可能比较痛苦,建议先去补补这方面的知识。
其实,github
上已经有人提供了run
这样的容器,叫做co。所以,我们只要把注意力放在容器中的生成器里面的代码上面就可以了。
注意点
在run
容器中yield
之后所有的代码都已经是异步执行的了,所以不管yield
后面跟的是不是一个promise
对象,后面的代码都是异步的。看一个简单的例子:
1 |
|
这段代码中yield
后面跟的是一个add
函数,函数的返回值是一个数值3
,并非一个promise
对象或其他异步操作。但这段代码执行的结果是:
1 |
|
哪怕yield
后面跟的不是一个函数,直接是一个数值3
,执行的结果也是跟上面一样。
为什么?
注意在run
中,我们是通过Promise.resolve(result.value)
来处理的,result.value
就是yield
后面跟的东西。对Promise
比较熟悉的话应该知道,Promise.resolve()
传入的参数如果是一个promise
对象,那么直接返回这个对象,如果传入的不是一个promise
对象,那么会返回一个新创建的promise
对象,并且是完成状态。也就是说Promise.resolve
无论如何都会返回一个promise
对象,而只有执行了then
方法中的result = task.next(value)
这句代码之后,yield
之后的代码才会继续执行,(sum
也才会接收到传过来的值)。因为result = task.next(value)
是异步执行的,所以yield
之后的代码自然就是异步的了。
async/await
如果你看懂了上面的介绍,那么理解async/await
就很轻松了;如果你觉得上面的写法很操蛋,那么下面的写法就是一个字爽。
异步实现
先直接上代码:
1 |
|
就是这么简单!一眼看上去,跟上面第3部分的代码有些相像,只是yield
变成了await
,*
变成了async
,外面多了一个容器run
。
再对比代码的执行顺序:
1 |
|
执行结果:
1 |
|
有木有很惊讶?就连执行的顺序都跟yield实现的方式一样。而且再也不用管什么容器了,看上去更加直观。这就是所谓的用同步的代码方式去写异步的操作,借用一下老外的说法:让那些烦人的回调见鬼去吧。
虽然这里不用管什么运行容器之类的东西了,但是理解它实现的原理还是很重要的。我不知道async/await
是否可以理解成yield
实现异步的语法糖,只不过async/await
纳入ES7的标准了,而yield
的写法是我们自己实现的(比如运行容器run
就是我们自己封装的,你也可以根据需求扩展出更强大的功能来)。
最后
感谢阅读!