七、异步操作
1,异步操作概述
(1)单线程模式
单线程模型指的是,jacascrapt只在一个线程上执行。javascript同时只能执行一个任务,其他任务都必须在后面排队等待。注意,javascript只在一个线程上运行,不代表javascript引擎只有一个线程。事实上,javascript引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
javascript之所以采用单线程,而不是多线程,跟历史有关系。jacascript从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,太复杂了。如果javascript同时有两个线程,一个线程在网页DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以为了避免复杂性,javascript一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。javascript语言本身并不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
如果排队是因为计算量大,CPU忙不过来倒也正常,但是很多时候CPU是闲着的,因为IO操作(输入输出)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。javascript语言的设计者意识到,这时CPU完全可以不管IO操作,挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是javascript内部采用“事件循环”机制(event loop)。
单线程模型虽然对javascript构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,javascript程序是不会出现堵塞的,这就是为什么node可以用很少的资源,应付大流量访问的原因。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许javascript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变javascript单线程的本质。
(2)同步任务和异步任务
程序里面所有的任务,可以分成两类:同步任务和异步任务。
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一遍边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如Ajax操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,异步任务不具有“堵塞”效应。
举例来说,Ajax操作可以当做同步任务处理,也可以当做异步任务处理,由开发者决定。如果是同步任务,主线程就等着Ajax操作返回结果,再往下执行;如果是异步任务,主线程在发出Ajax请求以后,就直接往下执行,等到Ajax操作有了结果,主线程再执行对应的回调函数。
(3)任务队列和事件循环
javascript运行时,除了一个正在运动的主线程,引擎还提供一个任务队列(task queue),里面各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
javascript引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环。“事件循环是一个程序结构,用于等待和发送消息和事件。”
(4)异步操作的模式
下面总结异步操作的几种模式。
a,回调函数
回调函数是异步操作最基本的方法。
下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。
1. function f1() {
2. // ...
3. }
4.
5. function f2() {
6. // ...
7. }
8.
9. f1();
10. f2();
上面代码的问题在于,如果f1是异步操作,f2会立即执行,不会等到f1结束再执行。这时,可以考虑改写f1,把f2写成f1的回调函数。
1. function f1(callback) {
2. // ...
3. callback();
4. }
5.
6. function f2() {
7. // ...
8. }
9.
10. f1(f2);
回调函数的优点是是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合) (coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任 务只能指定一个回调函数。
b,事件监听
另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。 还是以 f1 和 f2 为例。首先,为 f1 绑定一个事件(这里采用的 jQuery 的写法)。
1. f1.on('done', f2);
上面这行代码的意思是,当 f1 发生 done 事件,就执行 f2 。然后,对 f1 进行改写:
1. function f1() {
2. setTimeout(function () {
3. // ...
4. f1.trigger('done');
5. }, 1000);
6. }
上面代码中, f1.trigger('done') 表示,执行完成后,立即触发 done 事件,从而开始执 行 f2 。 这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去 耦合”(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得 很不清晰。阅读代码的时候,很难看出主流程。
c,发布/订阅
事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发 布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么 时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察 者模式”(observer pattern)。
这个模式有多种实现,下面采用的是 Ben Alman 的 Tiny Pub/Sub,这是 jQuery 的一个插件。 首先, f2 向信号中心 jQuery 订阅 done 信号。
1. jQuery.subscribe('done', f2);
然后, f1 进行如下改写。
1. function f1() {
2. setTimeout(function () {
3. // ...
4. jQuery.publish('done');
5. }, 1000);
6. }
上面代码中, jQuery.publish('done') 的意思是, f1 执行完成后,向信号中心 jQuery 发 布 done 信号,从而引发 f2 的执行。
f2 完成执行后,可以取消订阅(unsubscribe)。
1. jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少 信号、每个信号有多少订阅者,从而监控程序的运行。
(5)异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守 这种顺序。
1. function async(arg, callback) {
2. console.log('参数为 ' + arg +' , 1秒后返回结果');
3. setTimeout(function () { callback(arg * 2); }, 1000);
4. }
上面代码的 async 函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函 数。 如果有六个这样的异步任务,需要全部完成后,才能执行最后的 final 函数。请问应该如何安排操作 流程?
1. function final(value) {
2. console.log('完成: ', value);
3. }
4.
5. async(1, function (value) {
6. async(2, function (value) {
7. async(3, function (value) {
8. async(4, function (value) {
9. async(5, function (value) {
10. async(6, final);
11. });
12. });
13. }

本文详细介绍了JavaScript中的异步操作原理,包括单线程模式、同步与异步任务的概念、事件循环机制、异步操作的不同模式及其流程控制,以及定时器的使用方法。此外,还深入探讨了Promise对象在异步编程中的应用。
最低0.47元/天 解锁文章
257

被折叠的 条评论
为什么被折叠?



