JavaScript与Event Loop

二叶草 2020年2月14日09:26:46函数代码评论阅读模式

JavaScript是单线程工作的

JavaScript 語言的一大特性就是说单线程。换句话说,同一个時间只有做一件事。从JavaScript 模块的视角看来,就是说无论如何只有有一段编码在实行。那麼,为何 JavaScript 不应用好几个进程呢?终究,多线程的效率更高呢。

其实,JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果用过类似 JAVA 语言的多线程编程,会了解到多线程在提供便利性的同时,也带来了问题:线程切换带来性能开销、线程死锁情况等。

于是,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已成为这门语言的核心特征,将来也不会改变。

单线程意味着什么呢?意味着:程序启动后,在 JavaScript 引擎中,只会有一个调用栈(call stack),任何时刻只能有一段代码在执行。

JavaScript引擎与JavaScript执行环境

JavaScript 引擎的任务是严格遵循 ECMAScript 规范,解析对应的 JS 语句并执行。仅此而已。

再来说 JavaScript 执行环境。如果宿主环境是浏览器,那么 JavaScript 执行环境就包含了很多东西了:JavaScript引擎、WEB API、Event Loop等。(宿主环境若是Node环境,其构成也类似)

于是可以看到,在宿主环境中,JavaScript 执行环境是包含了 JavaScript 引擎的。两者是包含关系,而不是对等关系。这个必须要清楚。

Event Loop 机制

我们看到,在 JavaScript 执行环境中包含了 Event Loop。那么,这个 Event Loop 是干嘛用的呢?简单讲,Event Loop 是 JavaScript 执行环境中实现异步调用的一种实现方式。

先来看看 whatwg 的网站对于 Event Loop 的定义与规范。有人将这些规范变成了伪代码,如下:

//whatwg网站地址:https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

eventLoop = {

    taskQueues: {
        events: [], // UI events from native GUI framework
        parser: [], // HTML parser
        callbacks: [], // setTimeout, requestIdleTask
        resources: [], // image loading
        domManipulation[]
    },

    microtaskQueue: [
    ],

    nextTask: function() {
        // Spec says:
        // "Select the oldest task on one of the event loop's task queues"
        // Which gives browser implementers lots of freedom
        // Queues can have different priorities, etc.
        for (let q of taskQueues)
            if (q.length > 0)
                return q.shift();
        return null;
    },

    executeMicrotasks: function() {
        if (scriptExecuting)
            return;
        let microtasks = this.microtaskQueue;
        this.microtaskQueue = [];
        for (let t of microtasks)
            t.execute();
    },

    needsRendering: function() {
        return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch());
    },

    render: function() {
        dispatchPendingUIEvents();
        resizeSteps();
        scrollSteps();
        mediaQuerySteps();
        cssAnimationSteps();
        fullscreenRenderingSteps();

        animationFrameCallbackSteps();


        while (resizeObserverSteps()) {
            updateStyle();
            updateLayout();
        }
        intersectionObserverObserves();
        paint();
    }
}
// how it work:
while(true) {
    task = eventLoop.nextTask();
    if (task) {
        task.execute();
    }
    eventLoop.executeMicrotasks();
    if (eventLoop.needsRendering())
        eventLoop.render();
}

以上代码粗略地描述了 Event Loop 的构成与用法。这当中有两个概念:taskQueues、microtaskQueue。

taskQueues 即是我们常说的宏任务,它是一个 Set,将不同的任务分放到不同的队列中(当然,队列之间有不同的执行优先级)。我们看到,这其中既有 UI 事件队列,也有回调函数队列等。

microtaskQueue 即是我们常说的微任务,它的结构比较简单,就是一队列。

Event Loop 是怎么工作的呢?看最后的代码:

while(true) {
    task = eventLoop.nextTask();
    if (task) {
        task.execute();
    }
    eventLoop.executeMicrotasks();
    if (eventLoop.needsRendering())
        eventLoop.render();
}

