由于js是单线程的运行环境,但是为了解决线程阻塞的问题,所以就使用异步编程的方式。
首先,最传统的异步编程的方式就是使用回调函数。
回调函数其实就是一个普通函数,它的特别之处在于它的调用方式。在这里我们还需要弄清楚函数调用和回调函数的调用方式的区别。
假设有两个函数f1与f2,如果在满足一定的条件下f2里调用f1,则此调用方式为函数调用;如果把f1当成f2的参数并且在满足一样的条件下使用参数f1,则此调用方式为回调函数。
Talk is cheap,show you the code:
//这是函数调用
function f1() {
console.log('I am f1!');
}
function f2(count) {
console.log('I am f2!');
if (count > 1) {
f1();
}
}
f2(2);
//这是回调函数
function f1() {
console.log('I am f1!');
}
function f2(count,f) {
console.log('I am f2!');
if (count > 1) {
f();
}
}
f2(2,f1);
其实没必要太过区分函数调用和回调函数的区别,因为这两种方式本质都一样,并且可以相互转换,只是使用回调函数的方式由于把函数作为参数传递使得其灵活性变得更好。
正是由于回调函数可以回调的特性,我们就可以在同步代码里通过调用回调函数实现异步操作。
回调函数看似操作简便,但是多层回调就存在一个非常致命的“回调地狱”问题:
f(text1, function (count) {
if (count > 1) {
f(text2, function (count) {
if (count > 1) {
f(text3, function (count) {
//...
})
}
})
}
})
这是一种类似递归的横向嵌套回调函数扩展使得代码很容易失控,所以就引入了Promise这种更加灵活更为简便的支持异步编程的对象。
promise是一个Promise类型的对象,简单来说也是一个容器,其基本用法如下:
let promise = new Promise(function(resolve, reject) {
//code
});
它的参数resolve和reject是JavaScript本身提供的回调函数。 我们的代码仅在执行器内部。
它的返回值promise有两个属性:state(初始值为"pending")和result(初始值为undefined)。
注意:Promise内部代码只能执行一个resolve或者一个reject,一旦修改的状态确定,则后面所有的resolve和reject都会被忽略。
返回的promise对象我们无法直接操作,我们可以借助其一个重要的API:promise.then()。
promise.then(
function(result) { /* ...*/ },
function(error) { /* ... */ }
);
这个方法提供两个参数,第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的。
Promise对象还有更多的API,例如.catch()、.finally()等等,这里我不多做介绍。
下面将上面的回调函数的例子改写成Promise版本:
function f1() {
console.log('I am f1!');
}
function f2(count) {
console.log('I am f2!');
if (count > 1) {
return new Promise(resolve => resolve());
}
}
f2(2).then(f1);
也许在这里感觉用Promise和直接使用回调函数的方式差不多,那是因为这里代码逻辑简单。
更重要的一点,使用链式promise.then()的方式可以完美的解决“回调地狱”的问题:
new Promise(function(resolve, reject) {
}).then(function(result) { // (**)
}).then(function(result) { // (***)
}).then(function(result) {
});
注意:promise.then().then()与promise.then();promise.then();不一样。因为每次promise.then()都是返回一个新的promise对象。而我们所说的链式promise.then()是指第一种调用方式。
一句话描述,async/await就是Promise的语法糖,async/await与Promise相比,最明显的特征就是可以直接在同步代码里实现异步编程!
先简单介绍一下async和await:
首先,它俩都是js的关键字,async应作用于function(注:es6的class本质上也是function)并且此function总是返回一个promise对象。而await作用于一个promise对象,其效果类似于promise.then()。
注意:await若处于一个function内,则此function一定有async修饰!
Talk is cheap,show you the code:
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
function f() {
wait().then(result => console.log(result));
}
f();
运行上述代码,会在1秒后在控制台输出数字10。
此代码是Promise与async/await的综合案例,首先,在wait()函数内,由于此函数有async修饰,所以其返回的10不是Number类型而是Promise类型且此PromiseValue的值是10。正是由于wait()函数返回的是一个Promise对象,所以函数f里可以直接使用wait().then()的方式进行调用。
此代码还有另外一种更常见的编写方式:
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
async function f() {
console.log(await wait());
}
f();
Generator是一个函数,正常的函数只能返回一个值或者没有返回值,而Generator函数可以按需求一个接一个地返回多个值。
一个基本的Generator函数:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generate = generateSequence();
不难发现Generator函数与普通函数相比有两个明显的特征:*以及yeild关键字。
直接调用Generator函数并不会运行此函数,而是得到一个指向其内部状态的指针对象,真正运行Generator函数是通过调用其指针对象调用.next()方法来改变此指针对象的状态。调用.next()方法后,它将一直执行直到最接近的yield语句。 然后函数执行暂停,并将产生的值返回到外部代码。
.next()方法返回的结果有两个属性:value和done。
说道.next()方法,这不就是迭代器里面的一个重要元素吗?没错,其实Generator就是迭代器。所以Generator的最主要作用就是用于流程管理。
Generator函数本身单独并不能实现异步编程,通过yield可以暂停函数执行的特性可以将Generator函数里的Promise对象用yield修饰,这样每次进行Generator函数的迭代我们都能得到Promise对象然后进行我们自己的异步逻辑并且还能暂停函数的执行。
下面给出阮一峰教程的一个用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);
});
首先我们看一个不含异步操作的代码:
function delay() {
console.log('延迟2秒');
}
function sequence() {
console.log('函数开始');
setTimeout(delay, 2000);
console.log('函数结束');
}
sequence();
运行这段代码,结果显示为:函数开始,函数结束,延迟2秒。
现在,我们想要上面介绍过的四种方式来实现异步操作,使得其结果为:函数开始,延迟2秒,函数结束。
首先,使用回调函数的方式:
function callbackDelay(delay) {
delay();
console.log('函数结束');
}
//callback
function callbackSequence() {
console.log('函数开始');
setTimeout(() => callbackDelay(delay), 2000);
}
callbackDelay();
此回调的条件就是延迟两秒后再调用回调函数。
使用Promise的方式:
//promise
function promiseSequence() {
console.log('函数开始');
return new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
}
let promise = promiseSequence();
promise.then(() => console.log('函数结束'));
将函数结束封装在promise.then()内可确保此操作一定在延迟之后才会进行。
使用async/await的方式:
//async/await
async function asyncSequence() {
console.log('函数开始');
await new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
console.log('函数结束');
}
asyncSequence();
不愧是在同步代码里实现异步,简简单单只需要在同步代码逻辑上添加上相应的关键字即可。
最后,再看看Generator的方式:
//generator
async function* generateSequence() {
console.log('函数开始');
yield await new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
console.log('函数结束');
}
let generate = generateSequence();
for await(let value of generate) {
}
乍一看,像是在async/await的基础上进行的画蛇添足,谁让Generator是迭代器,对于只有单个promise的迭代用上Generator肯定是大材小用了。
此篇文章主要是讲述了callback,promise,async/await,generator这四种常见的异步编程的基本知识和彼此之间的使用关联,而它们里面所包含的细节知识点远远不止于此,更多的详细内容可以参看我下面给出的两个链接。
阮一峰es6教程:https://es6.ruanyifeng.com/
javascript.info教程:https://javascript.info/