JS异步编程
JavaScript
语言的一大特点就是单线程,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
HTML5
提出Web Worker标准,允许JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。所以,这个新标准并没有改变JavaScript
单线程的本质。
相关概念
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务指的是,不进入主线程、而进入”任务队列”(
task queue
)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
主线程:
JS
只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
任务队列 ,异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同
事件循环,
JS
会创建一个类似于while (true)
的循环,每执行一次循环体的过程称之为Tick
。每次Tick
的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick
会查看任务队列中是否有需要执行的任务。
异步执行的运行机制
所有同步任务都在主线程上执行,形成一个执行栈(
execution context stack
)主线程之外,还存在一个”任务队列”(
task queue
)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
常见的异步任务
onclick
等事件 由浏览器内核的DOM Binding
模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。setTimeout
会由浏览器内核的timer
模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中
setTimeout(function(){
console.log('1');
},0);
console.log('2');
这段代码的输出为 2 1
,因为setTimeout
是一个异步的操作,当时间到达之后,回调函数会被添加到任务队列中。主线程中的所有任务结束之后,才会去参看任务队列中的任务,输出 1
ajax
则会由浏览器内核的network
模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。
异步编程的四种方法
除了onclick等事件、ajax请求这些天生的异步任务,有一些函数本身很耗时,其他函数依赖这个函数的执行结果,这时我们就要手动进行异步编程了。
以下我们假设:两个函数f1
和f2
,f1
很耗时,f2
需要等待f1
的执行结果。
回调函数
这是异步编程最基本的方法。
我们使用setTimeout
函数将f1
变成了异步操作,不会程序运行。f1
运行结束后,再运行f2
。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
f1(f2);
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱(不能直接看出f1
和f2
之间的依赖关系)
事件监听
任务的执行不取决于代码的顺序,而取决于某个事件是否发生
f1.trigger('done')
表示,执行完成后,立即触发done
事件,从而开始执行f2
。
f1.on('done', f2);
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
发布/订阅
我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(
publish
)一个信号,其他任务可以向信号中心”订阅”(subscribe
)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern
),又称”观察者模式”(observer pattern
)
f1
运行结束之后,像“消息中心”发出done
信号,因f2
之前向“消息中心”订阅了done
信号,所以,此时f2
开始执行。
jQuery.subscribe("done", f2);
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promises对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
每一个异步任务返回一个Promise
对象
function f1(){
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
dfd.resolve();
}, 500);
return dfd.promise;
}
f1().then(f2).then(f3);
//指定失败的回调函数
f1().then(f2).fail(f3);
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚。而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。
上面的四种方法,都是通过setTimeout
函数使得一个耗时的普通函数变成一个异步操作,不同之处在于通过不同的方式指定异步操作运行结束后的回调函数,比如,通过事件监听、发布订阅模式、promise
等。各有优缺点哦!
最后,关于异步编程,ES6
种介绍Generator
函数、async
函数、Promise
对象。由兴趣的同学可以研读 阮大神的书
参看文献