前言
你可能知道,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 参数没有写,默认为 0If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 如果嵌套的层次大于 5 ,并且 timeout 设置的数值小于 4 则直接取 4.
其次是while
循环会阻塞setTimeout
的执行
看这段代码:
var t = true;
window.setTimeout(function (){
t = false;
},1000);
while (t){}
alert('end');
结果是死循环导致setTimeout
不执行,也导致alert
不执行
js是单线程,所以会先执行while(t){}
再alert
,但这个循环体是死循环,所以永远不会执行alert
。
至于说为什么不执行setTimeout
,是因为js的工作机制是:当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout
是异步代码,所以setTimeout
只能等js空闲才会执行,但死循环是永远不会空闲的,所以setTimeout
也永远不会执行。
第三是,try..catch
捕捉不到他的错误
异步编程方法
回调函数
这是异步编程最基本的方法。
假定有两个函数f1和f2,后者等待前者的执行结果。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
f1(f2);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生
f1.on('done', f2);
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
JS 和 浏览器提供的原生方法基本都是基于事件触发机制的,耦合度很低,不过事件不能得到流程控制
Promises
对象
Promises
对象是CommonJS
工作组提出的一种规范,目的是为异步编程提供统一接口。
Promises
可以简单理解为一个事务,这个事务存在三种状态:
已经完成了
resolved
因为某种原因被中断了
rejected
还在等待上一个事务结束
pending
简单说,它的思想是,每一个异步任务返回一个Promises
对象,该对象有一个then
方法,允许指定回调函数,这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚
Promises
就是一个事务的管理器。他的作用就是将各种内嵌回调的事务用流水形式表达,其目的是为了简化编程,让代码逻辑更加清晰。
Promises
可以分为:
无错误传递的
Promises
,也就是事务不会因为任何原因中断,事务队列中的事项都会被依次处理,此过程中Promises
只有pending
和resolved
两种状态,没有rejected
状态。包含错误的
Promises
,每个事务的处理都必须使用容错机制来获取结果,一旦出错,就会将错误信息传递给下一个事务,如果错误信息会影响下一个事务,则下一个事务也会rejected
,如果不会,下一个事务可以正常执行,依次类推。
此处留坑讲generator实现异步编程
本来想自己总结下generator与异步的,看了下阮一峰老师的博客算是了解个大概,理解也是一知半解,有兴趣的话可以在底下的参考资料里找到去看看
封装好的实现
jquery
的Deferred
对象
简单说,Deferred
对象就是jquery
的回调函数解决方案。在英语中,defer的意思是"延迟",所以Deferred
对象的含义就是"延迟"到未来某个点再执行。
首先,回顾一下jquery
的ajax操作的传统写法:
$.ajax({
url: "test.html",
success: function(){
alert("哈哈,成功了!");
},
error:function(){
alert("出错啦!");
}
});
有了<ode>Deferred对象以后,写法是这样的:
$.ajax("test.html")
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
可以看到,done()
相当于success
方法,fail()
相当于error
方法。采用链式写法以后,代码的可读性大大提高。
了解jQuery.Deferred
对象可以看下面这个表格。
when.js
AngularJS
内置的Kris Kowal的Q框架,和cujoJS的when.js
,两者都是Promises/A
规范的实现when.js
实例
var getData = function() {
var deferred = when.defer();
$.getJSON(api, function(data){
deferred.resolve(data[0]);
});
return deferred.promise;
}
var getImg = function(src) {
var deferred = when.defer();
var img = new Image();
img.onload = function() {
deferred.resolve(img);
};
img.src = src;
return deferred.promise;
}
var showImg = function(img) {
$(img).appendTo($('#container'));
}
getData()
.then(getImg)
.then(showImg);
看最后三行代码,是不是一目了然,非常的语义化
var deferred = when.defer();
定义了一个deferred
对象。
deferred.resolve(data);
在异步获取数据完成时,把数据作为参数,调用deferred
对象的resolve
方法。
return deferred.promise;
返回了deferred
对象的Promises
属性。
控制流程工具step.js
github地址step.js
是控制流程工具(大小仅 150 行代码),解决回调嵌套层次过多等问题。适用于读文件、查询数据库等回调函数相互依赖,或者分别获取内容最后组合数据返回等应用情景。异步执行简单地可以分为“串行执行”和“并行”执行
使用示例:
Step(
function readSelf() {
fs.readFile(__filename, this);
},
function capitalize(err, text) {
if (err) throw err;
return text.toUpperCase();
},
function showIt(err, newText) {
if (err) throw err;
console.log(newText);
}
);
Step
的一个约定,回调函数的第一个参数总是 err,第二个才是值(沿用 Node
回调的风格)。如果上一个步骤发生异常,那么异常对象将被送入到下一个步骤中。
扩展阅读
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
的时候,满足下面两个条件:
刚好执行到了当前这一轮事件循环的底部。
刚好此时事件队列为空。
那么setTimeout
的回调函数就可以立即执行。当然“立即执行”的意思是在任何其他代码前执行。
参考资料
知乎:setTimeout的异步以及js是单线程的面试题?
阮老师的博客
小胡子哥的博客
知乎:JavaScript 既是单线程又是异步的,请问这二者是否冲突,以及有什么区别?
细嗅Promise
whenjs文档
jQuery的deferred对象详解
jQuery 中的 Deferred 和 Promises (2)
Step.js 使用教程(附源码解析)
Generator 函数的含义与用法
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。