js 并发模型

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 定时多短也没用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值