Event Loop 是一直都在 run 的。在一次循环中,从宏任务 taskQueues 中找到一个 task 去执行;在每个宏任务执行完之后,再去执行 microtaskQueue 微任务队列中所有的微任务。具体如下图所示:

JavaScript与Event Loop

那么,你可能会问了:伪代码里并没有写清楚,这些宏任务与微任务是怎么放到 Event Loop 对应的队列中的呀?

不急。我们接着往下看。

概念整合

宿主环境 -> JavaScript 引擎

当宿主(浏览器或者 Node 环境)拿到一段 JavaScript 代码时,首先做的就是:传递给 JavaScript 引擎,并且要求它去执行。

所以,我们首先应该形成一个感性的认知:一个 JavaScript 引擎会常驻于内存中,它等待着宿主把 JavaScript 代码或者函数传递给它执行。

宿主环境 -> Event Loop -> JavaScript 引擎

然而,执行 JavaScript 并非一锤子买卖。

宿主环境当遇到一些事件时,会将事件放入到 Event Loop 对应的队列中,并设置对应的 Watcher。Event Loop 在轮询时,会询问对应Watcher所关联的事件是否完成,若完成,就会将该事件对应的回调函数或代码(如果有),传递给 JavaScript 引擎去执行。

此外,我们可能还会提供 WEB API 给 JavaScript 引擎,比如 setTimeout 这样的 API。JavaScript 引擎会将其放入到Event Loop 对应的队列中,并设置对应的定时器 Watcher。并依据类似的机制,Event Loop 会询问定时器 Watcher 对应的时间是否已到。若时间已到,那么就会将监控到 setTimeout 对应的回调函数传递给 JavaScript 引擎去执行。

上面这个过程,Event Loop 在实际过程中用到了 Watcher 观察者。上一章节的问题:这些宏任务与微任务是怎么放到 Event Loop 对应的队列中的呀?答案就是 Watcher 观察者。关于其实现的详细过程,在此不敷述。

宿主环境 -> Event Loop -> JavaScript 引擎 -> 微任务

在 ES3 和更早的版本中,JavaScript 引擎本身还没有异步执行代码的能力。这也就意味着,宿主环境(通过 Event Loop)传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。

但是,在 ES5 之后,JavaScript 引入了 Promise。这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。

采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏任务,把 JavaScript 引擎发起的任务称为微任务。于是,这个时候,Event Loop 的微任务队列也正式被使用了起来。

因此我们知道,在 Promise 出现以前,所有的异步事件与异步 Web API 的实现都属于 Event Loop 中的宏任务。

写在最后

如果我们弄清楚了这些概念,那么关于 Event Loop 相关的知识点,就可以较好解决了。

那么做个小测试吧~ 下面代码的输出是什么呢?

var r = new Promise(function(resolve, reject){
    console.log("a");
    resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")


本文来源于:JavaScript与Event Loop-变化吧门户
特别声明:以上文章内容仅代表作者本人观点,不代表变化吧门户观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
二叶草
Go语言中的常量 函数代码

Go语言中的常量

1 概述 常量,一经定义不可更改的量。功能角度看,当出现不需要被更改的数据时,应该使用常量进行存储,例如圆周率。从语法的角度看,使用常量可以保证数据,在整个运行期间内,不会被更改。例如当前处理器的架构...
Go语言的接口 函数代码

Go语言的接口

Go语言-接口 在Go语言中,一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为。一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明...
Go语言支持的正则语法 函数代码

Go语言支持的正则语法

1 字符 语法 说明 . 任意字符,在单行模式(s标志)下,也可以匹配换行 字符类 否定字符类 d Perl 字符类 D 否定 Perl 字符类 ASCII 字符类 否定 ASCII 字符类 pN U...
Go语言的包管理 函数代码

Go语言的包管理

1 概述 Go 语言的源码复用建立在包(package)基础之上。包通过 package, import, GOPATH 操作完成。 2 main包 Go 语言的入口 main() 函数所在的包(pa...

发表评论