深入理解Javascript之Callstack&EventLoop

1,342 阅读13分钟

1.概述

众所周知,Javascript是一个单线程的语言。这意味着,在Javascript中,同一时间只能做一件事情。

这样的设计有一些优点,例如简单,避免了多线程中复杂的状态同步,写程序时不用考虑并发访问。但同时也带来了一些其他问题,其中比较突出的一个问题是:代码逻辑不直观。由于Javascript是单线程的,其中只有一个执行序列。所以,在执行异步操作(例如定时,网络请求这些不能立即完成的操作)时,Javascript运行时不可能在那里等着操作完成。否则整个运行时都被阻塞在那里了,导致其他所有的操作都无法进行,例如网页渲染,用户点击、滚动页面等操作。这样的用户体验是非常糟糕的。

正因为如此,Javascript使用回调函数处理异步操作结果。进行异步操作时传入一个回调,操作完成之后由Javascript引擎执行这个回调,将结果传入。慢慢地,Javascript中充斥着大量的回调。过多的使用回调让一段完整的逻辑被拆分成了很多片段,非常不利于阅读与维护。回调过多的问题在NodeJS中更为突出,故而出现了Promise(见我的前一篇博客)和async/await

那么异步操作完成时,Javascript运行时是怎样感知到并调用对应的回调函数的呢?

答案是EventLoop(事件循环)。

要了解EventLoop是怎样运作的,我们首先需要了解Javascript是怎样处理一个个任务,调用一个个函数的。这就是CallStack(调用栈)所做的事。

2.调用栈

相信有过其他语言编程经验的读者都听说过CallStack的概念。Javascript中的CallStack类似。

CallStack是一个栈结构,栈的特点是LIFO(后入先出),出栈入栈只会在一端(也就是栈顶)进行。

CallStack是用来处理函数调用与返回的。每次调用一个函数,Javascript运行时会生成一个新的调用结构压入CallStack。而函数调用结束返回时,JavaScript运行时会将栈顶的调用结构弹出。由于栈的LIFO特性,每次弹出的必然是最新调用的那个函数的结构。

Javascript启动时,从文件或标准输入加载程序。加载完成时,Javascript运行时会生成一个匿名的函数,函数体就是输入的代码。这个函数就有点类似于C/C++中的main()函数,是我们的入口函数。我们姑且称之为<main>函数。Javascript启动时,首先调用就是<main>函数。看下面代码:

function func1() {
    console.log('in function1');
}

function func2() {
    func1();
    console.log('in function2');
}

function func3() {
    func2();
    console.log('in function3');
}

func3();

上面代码很好理解,我们来看看Javascript是如何运行这段代码的。

Javascript首先加载代码,创建一个匿名<main>包裹这段代码并调用该函数。<main>函数执行,依次定义函数func1func2func3,然后调用函数func3。为func3创建调用结构并压栈。函数func3中调用func2,为func2创建调用结构并压栈。函数func2中调用func1,为func1创建调用结构并压栈。这个过程中,CallStack的变化如下。

Call Stack Push

然后,函数func1执行完成,从栈顶弹出调用结构。然后func2继续执行,func2执行完成后从栈顶弹出其调用结构。然后func3继续执行,func3执行完成后从栈顶弹出其调用结构。这个过程中,CallStack的变化如下。

Call Stack Pop

当然,我这里有一个地方不太严谨。不知道读者有没有注意到,console.log也是函数函数哦。所以在func1中调用console.log时,CallStack上也会有对应的调用堆栈。func2func3中的console.log调用同样如此。有兴趣的话,可以自己画一画完整的调用流程,这样可以加深理解😀。

这里我推荐大家使用Google Chrome的开发者工具来帮助我们理解CallStack。下图是上面代码在开发者工具中的一步步执行的结果:

Call Stack Demo

  • Chrome中<main>称为<anonymous>

  • 单步执行时,重点观察右侧工具栏中Call Stack一栏的变化。

在CallStack中执行的函数,我们称之为一个task(任务)。

接下来,我们来思考这样一个问题:setTimeoutsetIntervalAJAX请求这些功能是怎么实现的?

一位牛人Philip Roberts曾经将V8引擎(Chrome内置的Javascript引擎)源码下载下来,然后用grep查找,发现源码中并没有实现这些函数的代码😂。

