异步编程的方法
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
Promise
Promise 实际上是利用编程技巧将回调函数改成链式调用,避免回调地狱。最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
Generator函数
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try ... catch 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
Generator 函数的用法
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
Thunk 函数的含义和用法
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function (x) {
return x + 5;
};
function f(thunk){
return thunk(2) * 2;
}
这就是 Thunk 函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式。
Thunk函数的自动流程管理
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
上面代码的 run 函数,就是一个 Generator 函数的自动执行器。内部的 next 函数就是 Thunk 的回调函数。 next 函数先将指针移到 Generator 函数的下一步(gen.next 方法),然后判断 Generator 函数是否结束(result.done 属性),如果没结束,就将 next 函数再传入 Thunk 函数(result.value 属性),否则就直接退出。
有了这个执行器,执行 Generator 函数方便多了。不管有多少个异步操作,直接传入 run 函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在 yield 命令后面的必须是 Thunk 函数。
co函数库
co 函数库可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
})
co函数库原理
为什么 co 可以自动执行 Generator 函数?
Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。 使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。
next函数的内部代码中,只有实现以下四种逻辑的命令
-
第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
-
第二行,确保每一步的返回值,是 Promise 对象。
-
第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
-
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
async函数
一句话,async 函数就是 Generator 函数的语法糖。
一个generator函数:
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
写成 async 函数,就是下面这样。
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。
async 函数的优点
async 函数对 Generator 函数的改进,体现在以下三点。
(1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
async 函数的实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。
async 函数的用法
同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}
await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果确实希望多个请求并发执行,可以使用 Promise.all 方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
几个理解async/await和事件循环机制的面试题
// 第一题
async function async1() {
console.log('async1');
}
async function async2() {
await async1();
console.log(2);
}
setTimeout(() => {
console.log(4)
}, 0)
async2();
console.log(1);
Promise.resolve().then(() => {
console.log(3);
})
输出结果依次为:async1 -> 1 -> 2 -> 3 -> 4。
执行过程:执行async1函数,执行await async1(),打印async。执行完以后,async1没有返回值,因为async函数返回的是Promise,此时,相当于返回Promise.resolve(undefined)。Promise进入微任务队列,await等待并且会阻塞当前async函数中在它之后的代码。而会去执行函数外的第一轮宏任务代码,setTimeout进入宏任务队列,Promise进入微任务队列。然后输出1,此时第一轮宏任务代码执行结束。此时执行上边那个微任务,执行完以后,打印2。然后打印3,最后打印setTimeout中的4.
// 第二题
Promise.resolve().then(() => {
console.log('promise1');
})
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise2');
resolve();
}).then(function() {
console.log('promise3');
});
console.log('script end');
输出结果依次为:script start -> async1 start -> async2 -> promise2 -> script end -> promise1 -> async1 end -> promise3 -> setTimeout
// 第三题
async function test1() {
console.log('start test1');
console.log(await test2());
console.log('end test1');
}
async function test2() {
console.log('test2');
let x = await 'return test2 value';
console.log("pass");
return x;
}
test1();
console.log('start async');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('end async');
输出结果依次为:start test1 -> test2 -> start async -> promise1 -> end async -> pass -> promise2 -> return test2 value -> end test1 -> setTimeout
参考文章
本来大部分内容转自以下文章: