JavaScript 的并发模型
今天看到一个关于 setTimeout 的地方,有个地方疑惑不解,引发了我对于 JavaScript 事件循环知识体系的重构。经过不断推翻与重建,终于得到了我认为满意的答案。
今天打算先理清楚 JavaScript 的并发模型(基于事件循环),然后通过 setTimeout 的多个例子,重新印证并发模型。
并发模型
JavaScript 的并发模型基于"事件循环"。
可视化图:

Javascript 运行的时候,产生堆(heap)和栈(stack)
栈:函数调用形成了一个栈帧。
堆:对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。
队列:一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。(可以看在后面事件循环的例子2时再回头看这个解释)
例子1:
function foo(b) {
console.log("foo");
var a = 10;
return a + b + 11;
}
function bar(x) {
console.log("bar");
var y = 3;
return foo(x * y);
}
console.log(bar(7));
//输出:
//bar
//foo
//42

解析:
先执行 console.log(bar(7))
然后调用 bar 时,创建了第一个帧 ,帧中包含了 bar 的参数和局部变量,输出 “bar”
当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量,输出 “foo”
当 foo 返回时,最上层的帧就被弹出栈(剩下bar函数的调用帧 )。当 bar 返回的时候,输出 “42”,栈就空了
事件循环(event loop)

javascript 是单线程,所谓的单线程是指在 JS 引擎中负责解释和执行JavaScript代码的线程只有一个,可以叫它为主线程*(并发模型的栈 stack)。
除了主线程,还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程等等。
执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列(并发模型的队列)中顺序获取任务来执行,这一过程是一个不断循环的过程,称为事件循环模型。
例子2:
console.log("start");
setTimeout(function(){
console.log("Wake up after 1s")
},1000);
console.log("end");
//输出:
//start
//end
//Wake up after 1s

解析:
主线程依次执行(1)console.log(“start”)(2)setTimeout()(3)console.log(“end”)
首先处理(1),输出 “start”
处理到(2)时, 定时器线程增加一个任务(setTimeout 的函数),定时器线程会在 1000ms 后把该函数推入任务队列
然后处理(3),输出 “end”
这时候主线程(栈)执行完毕空闲了,就会按顺序从任务队列获取任务来执行,因为任务队列只有一个任务,所以输出 “Wake up after 1s”
例子3:
例子3结合了例子1和2,继续琢磨琢磨:
function foo(b) {
setTimeout(function(){console.log("foo")},0);
var a = 10;
return a + b + 11;
}
function bar(x) {
setTimeout(function(){console.log("bar")},0);
var y = 3;
return foo(x * y);
}
console.log(bar(7));
//输出:
//42
//bar
//foo
理解每个名词的解释和执行的顺序,把前面几个例子弄清楚,然后再看后面
例子4:
console.log(1);
//Time1
setTimeout(function(){
console.log(2);
},300);
//Time2
setTimeout(function(){
console.log(3)
},400);
for (var i = 0;i<100;i++) {
console.log(4);
}
//Time3
setTimeout(function(){
console.log(5);
},100);
解析:
主线程执行(1)console.log(1) (2)setTimeout(Time1) (3)setTimeout(Time2) (4)for(…){…console.log(4)} (5)setTimeout(Time3)
执行(1),输出 “1”
执行(2),定时器线程推入 Time1 ,定时 300ms
执行(3),定时器线程推入 Time2 ,定时 400ms
执行(4)输出 100 个 “4”
执行(5),定时器线程推入 Time3 ,定时 100ms
消息队列依次输出 “5” “2” “3”
有些人也许发现了这里有个大坑,定时器的时间貌似会影响输出
现在输出 “1” , 100 个 “4” , “5” , “2” , “3”
要是 Time3 换成 300ms
就会输出 “1” , 100 个 “4” , “2” , “5” , “3”
其实,这里的顺序看的是谁先进入任务列队,如果 Time1 进入定时器线程后到进入任务队列的时间,比从 Time1 执行到 Time3 的时间加上 Time3 进入定时器线程的定时时间长,则慢,反之亦然。如果 Time1 Time2 定时一样,肯定Time1 先。
例子5:
怎么还有例子5,代码的世界无穷尽啊
其实如果你把例子4 的 for 循环的 i 改成 <10000 或者更大,你会发现输出又不一样了,Why?
回头看看事件循环中队列的解释,没错,只有等主线程空闲了,才会执行消息,如果 i 循环过大,阻塞了栈的执行,那么后面等到 for 循环执行完,抱歉,Time1 Time2 已经进入消息队列了,不管你 Time3 定时多短也没用
435

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



