运行时包含的一些基本概念

519 阅读4分钟

mdn-并发模型与事件循环

栈由帧组成,帧先进后出,加入栈时从头部加入
帧中包含了函数的参数和变量

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

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执行完成并返回时,bar的调用帧弹出,栈也就被清空了。

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

队列

先进先出

js运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回掉函数。
事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理。被处理的消息会被移除队列,并作为输入参数来调用与之关联的函数(对应前面提到的,调用一个函数总是为其创建一个新的栈帧)。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环会处理队列中的下一个消息。

事件循环

之所以被称为事件循环,是因为它经常按照类似如下的方式来被实现。
queque.processNextMessage()会同步地等待消息到达(如果当前没有任何消息等待被处理)

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

"执行至完成"

每一个消息完整的执行后,其他的消息才会被执行。

  • 优点:当一个函数执行时,不会被抢占,只有在它运行完毕才会运行其他代码,才能修改这个函数操作的数据
  • 缺点:一个消息需要太长时间才能处理完毕,Web应用程序就无法处理与用户的交互(如点击、滚动)。为了缓解这个问题,浏览器一般会弹出“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。

添加消息

在浏览器里,每一个事件发生并且被事件监听器绑定时,一个消息就会被添加进消息队列(如果没有事件监听器,这个事件会丢失)。
例如一个带有点击事件处理器的元素被点击时,会产生一个消息。

setTimeout接受两个参数:待加入队列的消息、时间(默认为0)。
这个时间值代表消息被加入到队列的最小延迟时间。
如果队列中没有其他消息,并且栈为空时,这段延迟时间过去之后,消息会立马被处理。
如果有其他消息,setTimeout消息必须等其他消息处理完 question: 这里的等其他消息处理完是指等其他消息处理完才处理还是指才添加到消息队列中?

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

    while(true) {
        if(new Date().getSeconds() - s >= 2) {
            console.log("循环2s");
            break;
        }
    }

零延迟

零延迟并不意味立即执行,取决与队列中的消息是否为空,并且栈为空。 setTimeout需要等待队列中的消息全部执行完毕才能执行(所以会超出指定时间)

多个运行时互相通信

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

永不阻塞

js的事件循环模型有个与其他语言不一样的特性-它永不阻塞。处理I/O通常是通过事件和回调来执行,所以当一个应用正等待一个IndexDB查询返回或者返回一个XHR请求返回时,它仍然可以处理其他事情,比如用户输入。
有例外:alert或同步的XHR,应该避免使用它们。
有例外的例外:但通常都是你写的代码错误。