Javascript单线程及定时器原理分析(1)

@kuitos 2015-08-23 09:36:15发表于 kuitos/kuitos.github.io javascript

Javascript单线程及定时器原理分析(1)

原文写于 2014-07-28

Talk is cheap, code first.

说出代码运行结果

var i,a=0;
setTimeout(function(){
    console.log("timeout");
}, 1000);
for(i=0;i<10;i++){
    a += i;
    if(i === 9){
        console.log("loop over");
    }
}

没错,当然是 loop over --> timeout
那么如果我们把timeout时间设为 0 呢,就像这样

// code1
setTimeout(function(){
    console.log("timeout");
}, 0);

结果还是 loop over --> timeout !!
我们假设你已经知道HTML5 spec定义了setTimeout最短时间间隔为4ms这个事实,但是浏览器实现的最短时间间隔一般是10ms。
这个时候你可能会说for循环先执行完且时间短于4ms,所以顺序是 loop over --> timeout.
假如我们把循环放大, 就像这样

// code2
for(i=0;i<1000000;i++){
    a += i;
    if(i === 999999){
        console.log("loop over");
    }
}

结果却还是 loop over --> timeout.
你可能会说 1000000 次循环时间小于4ms, 好吧我们假设chrome已经如此这般牛x.
那如果我们把完整代码改成这样呢

//code 3
var i,a=0,begin;
setTimeout(function(){
    console.log("timeout");
}, 1000);
console.log(begin=Date.now())
while(true){
    if(Date.now()-begin>1500){
        console.log("loop over");
        break;
    }
}
console.log(Date.now());
console.log("WTF!");

WTF!循环时间超过1000ms的情况下结果依然是 loop over --> WTF --> timeout !!
会出现这种结果的一起原因归结于 Javascript是单线程的,所以导致 javascript的定时器从来都不是可靠的,当JS引擎的线程一直很忙的时候,它的定时器是永远不会执行的。只有线程空闲下来的,才有功夫去玩你的定时器。看这段代码

// code4
var a = 0;
setTimeout(function(){
    console.log("timeout");
}, 1000)
while(true){
    a++;
}

没错,timeout永远都不会打印出来。(如果你不幸在浏览器中运行了这段代码,请调出进程管理器,kill process ....)。

假如你有过一定的js代码经验,你一定会问,既然是单线程的,那么javascript中的异步是怎么实现的呢,那些所谓的回调又是几个意思,用来装x的专业术语而已?

Javascript中的异步是基于 event-driver(事件驱动) 的,几乎所有的单线程语言都是通过这种方式实现异步的。什么是异步呢,言简意赅的说,异步函数就是会导致将来运行一个取自事件队列的函数的函数。

事件队列又是什么东西呢??

我们举个例子说明,假设:我们处于一个页面,这个页面上有一个setTimeout正在执行延时1000ms执行某段代码;而在这个200ms的时候,我们点击了一个按钮,因为此时已经满足事件触发条件,且JavaScript线程空闲,所以按照我们的脚本浏览器会立即执行与这个事件绑定的另外某段代码;点击事件触发的某段代码会做两件事,一件事是注册一个setInterval要求每隔700ms执行某段代码;另一件是发送一个ajax请求,并要求请求返回后执行某段代码,这个请求会在1500ms后返回。在这之后,可能还会有其它的事件被触发。
那么在整个事件队列中,他是如下排列的:
事件队列

这时候我们可以再来试着解释一下code3那段代码的执行机制是怎样的。
在此之前有一点你必须要明确,Javascript中函数是一等公民,所有代码块的运行都是基于函数的!
code4代码处于全局环境,所以我们可以将它一整个代码块理解成一个 立即执行的匿名函数, 就像这样

// code5
(function(){
var i,a=0;
setTimeout(function(){
    console.log("timeout");
}, 1000);
for(i=0;i<100000;i++){
    a += i;
    if(i === 99999){
        console.log("loop over");
    }
}
console.log("WTF!");
})();

那么js引擎解释这段代码时过程是这样的:

  1. 发现一个匿名函数,将该函数加入到事件队列中。执行匿名函数
  2. 执行到setTimeout方法,构建一个定时器,将定时器加入到事件队列中,此时队列顺序为: 匿名函数 --> 定时器回调
  3. js引擎线程继续处理匿名函数,依次输出 loop over –> WTF
  4. 匿名函数执行完毕,发现队列里还有一个定时器,于是执行定时器回调。