异步函数这个术语有点名不副实,调用一个函数后,程序只在该函数返回后才能继续。Javascript程序员如果称一个函数为异步的,其意思就是这个函数会导致将来再运行另一个函数,后者取自于事件队列。如果后面这个函数是作为参数传递给前者的,则称其为回调函数。
回调函数是异步编程最基本的方式。
采用这种方式,我们把同步操作变成了异步操作,主函数不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护。
我们定义一个delay函数,它是异步的,也就是说它会拖延指定函数的执行,从而使现在正在执行的程序继续执行。delay函数如下:
function delay(time, callback) {
setTimeout(function () {
callback("Slept for "+time);
}, time);
}
那么如果我要delay两次呢?
delay(1000, function(msg) {
console.log(msg);
delay(1200, function (msg) {
console.log(msg);
}
})
//...waits 1000ms
// > "Slept for 1000"
//...waits another 1200ms
// > "Slept for 1200"
只有这样我们才能够确保两个delay一个接一个的执行。如果层次多了呢?就会形成回调地狱。当异步任务很多时,维护大量的callback将是一场灾难。
Promise是一个被纳入ES6中的规范,各大框架也早已实现相关方法。
Promise可以理解为一个承诺,如果A调用B,B返回一个承诺给A,然后A就可以在写计划的时候这么写,当B返回结果的时候,A就执行方案1,如果B没有返回A要的结果,A就执行方案2。这样一来,所有的潜在风险就都在A的可控范围之内了。
我们看看ES6中promise的用法示例:
'use strict';
var promiseCount = 0;
function testPromise() {
var thisPromiseCount = ++promiseCount;
var log = document.getElementById('log');
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Started (<small>Sync code started</small>)<br/>');
// 我们创建一个新的Promise,期望3秒后得到结果
var p1 = new Promise(
//当Promise解决或拒绝时该函数被调用
function(resolve, reject) {
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Promise started (<small>Async code started</small>)<br/>');
// 创建异步操作
window.setTimeout(
function() {
// 满足Promise
resolve(thisPromiseCount);
}, Math.random() * 2000 + 1000);
});
// 当Promise被满足时执行
p1.then(
// 输出信息和值
function(val) {
log.insertAdjacentHTML('beforeend', val +') Promise fulfilled (<small>Async code terminated</small>)<br/>');
})
.catch(
// 当Promise被拒绝时执行
function(reason) {
console.log('Handle rejected promise ('+reason+') here.');
});
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Promise made (<small>Sync code terminated</small>)<br/>');
}
说明1,2异步操作后正常顺序执行完毕。更多Promise的详细用法请参考:MDN
很多框架也提供了Promise相关方法,这里我们以jquery为例。
$("button").bind( "click", function() {
$("p").append( "Started...");
$("div").each(function( i ) {
$( this ).fadeIn().fadeOut( 1000 * (i+1) );
});
$( "div" ).promise().done(function() {
$( "p" ).append( " Finished! " );
});
});
可以看到,当$(“div”)的所有任务执行完毕后,就会调用最后的done操作。
Jquery中的Promise也可以代表多种结果,出现不同结果时会分别调用相应的回调。我们以ajax调用为例。1.5之前版本中,代码必须写成这样:
$.get(‘/getdata’,{
success:onSuccess,
failure:onFailure,
always:onAlways
});
而1.5+版本引入了Promise对象后。可以写成如下形式:
var promise = $.get(‘/getdata’);
promise.done(onSuccess);
promise.fail(onFailure);
promise.always(onAlways);
那么这种变化有什么好处呢?为什么非要在触发ajax调用之后再附加回调呢?如果ajax要实现很多效果,比如触发动画、插入html、锁定输入等,那么仅仅由负责发出请求那部分应用代码来处理所有这些效果,显然很蠢。只传递Promise就显得很优雅。
更多详细请参考:jquery
Promise虽然是很优雅,但是也只是解决了回调地狱的问题,真正简化javascript异步编程的还是Generator。
生成器是ES6中的语法。
何为生成器?让我们先看看以下代码:
function* quips(name) {
yield "hello " + name + "!";
yield "i hope you are enjoying the blog posts";
if (name.startsWith("X")) {
yield "it's cool how your name starts with X, " + name;
}
yield "see you later!";
}
你没有看错,这就是javascript代码。是不是和你曾经认识的javascript不太一样。这个函数就叫做生成器函数。生成器函数看起来和普通的函数是不是有点相像呢?
它们的区别如下:
我们看看next方法的使用:
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "hello jorendorff!", done: false }
> iter.next()
{ value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
{ value: "see you later!", done: false }
> iter.next()
{ value: undefined, done: true }
可以看到,每一次next方法后,生成器函数就执行到下一个yield位置处。
下面我们讲解如何通过生成器函数来取代回调函数。我们还是以一开始的多次延迟delay为例。
首先,我们需要定义一个生成器:
function* myDelayedMessages() {
/* delay 1000 ms and print the result */
/* delay 1200 ms and print the result */
}
然后我们需要设置delay时间来执行指定操作,我们现在暂定为空函数。
function* myDelayedMessages() {
console.log(delay(1000, function(){}));
console.log(delay(1200, function(){}));
}
然后我们使用yield关键字:
function* myDelayedMessages() {
console.log(yield delay(1000, function(){}));
console.log(yield delay(1200, function(){}));
}
然后我们就需要指定一个函数run来调用生成器函数的next方法,并且将空函数改为参数resume:
function run(generatorFunction) {
var generatorItr = generatorFunction(resume);
function resume(callbackValue) {
generatorItr.next(callbackValue);
}
generatorItr.next()
}
最后执行代码如下:
run(function* myDelayedMessages(resume) { console.log(yield delay(1000, resume)); console.log(yield delay(1200, resume)); })
//...waits 1000ms
// > "Slept for 1000"
//...waits 1200ms
// > "Slept for 1200"
这样就完美的避免了回调地狱噢!