Javascript异步编程详解

开发 前端
Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)."异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

前言

你可能知道,Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous).

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,***的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

setTimeout 函数的弊端

延时处理当然少不了 setTimeout这个神器,很多人对 setTimeout函数的理解就是:延时为 n 的话,函数会在 n 毫秒之后执行。事实上并非如此,这里存在三个问题:

一个是 setTimeout函数的及时性问题, setTimeout是存在一定时间间隔的,并不是设定 n 毫秒执行,他就是 n 毫秒执行,可能会有一点时间的延迟,setInterval 和 setTimeout 函数运转的最短周期是 5ms 左右,这个数值在 HTML规范 中也是有提到的:

  • Let timeout be the second method argument, or zero if the argument was omitted.如果 timeout 参数没有写,默认为 0
  • If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 如果嵌套的层次大于 5 ,并且 timeout 设置的数值小于 4 则直接取 4.

其次是while循环会阻塞setTimeout的执行

看这段代码:

  1. var t = true
  2.  
  3. window.setTimeout(function (){ 
  4.     t = false
  5. },1000); 
  6.  
  7. while (t){} 
  8.  
  9. alert('end');  

结果是死循环导致setTimeout不执行,也导致alert不执行

js是单线程,所以会先执行while(t){}再alert,但这个循环体是死循环,所以永远不会执行alert。

至于说为什么不执行setTimeout,是因为js的工作机制是:当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout是异步代码,所以setTimeout只能等js空闲才会执行,但死循环是永远不会空闲的,所以setTimeout也永远不会执行。

第三是,try..catch捕捉不到他的错误

异步编程方法

回调函数

这是异步编程最基本的方法。

