先看:javascript
都说js是单线程的,但是setTImeout和setInterval是什么鬼?这不是自相矛盾吗,然而,不存在的
众所周知,js是单线程的,但在js中的setTimeout和setInterval函数这哥俩却能很好的异步 以下
java
123456setTimeout(function(){console.log('若是我在2s后运行')},2*1000);console.log('若是同步,我将被阻塞2s')
浏览器
上面的代码已经很清楚了 可是结果倒是
bash
很显然,若是咱们js代码是完彻底全的同步的话,那么第二个打印应该会被第一个打印阻塞2s,可是并无阻塞主线程,倒是丢下他继续执行了下去.数据结构
反正别管这么多,咱们的js就是单线程的,那么那些异步操做是怎么完成的呢?答案是咱们的伟大的多线程的Browser,没错,虽然js是单线程的,可是浏览器是多线程的啊!多线程
当js读到setTimeout这个函数时,js告诉浏览器,嘿 兄弟 我这有个耗时操做 我搞不来 你帮我一下 固然浏览器很欣喜的答案了 并开了一个计时器线程,而后js就放心的丢下这个耗时操做 去执行下面的代码了。
那么问题就来了
并发
123456setTimeout(function(){console.log('若是我在2s后运行')},0);console.log('若是同步,我将被阻塞2s')
dom
上面的这几行代码我只是把时间换成了0而已 来 猜猜他的打印结果异步
好了 不卖关子了函数
wtf? 凭什么 我等待了0s 就差很少没等待吧 凭什么我仍是打印在你后面
这就是扯到咱们js的事件循环队列了
在js中 咱们全部同步(如今)的任务都在主线程中运行 造成执行栈
在主线程以外 咱们还有一个任务队列 专门运行异步(将来)执行的代码
咱们的js会优先读取执行栈中的同步任务 而后在去读取任务队列里的异步任务 这个过程叫作事件循环(Event Loop) 当咱们的异步任务执行完毕以后 就会在任务队列中插入一个事件 等待js执行
在 zepto 源码中,$.fn 对象 有个 ready 函数,其中有这样一句 setTimeout(fn,0);
$.fn = {
ready: function(callback){
// dont use "interactive" on IE <= 10 (it can fired premature)
//
// document.readyState:当document文档正在加载时,返回"loading"。当文档结束渲染但在加载内嵌资源时,返回"interactive",并引起DOMContentLoaded事件。当文档加载完成时,返回"complete",并引起load事件。
// document.documentElement.doScroll:IE有个特有的方法doScroll能够检测DOM是否加载完成。 当页面未加载完成时,该方法会报错,直到doScroll再也不报错时,就表明DOM加载完成了
if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll))
setTimeout(function(){ callback($) }, 0) // 重点
else {
// 监听移除事件
var handler = function() {
document.removeEventListener("DOMContentLoaded", handler, false)
window.removeEventListener("load", handler, false)
callback($)
}
document.addEventListener("DOMContentLoaded", handler, false)
window.addEventListener("load", handler, false)
}
return this;
},
}复制代码
时间设为 0 ,就是要当即执行,那为何还要特地将 fn 套到 setTimeout 里面呢?
1、线程
一、浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器一般由如下常驻线程组成:GUI 渲染线程,javascript 引擎线程,浏览器事件触发线程,定时触发器线程,异步 http 请求线程。
举个例子,看看这些线程如何配合工做的:
例子1:异步请求是由线程 JavaScript 执行线程、HTTP 请求线程 和 事件触发线程 共同完成的。JavaScript 执行线程 执行异步请求代码,这时浏览器会开一条新的 HTTP 请求线程 来执行请求,JavaScript 执行线程则继续执行 执行队列 中剩下的其余任务。而后在将来的某一时刻 事件触发线程 监视到以前的发起的 HTTP 请求已完成,它就会把完成事件的回调代码插入到 JavaScript 执行队列尾部 等待 JavaScript 执行线程空闲时来处理。
例子2:定时触发(setTimeout 和 setInterval)是由浏览器的 定时器线程 执行的定时计数,而后在定时时间结束时把定时处理函数的执行代码插入到 JavaScript 执行队列的尾端(因此用这两个函数的时候,实际的执行时间是大于或等于指定时间的,不保证能准肯定时的)。
二、javascript 是单线程的,同一个时间只能作一件事。
这里说一下 js调用栈(call stack),能够从根本上理解单线程的执行过程。
推荐一个神器网站:Loupe 能够用来图形化调用栈的过程,你们能够把例子在网站上运行一下,好用到疯掉。
js 调用栈(call stack):函数被调用时,就会被加入到调用栈顶部,执行结束以后,就会从调用栈顶部移除该函数,这种数据结构的关键在于后进先出,即 LIFO(last-in,first-out)。
举个例子:
来自(并发模型与Event Loop)
function f(b) {
var a = 12;
return a + b + 35;
}
function g(x) {
var m = 4;
return f(m * x);
}
g(21);复制代码
调用 g 函数 的时候,建立了第一个 堆( Heap ) 栈(stack) 帧 ,包含了 g 的参数和局部变量。当 g 调用 f 的时候,第二个 堆栈帧 就被建立、并置于第一个 堆栈帧 之上,包含了 f 的参数和局部变量。当 f 返回时,最上层的 堆栈帧 就出栈了(剩下 g 函数调用的 堆栈帧 )。当 g 返回的时候,栈就空了。
再举个例子:
function test() {
setTimeout(function() {
alert(1)
},1000);
alert(2);
}
test();复制代码
在执行函数 test 的时候,test 先入栈,若是不给 alert(1)加 setTimeout,那么 alert(1)第 2 个入栈,最后是 alert(2)。但如今给 alert(1)加上 setTimeout 后,alert(1)就被加入到了一个新的堆栈中等待,并1s后执行,所以实际的执行结果就是先 alert(2),再 alert(1)。
三、任务队列(消息队列):
同步函数:若是在函数A返回的时候,调用者就可以获得预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
例子:
console.log('Hi’); //函数返回时,就看到了预期的效果:在控制台打印了一个字符串复制代码
异步函数:即若是在函数A返回的时候,调用者还不可以获得预期结果,而是须要在未来经过必定的手段获得,那么这个函数就是异步的。
例子:
setTimeout(fn, 1000);//setTimeout是异步过程的发起函数,fn是回调函数。
复制代码
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务:主线程发起一个异步请求(即执行异步函数),相应的工做线程(浏览器事件触发线程、异步http请求线程等)接收请求并告知主线程已收到(异步函数返回);主线程能够继续执行后面的代码,同时工做线程执行异步任务;工做线程完成工做后,将完成消息放到任务(消息)队列,主线程经过事件循环过程去取任务(消息),而后执行必定的动做(调用回调函数)。
图中主线程即 Stack,任务队列即 Queue。
事件循环中有事件两个字的缘由:任务(消息)队列中的每条消息实际上都对应着一个事件——dom事件。
例子:
var button = document.getElement('#btn');
button.addEventListener('click',function(e) {
console.log(1);
});复制代码
从异步过程的角度看,addEventListener 函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。
那么 任务(消息)究竟是什么呢? 任务(消息)就是注册异步任务时添加的回调函数。若是 一个异步函数没有回调,那么他就不会放到任务(消息)队列里。
总结一下过程:主线程在执行完当前循环中的全部代码后,就会到任务(消息)队列取出一条消息,并执行它。到此为止,就完成了工做线程对主线程的通知,回调函数也就获得了执行。若是一开始主线程就没有提供回调函数,工做线程就不必通知主线程,从而也不必往消息队列放消息。
例子: 工做线程为异步 http 请求线程即 Ajax 线程
最后注意异步过程的回调函数,必定不在当前这一轮事件循环中执行。而是当 这一轮执行完了,主线程空了,再从任务(消息)队列中取。
再来看一下这张图
主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各类外部API,它们在"任务队列"中加入各类事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
3、setTimeout(fn, 0) 的做用
调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段做为函数的第二个参数被传入。若是队列中没有其它消息,消息会被立刻处理。可是,若是有其它消息,setTimeout 消息必须等待其它消息处理完。所以第二个参数仅仅表示最少的时间,而非确切的时间。
零延迟 (Zero delay) 并非意味着回调会当即执行。在零延迟调用 setTimeout 时,其并非过了给定的时间间隔后就立刻执行回调函数。其等待的时间基于队列里正在等待的消息数量。也就是说,setTimeout()只是将事件插入了任务队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等好久,因此并无办法保证回调函数必定会在setTimeout()指定的时间执行。
例子
setTimeout(function() {
console.log(1);
},0);
console.log(2)复制代码
执行结果2,1。由于只有在执行完第二行之后,主线程空了,才会去任务队列中取任务执行回调函数。
总结:setTimeout(fn,0)的含义是,指定某个任务在主线程最先可得的空闲时间执行,也就是说,尽量早得执行。它在"任务队列"的尾部添加一个事件,所以要等到主线程把同步任务和"任务队列"现有的事件都处理完,才会获得执行。在某种程度上,咱们能够利用setTimeout(fn,0)的特性,修正浏览器的任务顺序。