刚开始学习JavaScript编程时,你可能就已经知道,JavaScript是单线程(Single Thread)执行的。单线程的意思是一次只能执行一个方法,只有等一个方法返回才会去执行另一个方法。winform编程时如果UI线程中等待的话便会造成UI假死,但是在Web编程中没有线程的概念,也就是说如果代码等待则UI便会卡死。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
同步模式便是上述所说的UI卡死的情况;
异步模式的话最普遍的例子就是Ajax方法。
本文总结一些JavaScript中异步编程的方法,如若文中有错误或者有其他异步方法,望不吝赐教。
在下文的示例中会使用setTimeout模拟耗时操作。
1、 回调方法(Callback)
回调是我刚开始接触JavaScript异步时使用的方法,相信也是大多数人的体验。
var x = 0;
function f1(callback) {
setTimeout(function() {
x += 1;
callback();
}, 100);
}
function f2() {
alert(x);
}
f1(f2);
使用回调方法大致就是f1(f2)这样的情况,但是当回调的层级过多的时候代码便会呈现金字塔结构,使代码变得难看并且不易理解和维护,重构的过程也会充满各种陷阱。优点是这种方式会最原始最直接的,目前所有浏览器都能支持,所以也有很多人依然在用这种方式。
2、 事件监听
为了方便演示,下面示例使用JQuery的写法。
var x = 0;
f1.on('done', f2);
function f1(){
setTimeout(function () {
x += 1;
f1.trigger('done');
}, 1000);
}
function f2() {
alert(x);
}
f1();
通过f1.on(‘done’, f2)添加事件监听,当执行f1()方法,trigger触发’done’事件,此时事件被捕捉并响应,执行f2方法。
使用事件监听机制可以避免回调方法的多重嵌套,使代码扁平化。但是使整个程序变成事件驱动模式,使流程更加不清晰(你有时很难看懂执行那个方法后触发事件跳转到另外的方法)。而且会破坏方法的原子性。
3、观察者模式
观察者模式与事件监听机制类似,下面示例同样使用JQuery的写法。
var x = 0;
jQuery.subscribe('done', f2);
function f1(){
setTimeout(function () {
x += 1;
jQuery.publish('done');
}, 1000);
}
function f2() {
alert(x);
jQuery.unsubscribe("done", f2);
}
f1();
可以看到,代码上跟事件监听的差异不大,差别只是
f1.on(‘done’, f2)变成了jQuery.subscribe(‘done’, f2),
f1.trigger(‘done’))变成了jQuery.publish(‘done’)。
实际上前者使用的是监听,而后者更类似于通知。与前者相比的话优点是通过信号来统一控制。
4、Promise对象
ES6 原生提供了 Promise 对象。
所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
ES6的Promise来源于Promise/A+。使用Promise来进行异步流程控制,有几个需要注意的问题,
在We have a problem with promises一文中有很好的总结。
Promise 对象有以下两个特点。
(1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 方法和 reject 方法。
如果异步操作成功,则用 resolve 方法将 Promise 对象的状态,从「未完成」变为「成功」(即从 pending 变为 resolved);
如果异步操作失败,则用 reject 方法将 Promise 对象的状态,从「未完成」变为「失败」(即从 pending 变为 rejected)。
更多关于Promise的特性,可以参考:阮一峰ECMAScript 6 入门
5、Generator方法
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
function* gen(x){
var y = yield x + 2;
return y;
}
上面代码就是一个 Generator 函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下。
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器 )g 。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield 语句,上例是执行到 x + 2 为止。
换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
更多关于Generator的特性,可以参考:Generator 函数的含义与用法
5、async/await
async/await是ES7引进的新特性,即async function和await关键字,目前ES7由于浏览器支持等原因还未能投入生产环境,我们也只能暂时了解一下。
以下是async/await的一个简单示例:
async function testFun() {
let res, a, b, c, d;
try {
res = await f1(a, b);
res = await f2(c, res);
res = await f3(d);
return res;
} catch (err) {
return handleError(err);
}
}
testFun();
代码跟Promise&Generator实现类似,此处不加以赘述。
以上就是目前JavaScript中实现异步的一些方法,有些直接复制他人的博文,参考的文章也均已给出,里面有更加详细的介绍。