这篇文章主要是梳理一下本身对阮一峰大神写的关于async/await
文章,有写得不对的地方以及理解得不对的地方,各位大佬请指错!promise
简单对比传统异步
,promise异步
,async异步
异步
下文都会以setTimeout
来进行异步展现,方便理解。async
传统的回调函数
setTimeout(callback,1000); function callback(){ console.log("拿到结果了!"); }
setTimeout
函数传入了两个参数(1000
/callback
),setTimeout
被调用的时候,主线程不会等待1秒,而是先执行别的任务。callback
这个函数就是一个回调函数,即当1秒后,主线程会从新调用callback
(这里也再也不啰嗦去说event Loop方面的知识了
);oop
那么,当咱们异步函数须要嵌套的时候呢。好比这种状况:性能
setTimeout(function(){ console.log("第一个异步回调了!") setTimeout(function(){ console.log("第二个异步回调了!") setTimeout(function(){ console.log("第三个异步回调了!") setTimeout(function(){ console.log("第四个异步回调了!") setTimeout(function(){ console.log("第五个异步回调了!") },1000); },1000); },1000); },1000); },1000);
OK,想死不?线程
咱们用promise来处理设计
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } timeout(2000) .then(value => { console.log("第一层" + value); return timeout(2000); }) .then(value => { console.log("第二层" + value); return timeout(2000); }) .then(value => { console.log("第三层" + value); return timeout(2000); }) .then(value => { console.log("第四层" + value); return timeout(2000); }) .then(value => { console.log("第五层" + value); return timeout(2000); }) .catch(err => { console.log(err); });
OK,好看点了!指针
可是仍是不方便!code
咱们用async/await来处理:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } async function asyncTimeSys(){ await timeout(1000); console.log("第一层异步结束!") await timeout(1000); console.log("第二层异步结束!") await timeout(1000); console.log("第三层异步结束!") await timeout(1000); console.log("第四层异步结束!") await timeout(1000); console.log("第五层异步结束!") return "all finish"; } asyncTimeSys().then((value)=>{ console.log(value); });
OK,舒服了!
在这个asyncTimeSys
函数里面,全部的异步操做,写的跟同步函数没有什么两样!
async
函数究竟是什么?其实他就是Genarator函数(生成器函数)的语法糖而已!
- 内置执行器。
Generator 函数的执行必须靠执行器,因此才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数如出一辙。彻底不像 Generator 函数,须要调用next方法,或者用co模块,才能真正执行,获得最后结果。
- 更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。
- 更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,能够是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)。
- 返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用then方法指定下一步的操做。
进一步说,async函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
其实,async函数就是一个由Generator封装的异步环境,其内部是经过交换函数执行权,以及thunk函数来实现的!
OK,咱们简单的封装一个:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function *times(){ let result =yield timeout(1000); return "second next" } let gen = times(); //拿到生成器函数,gen能够理解为指针 let firstYield = gen.next(); //firstYield此时为gen指针指向的第一个yield右边的表达式,此时timeout(1000)被执行 console.log(firstYield); // firstYield = {value:Pomise,done:false}; //接下来就是将firstYield中的value里的promise拿出来,做为正常的Promise调用,以下: firstYield.value.then(()=>{ //当timeout异步结束以后,执行如下代码,再将gen指针执行下一个yield,因为如下没有yield了,因此gen.next()的value为return里的东西 console.log("timeout finish"); console.log(gen.next()); //{value: "second next", done: true} }).catch((err)=>{ });
这样封装有什么用呢,yield返回回来的东西,仍是得像promise那样调用。
咱们先来看看同步的代码,先让它长得像async和await那样子:
function* times() { yield console.log(1); yield console.log(2); yield console.log(3); return "second next"; } let gen = times(); let result = gen.next(); while (!result.done) { result = gen.next(); }
OK,很是像了,可是,这是同步的。异步请求必须得等到第一个yield执行完成以后,才能去执行第二个yield。咱们若是改为异步,确定会形成无限循环。
那么,times生成器里面若是都是异步的话,咱们应该怎么调用呢?
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function *times(){ yield timeout(2000); yield timeout(2000); yield timeout(2000); return "finish all!"; } let gen = times(); let gen1 = gen.next(); gen1.value.then(function(data){ console.log(data+" one"); let gen2 = gen.next(); gen2.value.then(function(data){ console.log(data+" two"); let gen3 = gen.next(); gen3.value.then(function(data){ console.log(data+" three"); }) }) });
仔细观察能够发现,其实每个value的.then()
方法都会传入一个相同的回调函数,这意味着咱们可使用递归来流程化管理整个异步流程;
改造一下这个上边的代码;
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function* times() { yield timeout(2000); yield timeout(2000); yield timeout(2000); return "finish all!"; } function run(fn){ let gen = fn(); function next(){ console.log("finish"); let result = gen.next(); if(result.done) return; result.value.then(next); } next(); } run(times);
OK,如今咱们可使用run
函数,使得生成器函数times
里的异步请求,一步接着一步往下执行。
那么,这个run
函数里边的next究竟是什么呢,它实际上是一个thunk函数
;
Thunk函数的诞生是源于一个编译器设计的问题:求值策略,即函数的参数到底应该什么时候求值。
看下边的代码,请思考何时进行求值:
var x = 1; function f(m) { return m * 2; } f(x + 5);
试问:x+5
这个表达式应该何时求值
x+5
的值,再将这个值(6
)传入函数f
,例如c语言,这种作法的好处是实现比较简单,可是有可能会形成性能损失。x+5
)传入函数体,只在用到它的时候求值。OK,thunk
函数到底是什么:
编译器的传名调用实现,每每就是将参数放到一个临时函数之中,再将这个临时函数转入函数体,这个临时函数就叫作Thunk函数。
将上边的代码进行改造:
var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
js中的传名调用是什么呢,与真正的thunk
有什么区别呢?
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不一样。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。
网上对于thunk
的演示都是使用的fs
模块的readFile
方法来进行演示
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback); // Thunk版本的readFile(单参数版本) var Thunk = function (fileName) { return function (callback) { return fs.readFile(fileName, callback); }; }; var readFileThunk = Thunk(fileName); readFileThunk(callback);
其实,任何函数,只要参数有回调函数,就能写成Thunk
函数的形式。下面是一个简单的Thunk
函数转换器。
让咱们用setTimeout
来进行一次演示:
//正常版本的setTimeout; setTimeout(function(data){ console.log(data); },1000,"finish"); //thunk版本的setTimeout let thunk = function(time){ return function(callback){ return setTimeout(callback,time,"finish"); } } let setTimeoutChunk = thunk(1000); setTimeoutChunk(function(data){ console.log(data); });
如今回头看一看用Generator函数封装异步请求
这一节中最后一个实例中,咱们封装的timeout
函数,他其实就是一个thunk
函数,我在那一节中没有给你们说明这一条:
为何Generator
里面必须使用thunk
函数呢,由于咱们须要确保传入的值只有一个,利用其回调函数,来进行递归自动控制Generator
函数的流程,接收和交还程序的执行权;