假定有两个函数f1和f2,后者等待前者的执行结果。

  1. function f1(callback){ 
  2.   setTimeout(function () { 
  3.     // f1的任务代码 
  4.     callback(); 
  5.   }, 1000); 
  6. f1(f2);  

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生

  1. f1.on('done', f2); 
  2. function f1(){ 
  3.   setTimeout(function () { 
  4.     // f1的任务代码 
  5.     f1.trigger('done'); 
  6.   }, 1000); 
  7.  

JS 和 浏览器提供的原生方法基本都是基于事件触发机制的,耦合度很低,不过事件不能得到流程控制

Promises对象

Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。

Promises可以简单理解为一个事务,这个事务存在三种状态:

  • 已经完成了 resolved
  • 因为某种原因被中断了 rejected
  • 还在等待上一个事务结束 pending

简单说,它的思想是,每一个异步任务返回一个Promises对象,该对象有一个then方法,允许指定回调函数,这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚

Promises就是一个事务的管理器。他的作用就是将各种内嵌回调的事务用流水形式表达,其目的是为了简化编程,让代码逻辑更加清晰。

Promises可以分为:

  • 无错误传递的 Promises,也就是事务不会因为任何原因中断,事务队列中的事项都会被依次处理,此过程中 Promises只有pending和 resolved两种状态,没有 rejected状态。
  • 包含错误的 Promises,每个事务的处理都必须使用容错机制来获取结果,一旦出错,就会将错误信息传递给下一个事务,如果错误信息会影响下一个事务,则下一个事务也会 rejected,如果不会,下一个事务可以正常执行,依次类推。

此处留坑讲generator实现异步编程

封装好的实现

jquery的Deferred对象

简单说,Deferred对象就是jquery的回调函数解决方案。在英语中,defer的意思是"延迟",所以Deferred对象的含义就是"延迟"到未来某个点再执行。

首先,回顾一下jquery的ajax操作的传统写法:

  1. $.ajax({ 
  2.     url: "test.html"
  3.     success: function(){ 
  4.       alert("哈哈,成功了!"); 
  5.     }, 
  6.     error:function(){ 
  7.       alert("出错啦!"); 
  8.     } 
  9.   });  

有了Deferred对象以后,写法是这样的:

  1. $.ajax("test.html"
  2.  .done(function(){ alert("哈哈,成功了!"); }) 
  3.  .fail(function(){ alert("出错啦!"); });  

可以看到,done()相当于success方法,fail()相当于error方法。采用链式写法以后,代码的可读性大大提高。

了解jQuery.Deferred对象可以看下面这个表格。

 when.js

AngularJS内置的Kris Kowal的Q框架,和cujoJS的when.js,两者都是Promises/A规范的实现

when.js实例

  1. var getData = function() { 
  2.     var deferred = when.defer(); 
  3.  
  4.     $.getJSON(api, function(data){ 
  5.         deferred.resolve(data[0]); 
  6.     }); 
  7.  
  8.     return deferred.promise; 
  9.  
  10. var getImg = function(src) { 
  11.     var deferred = when.defer(); 
  12.  
  13.     var img = new Image(); 
  14.  
  15.     img.onload = function() { 
  16.         deferred.resolve(img); 
  17.     }; 
  18.  
  19.     img.src = src; 
  20.  
  21.     return deferred.promise; 
  22.  
  23. var showImg = function(img) { 
  24.     $(img).appendTo($('#container')); 
  25.  
  26. getData() 
  27. .then(getImg) 
  28. .then(showImg);  

看***三行代码,是不是一目了然,非常的语义化

  1. var deferred = when.defer(); 

定义了一个deferred对象。

  1. deferred.resolve(data); 

在异步获取数据完成时,把数据作为参数,调用deferred对象的resolve方法。

  1. return deferred.promise; 

返回了deferred对象的Promises属性。

此处留坑讲之前用过的step.js

扩展阅读

Javascript既是单线程又是异步的,请问这二者是否冲突,以及有什么区别?

Answer1:Javascript本身是单线程的,并没有异步的特性。

由于 Javascript的运用场景是浏览器,浏览器本身是典型的 GUI 工作线程,GUI 工作线程在绝大多数系统中都实现为事件处理,避免阻塞交互,因此产生了 Javascript异步基因。此后种种都源于此。

Answer2: JS的单线程是指一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行(你可以使用IE的标签式浏览试试看效果,这时打开的多个页面使用的都是同一个JS执行线程,如果其中一个页面在执行一个运算量较大的function时,其他窗口的JS就会停止工作)。

而异步机制是浏览器的两个或以上常驻线程共同完成的,例如异步请求是由两个常驻线程:JS执行线程和事件触发线程共同完成的,JS的执行线程发起异步请求(这时浏览器会开一条新的HTTP请求线程来执行请求,这时JS的任务已完成,继续执行线程队列中剩下的其他任务),然后在未来的某一时刻事件触发线程监视到之前的发起的HTTP请求已完成,它就会把完成事件插入到JS执行队列的尾部等待JS处理。又例如定时触发(setTimeout和setinterval)是由浏览器的定时器线程执行的定时计数,然后在定时时间把定时处理函数的执行请求插入到JS执行队列的尾端(所以用这两个函数的时候,实际的执行时间是大于或等于指定时间的,不保证能准确定时的)。

所以,所谓的JS的单线程和异步更多的应该是属于浏览器的行为,他们之间没有冲突,更不是同一种事物,没有什么区别不区别的。

setTimeout(fn,0)立即执行的问题

首先,不会立即执行,原因:

setTimeout(fn,0)的作用很简单,就是为了把fn放到运行队列的***去执行。也就是说,无论setTimeout(fn,0)写在哪,都可以保证在队列的***执行。js解析器会把setTimeout(fn,0)里的fn压到队列的***,因为它是异步操作。有个延时,具体是16ms还是4ms取决于浏览器

立即执行还是有可能的,只要在你调用setTimeout的时候,满足下面两个条件:

  1. 刚好执行到了当前这一轮事件循环的底部。
  2. 刚好此时事件队列为空。

那么setTimeout的回调函数就可以立即执行。当然“立即执行”的意思是在任何其他代码前执行。

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2020-10-15 13:29:57

javascript

2017-07-13 12:12:19

前端JavaScript异步编程

2015-04-22 10:50:18

JavascriptJavascript异

2014-05-23 10:12:20

Javascript异步编程

2011-11-11 15:47:22

JavaScript

2021-06-02 09:01:19

JavaScript 前端异步编程

2021-06-06 16:56:49

异步编程Completable

2021-12-10 07:47:30

Javascript异步编程

2011-07-27 14:10:43

javascript

2022-10-31 09:00:24

Promise数组参数

2021-06-06 19:51:07

JavaScript异步编程

2023-12-04 13:22:00

JavaScript异步编程

2011-11-10 10:23:56

Jscex

2013-03-08 09:33:25

JavaScript同步异步

2013-04-01 15:38:54

异步编程异步编程模型

2013-01-07 10:44:00

JavaScriptjQueryJS

2016-10-21 11:04:07

JavaScript异步编程原理解析

2012-03-31 11:04:32

ibmdw

2021-11-01 22:36:04

JavaScript

2011-11-11 13:38:39

Jscex
点赞
收藏

51CTO技术栈公众号