JavaScript异步编程

什么异步?

异步,是相对于同步而言的。同步是指多个任务一个个按顺序去完成,异步则是将比较耗时的任务放到其他地方执行,接着执行当前任务列表中的任务,当耗时任务执行完毕,再将该任务的回调添加到当前任务列表中。举个栗子:

很多人去寄快递,然后快递员说:“排好队,一个个来”。人们就排好队,一个个跟快递员说明自己的需求,这就是同步。而异步呢,就是当排队寄快递的某个人,由于寄的东西比较多,也没整理好,快递员就跟他说:“你先把要寄送的东西整理下,给你个快递箱,整理好你告诉我”。然后那个人就到一旁整理自己寄送的物品,其他人继续跟快递员沟通,寄快递。当那个人整理好自己寄送的物品后,就跟快递员说:“整理好了”(回调)。快递员处理完手上的工作就立刻帮那个人寄快递,这就是异步。

异步,就是当任务A被执行,由于任务A需要执行相当长的时间,则把任务A放在其他地方(事件队列)执行,然后先执行后续的任务B,任务C,任务D等,当任务A执行完毕时,会将任务A的一个回调或多个回调插入到当前任务列表中,可能是任务B后面,任务C后面或任务D后面,这取决于任务A执行完毕的时间。

异步与同步的优缺点

同步
优点:更符合我们的思考模式,代码的先后顺序也是程序执行的先后顺序,确保一个任务接着一个任务执行,上一个任务未执行完成,下一个任务将不会开始执行。
缺点:当某个任务耗时很长时,后面的任务必须排队等着,会拖延整个程序的执行,常见的就是浏览器卡死。

异步
优点:当某个任务被“调用”后不会等它执行结束再执行它后面的代码,而是调用之后直接往下执行,异步函数的“执行”实际上是放在“其他地方”,待“执行”完成后再把结果通过回调函数来进行进一步的使用或处理,这样也就不会导致程序卡死。
缺点:异步编程的代码可读性较差(通过近些年的一些方法,可以像写同步代码一样实现异步),而且在早期的编码风格中,异步经常导致回调地狱(不断的回调嵌套)。

异步编程的六种方法

回调函数

回调函数是异步编程最简单,也是最原始的方法。

1
2
3
4
5
6
7
8
9
10
11
var callback = function () {
console.log('This is a callback function')
}
var fn = function (callback) {
setTimeout(() => {
callback()
}, 1000)
}
fn(callback)

上述代码中,callback需要等待fn执行后的结果,所以将callback传入到fn中,在fn执行完成后,立即调用callback。
这看上去很简单,但是在很多业务场景中,可能会很复杂,例如任务B等待任务A执行的结果,任务C等待任务B执行的结果。。。
可以得到以下代码:

1
2
3
4
5
6
7
setTimeout(function A () {
setTimeout(function B () {
setTimeout(function C() {
// ...
})
})
})

这样就会导致回调地狱,不仅代码耦合性高,流程混乱,而且阅读性差。

事件监听

事件监听,也就是一种事件驱动的模式,任务的执行不取决于代码顺序,而是取决于事件触发的时刻。
下面是一个常见的栗子(jQuery写法):

1
$('#test').on('click', fn)

上述代码中,当元素被点击后,立即执行fn函数,这也是一种异步编程的形式。这种方式在日常中用得也多,也很方便,容易理解。但如果大量使用的话,那么整个程序将变成事件驱动模式,运行流程不太清晰

发布/订阅模式

发布/订阅模式,其实与事件监听有点类似,但优于事件监听模式。
发布/订阅模式,会有个消息中心,当一个任务完成时,会向消息中心发送一个“已执行完毕”的消息,其他任何订阅了这个消息的任务就会开始执行

1
2
3
4
5
6
7
msgCenter.subscribe('sendMsg', f2)
function f1 () {
setTimeout(() => {
msgCenter.publish('sendMsg')
})
}

上述代码中,任务f2向消息中心订阅了’sendMsg’的消息,而当f1执行后,会发送’sendMsg’消息,f2收到’sendMsg’消息后,立即开始执行。

发布/订阅模式能够通过消息中心很好地管理消息与消息订阅者,但是此方法需要借助第三方库,或者自主实现,相对较复杂。

Promise 对象

Promise 对象是一个具有then方法的对象,同时也被称为thenable对象。在Promise编程中,每一个异步任务都会返回一个Promise对象,该对象具有then方法,允许指定回调函数。Promise 对象只有三种状态:pending、fulfilled、rejected,三种状态中,只能由pending转换为fulfilled或rejected,此过程不可逆,同时fulfilled与rejected也不可以相互转换。

ES6将Promise纳入了标准,不再需要第三方库来实现Promise,下面介绍Promise在ES6中的运用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MDN示例
var myFirstPromise = new Promise(function(resolve, reject){
//当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
//在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
setTimeout(function(){
resolve("成功!"); //代码正常执行!
}, 250);
});
myFirstPromise.then(function(successMessage){
//successMessage的值是上面调用resolve(...)方法传入的值.
//successMessage参数不一定非要是字符串类型,这里只是举个例子
console.log("Yay! " + successMessage);
});

上述就是ES6中Promise的运用,这只是简单的例子,在MDN中还有更多关于Promise的介绍和用法。

Generator 生成器

Generator 生成器,这是ES6中的新特性。

生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
下面结合代码来理解下:

1
2
3
4
5
6
7
8
9
10
function* getNum() {
yield 1;
yield 2;
yield 3;
}
//调用生成器,生成一个可迭代的对象
const gen = getNum();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: true}

生成器在形式上与普通函数差别不大,主要使用 function* 的形式,同时在生成器中还拥有 yield 关键字。生成器最大的特点是:当代码执行后,遇到 yield 关键字时会暂停,当调用生成器生成的对象的next()方法时,就继续执行。这也就是说,生成器可以将函数的执行权交出。

接下来看下,生成器 Generator如何实现异步编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 代码中的定时器用于模拟异步请求,可替换成相应的ajax代码
function getFirstName () {
setTimeout(() => {
gen.next('hello');
},2000);
}
function getLastName () {
setTimeout(() => {
gen.next('world');
},1000);
}
function* getFullName () {
let firstName = yield getFirstName()
let lastName = yield getLastName()
console.log(firstName + lastName)
}
var gen = getFullName()
gen.next() // 立即打印:{value: undefined, done: false}
// 几秒后打印:helloworld

可以看到,上述代码代码能够让我们像书写同步代码一样写异步代码,而且不用再写回调函数。这是一种非常不错的异步编程的方式。

async/await

这是ES7中提出异步函数,返回一个Promise对象。async/await其实是对Promise和Generator的封装,也就是一种语法糖。

1
2
3
4
5
6
7
8
9
async function add(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add(10).then(v => {
console.log(v); // 4s后输出60
});

异步函数可能会包括 await 表达式,这将会使异步函数暂停执行并等待 promise 解析传值后,继续执行异步函数并返回解析值。
也就是说,使用 async 声明的函数,如果函数体内有 await 关键字,那么将等待 await 后的函数传回Promise对象,才继续往下执行。

async/await 形式的异步编程,具有更好的语义,更广的适用性(async函数的await命令后面,可以是Promise对象和原始类型的值)。这也是目前异步编程最好的一种形式。

参考链接:
阮一峰:Javascript异步编程的4种方法
Promise In ES6
前端的异步解决方案

坚持原创技术分享,您的支持将鼓励我继续创作!