那么这些函数到底是如何实现的,又是如何与Javascript引擎交互的呢?

答案是:宿主(网页中指的是浏览器,Node中指的是Node引擎)提供实现,并在操作完成时将结果(异步的,会有延迟)放入Javascript引擎的task队列,由Javascript引擎处理。

3.事件循环

EventLoop顾名思义,其实就是Javascript引擎中的一个循环,它就是一个不停地从任务队列(task queue)中取出任务执行的过程。

我们前面详细了解了CallStack以及Javascript启动时是如何处理的。但是<main>退出后,Javascript引擎就没事做了吗?当然不是,有很多任务会不定时的触发需要Javascript引擎去处理。例如,用户点击按钮,定时器,页面渲染等。

其实,Javascript引擎中维护着一个任务队列。当CallStack中没有任务在执行时,引擎会从任务队列中取出任务压入CallStack处理。我们通过代码来具体看看(引用jesstelford):

setTimeout(() => {
    console.log('hi');
}, 1000);

我们的Js代码,call stack,task queue和Web APIs(浏览器中实现)关系如下:

        [code]        |   [call stack]    | [task queue] | |    [Web APIs] |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

开始时,代码未执行,所有都是空的。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | <main>            |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

开始执行代码,压入我们的<main>函数。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
> setTimeout(() => {  | <main>            |              | |               |
    console.log('hi') | setTimeout        |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

执行第一行代码,调用函数setTimeout。我们前面说过,每个函数调用都会创建一个新的调用记录压到栈上。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | <main>            |              | | timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

setTimeout执行完成,从栈中移除对应调用记录。Web APIs记录超时和回调,超时机制由浏览器实现。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | | timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

代码中没有其他逻辑,<main>函数结束,从栈移除调用信息。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   | function   <-----timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

超时时间到了,Web APIs将回调放入task queue中。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function        <---function     | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

EventLoop检测到Javascript没有任务处理,从task queue中取出任务执行。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function          |              | |               |
>   console.log('hi') | console.log       |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |

执行该回调函数,回调函数中又调用了console.log函数。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function          |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
> hi

console.log执行完成,输出"hi"。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
> hi

回调函数执行完成,CallStack再次为空。

上面就是setTimeout的执行过程,从中开始看出EventLoop在幕后做的工作。

下面我们再来看一段代码:

console.log("start");

setTimeout(() => {
    console.log("timeout");
}, 0);

Promise.resolve()
  .then(() => {
      console.log("promise1");
  })
  .then(() => {
      console.log("promise2");
  });

console.log("end");

这段程序的输出是什么?建议大家先思考一下,最好能动笔画一画图😎。

4.微任务队列

接着上一节的代码,符合标准(很多旧版本的浏览器实现都是不符合标准的,具体参见参考链接)的输出应该是:

start
end
promise1
promise2
timeout

但是,为?什?么?

实际上,Javascript中有另外一种队列。Promise的回调是被放入这个队列的。这个队列叫做microtask queue(微任务队列),ES6标准中叫Job Queue。microTask的优先级是比task高的,也就是说microtask队列中的任务要先处理。

  • EventLoop检测到当前没有任务在执行,首先检查microtask队列中有没有需要处理的任务。如果有那么一个个执行,直到microtask队列为空。

  • microtask队列中没有任务了,执行task队列中的任务。这里需要注意,**每执行一个task队列中的任务,就检查一下microtask队列状态。将microtask队列中所有任务都执行完成之后,再从task队列中取出任务执行。

下面我们看看上面那段代码是怎么一步步执行的:

为方便起见我们称setTimeout回调为timeoutcb,称第一个then成功回调为promisecb1,第二个then回调为promisecb2

        [code]                    |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  --------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");        |                   |              | |                   | |               |
  2.                              |                   |              | |                   | |               |
  3. setTimeout(() => {           |                   |              | |                   | |               |
  4.     console.log("timeout");  |                   |              | |                   | |               |
  5. }, 0);                       |                   |              | |                   | |               |
  6.                              |                   |              | |                   | |               |
  7. Promise.resolve()            |                   |              | |                   | |               |
  8. .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1"); |                   |              | |                   | |               |
  10.})                           |                   |              | |                   | |               |
  11..then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2"); |                   |              | |                   | |               |
  13.});                          |                   |              | |                   | |               |
  14.                             |                   |              | |                   | |               |
  15.console.log("end");          |                   |              | |                   | |               |

