【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列

最近研究JavaScript里的函数事件这些到底是如何调用的,查阅了好些资料,特别是国外一些大牛写的文章,启发非常的大,于是打算对这些知识进行梳理。

基本知识

  • JS是什么?
    • JS是单线程,非阻塞,异步,并发的语言
    • JS有 调用栈,事件循环,回调队列,其它的APIs。
  • JS 在运行时
    • JS在运行时(像V8引擎)有堆(内存分配)和栈(执行环境), 但是他们没有 setTimeout, DOC 等,这些是浏览器的web APIs。
  • 我们知道的JS在浏览器里有:
    • 运行时的V8(堆/栈)
    • 浏览器提供web APIs,比如DOM, ajax, 和setTimeout
    • 事件的回调队列,比如onClick, onLoad, onDone。
    • 事件循环
      【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第1张图片
  • JS的单线程(single thread):仅仅是指JS代码执行在单线程里面。
  • 调用栈(call stack):又称执行环境(execution contexts), 当函数或者程序调用的时候,就把该函数push到调用栈,结束时候,就从栈顶端移除。遵循FILO(先进后出)原则。注意:调用栈有个最大调用帧,chrome 浏览器最大支持16000个调用帧,超过后,会报“RangeError:Maximum call stack size exceeded”
  • 堆(Heap): 内存分配(memory allocation)的一块空间。JS的引用类型的值是放在堆内存的。
  • 回调队列(callback Queue):JS在运行时候,有一个列表用于记录将要处理的回调事件。遵循FIFO(先进先出)原则。
  • 事件循环(event loop): 事件循环不断的轮询检测调用栈是否为空?如果不为空,就等待调用栈为空,否则就把回调队列列表里的事件放到调用栈执行。循环直到回调队列列表为空。

##同步函数调用栈机制
我们以一个例子来进行同步函数执行的机制, 有两个函数foo,和bar,bar内部调用foo。

function foo(a, b) {
console.log("in foo");
return a * b;
}

function bar(y) {
var x = 10;
console.log("in bar");
return foo(x, y);
}

console.log(bar(5));

以上代码运行顺序如下:

  1. 最先进入console.log(bar(5)); 因此它被推送到调用栈。
  2. console.log(bar(5));调用函数bar(5), 等待bar(5)的返回结果,于是把bar(5)推送到调用栈顶部。
  3. 函数bar(5)里有console.log("in bar");于是推送console.log("in bar");到栈顶。
  4. console.log("in bar");执行完成返回,然后将console.log("in bar");从栈顶移除。
  5. 接着运行return foo(x, y);,这是调用foo(x, y),把foo(x, y)推入到调用栈。
  6. 执行console.log("in foo");推入栈顶,结束,移除栈。
  7. 运行return a * b; 调用 a * b,推送a * b到栈顶,a * b运行完后,出栈。
  8. 接着 return 10+5的结果返回给bar函数的foo(10, 5),这样foo(x,y)函数结束执行,foo(x,y)从调用栈移除。
  9. 接着bar(y)函数return foo(10, 5)的值给bar(5),这样bar(5)执行结束,从调用栈中移除。
  10. 最后console.log(bar(5));打印出15值,console.log调用完成,将console.log移除栈。这时调用栈就为空了,调用结束。
    执行顺序请参考下图:
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第2张图片

Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1

异步调用的事件循环机制

我们以setTimeout 为例, 有这样代码,用两个不同的timeout来阐述setTimeout的事件循环机制。

console.log("1");

setTimeout(function secondTimeout() {
    console.log("2");
}, 5000);

setTimeout(function firstTimeout() {
    console.log("3");
}, 0);
console.log("4.");

以上代码运行后打印顺序是: 1, 4, 3, 2
以上代码执行机制如下:

  1. 代码运行的console.log("1");将其push到调用栈,紧接着完成,从调用栈删除
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第3张图片
  2. 接着调用setTimeout的secondTimeout,将其push到调用栈,由于setTimeout是web API,因此web API 处理secondTimeout这个事件,并从调用栈中移除,并且在web API中开始等待5秒。
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第4张图片
  3. 接着调用setTimeout的firstTimeout,将其push到调用栈,由于setTimeout是web API,因此web API 处理这个firstTimeout事件, 由于等待是0秒,于是将firstTimeout推送到回调队列,等待调用栈的执行(遵循先进先出原则)。这时事件循环(event loop)机制开始运行,检查这时调用栈不为空?继续排队等待:把回调队列列表的第一个队列推送到调用栈运行。由于此时,调用栈还要继续执行console.log("4.");所以继续等待调用栈结束。
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第5张图片
  4. 接着console.log("4.");打印完成,这时已经是到代码执行尾部,没有主代码运行了,调用栈为空。事件循环机制检查到调用栈为空后,将第一个在callback Queue里的函数firstTimout() 推送到调用栈执行,并调用console.log(3)。结束后释放调用栈。
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第6张图片
  5. 当此时调用栈为空时,event loop继续将callback Queue例表里的其他队列一个一个push到调用栈去执行。如果此时回调队列例表里有多个队列排队,那么就安装此循环,直到全部完成为止。
    【JavaScript 学习--12】JavaScript深入理解调用栈,事件循环机制,回调队列_第7张图片

promise的事件循环机制

参考资料

1. JavaScript’s Call Stack, Callback Queue, and Event Loop
2. Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1
3. Understanding the JavaScript call stack
4. 深入浅出js事件循环机制(上)
5. 深入浅出js事件循环机制(下)

你可能感兴趣的:(JavaScript)