并发模型与事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

函数调用形成了一个由若干帧组成的栈。

function foo(b) {
	let a = 10;
	return a + b + 11;
}

function bar(x) {
	let y = 3;
	return foo(x * y);
}

console.log(bar(7)); // 返回42

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了bar 的参数和局本变量。当 bar 调用 foo 时,第二帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

队列

一个JavaScript运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后时间循环将会处理队列中的下一个消息(如果还有的话)。

事件循环

之所以称之为事件循环,是因为它经常按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
	queue.processNextMessage();
}

queue.waitForMessage() 会同步地等待消息到达(如果当前没有任何消息等待被处理)

‘执行至完成’

每个消息完整地执行后,其他消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,他不会被抢占,只有在她运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。这与C语言不同,例如,如果函数在线程中运行,它可能在任何位置被终止,然后再另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web应用程序就无法处理与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器一般会弹出一个‘这个脚本运行时间过长’的对话框。一个良好的习惯是缩短单个信息处理时间,并在可能的情况下将一个消息裁剪成多个消息。

添加消息

在浏览器,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件建辉丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。

函数setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

下面的例子演示了这个概念(setTimeout 并不会在计时器到期之后直接执行):

const s = new Date().getSeconds();

setTimeout(function(){
	// 输出‘2’,表示回调函数并没有在500 毫秒之后立即执行
	console.log("Ran after " +new Date().getSeconds(- s) + "seconds");
},500)

while(true){
	if(new Date().getSeconds() - s >= 2){
		console.log("Good, looped for 2 seconds");
		break;
	}
}

零延迟

零延迟并不意味着回调会立即执行。以0为第二个参数调用setTimeout 并不表示在0毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,“这是一个消息”将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,并不保证是准确的等待时间。

基本上,setTimeout需要等待当前队列的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

(function() {

  console.log('这是开始');

  setTimeout(function cb() {
    console.log('这是来自第一个回调的消息');
  });

  console.log('这是一条消息');

  setTimeout(function cb1() {
    console.log('这是来自第二个回调的消息');
  }, 0);

  console.log('这是结束');

})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

多个运行时互相通信

一个web worker 或者一个跨域的iframe都是自己的栈、堆和消息队列。两个不同的运行只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则次方法会向改运行时添加消息。

永不阻塞

JavaScript的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理I/O通常通过事件和回调来执行,所以当一个应用正等待一个IndexedDB查询返回或者一个XHR请求返回时,它仍然可以处理其它事情,比如说用户输入。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值