开始时,call stack、task queue、microtask queue都为空。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start");         |     <main>        |              | |                   | |               |
  2.                               |     console.log   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |

程序开始运行,压入<main>函数。首先执行第一行代码,console.log("start"),将console.log压栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start");         |     <main>        |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start

console.log执行完成,输出"start"。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |              | |                   | |               |
  2.                               |    setTimeout     |              | |                   | |               |
> 3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start

第二行为空跳过,开始执行第三行代码,setTimeout压栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |   timeoutcb <------------------------~~timeoutcb, 0~~|
  2.                               |                   |              | |                   | |               |
> 3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start

setTimeout执行完成,由于超时是0,所以立即回调立即进入task queue中。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |  timeoutcb   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
> 7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start

代码执行到第7行:

  • 首先Promise.resolve压栈,执行完成后返回一个Promise对象。

  • 然后调用该对象的then方法,该方法压栈,执行完成后返回一个全新的Promise对象,我们称该对象为promise1。由于Promise.resolve返回对象的状态为resolved,所以promise1回调直接进入microtask队列

  • 接着又执行新对象的then方法,我们称该对象为promise2

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |    timeout   | |     promisecb1    | |               |
  2.                               |    console.log    |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start

代码执行到第15行,console.log压栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |    timeout   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end

console.log("end")执行完成,输出"end",出栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end

<main>函数没有逻辑需要执行了,出栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    promisecb1     |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end

EventLoop检查到microtask队列中有任务需要执行,将promisecb1取出压入call stack。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |      promisecb2   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1

promisecb1执行完成,输出"promise1",并且返回undefined。(其实在这里还有一个console.log压栈出栈的过程,我就不画了,下同)

深入理解Javascript之Promise中看到,如果返回一个值,那么对象立刻变为resolved所以第二个then的回调需要安排执行,进入microtask队列

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |     promisecb2    |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1

EventLoop检测到call stack中没有正在执行的任务,同时microtask队列不为空。从microtask队列取出任务压入call stack。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2

promisecb2执行完成,输出"promise2"。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    timeoutcb      |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2

接着,EventLoop检测到call stack和microtask队列都为空,从task队列中取出timeoutcb压入栈。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2
> timeout

timeoutcb执行完成,输出"timeout"。

5.总结

通过这篇文章,我们了解到Javascript时如何通过call stack来处理函数的调用与返回的。setTimeout等异步机制其实是宿主提供实现,并在异步操作完成负责将回调放入任务队列,最后由EventLoop在适合的时机取出压入call stack实际执行。

我们还看到了另外一种队列——microtask队列。该队列中存放的一般是优先级较高的任务,例如Promise的回调处理函数。

每当call stack中没有正在执行的任务时,EventLoop会优先从microtask队列中取出任务执行,当该队列为空时才会从task队列取。

在使用一门框架或语言时,对于是否需要了解底层运作机制和原理,往往会有比较大的争论。有人说,我不了解内部原理同样可以写出好程序,那为什么还需要花时间去研究呢? 对此,我觉得了解底层原理还是非常有必要的。有下面几个好处:

  • 可以让我们看到全貌,了解整个系统是如何运作的。

  • 底层原理大多是相通的,例如几乎所有语言的函数调用底层都是利用CallStack来实现的。学会了Javascript的CallStack运作机制,在学习其他语言的相关概念时往往能事半功倍。

  • 了解底层可以让我们心中有数,明白什么事情能做,什么事情不能做。例如Javascript是单线程的,我们写代码时一定不能让线程阻塞了😀。

  • 了解底层可以让我们更好的优化代码。当程序性能出现瓶颈时,可以更快地定位问题。

6.参考链接

  1. Philip Roberts在欧洲JSConf上的演讲(必看)
  2. What is the JS Event Loop and Call Stack? — Jess Telford
  3. Tasks, microtasks, queues and schedules
  4. Understanding the JavaScript call stack

关于我: 个人主页 简书 掘金