du blog
Hello, welcome to my blog
js运行机制(2)-调用堆栈-事件循环
created: Mar 7 21updated: Mar 8 21

调用栈(call stack)

调用栈是一种机制,可以帮助js解释器跟踪脚本调用的函数

每次执行调用一个函数的时候,都会将其添加到调用栈的顶部,每当函数退出,解释器便会将其从调用栈中弹出,

函数可以通过 return 语句退出,也可以通过到达作用域末尾退出

每次一个函数调用另外一个函数,它都被添加到栈顶,在调用函数的顶部,

调用栈处理每个函数遵循先进后出(后进先出)的原则,上边示例的步骤如下:

1. 文件加载并调用main函数,main函数代表整个文件(全局上下文),这个函数被添加到调用栈中

2. 文件执行,调用 calculation,将其添加到栈顶

3. calculation 调用 addThree,addThree被添加到栈顶

4. addThree 调用 addTwo, addTwo被添加到栈顶

5. addTwo调用addOne,addOne被添加到栈顶

6. addOne不调用任何函数,当它执行完毕(执行 return 语句 或者 达到作用域末尾),将其弹出调用栈

7. addTwo获取addOne执行结果,并执行完毕,将其弹出调用栈

8. addThree获取addTwo的执行结果,并执行完毕,将其弹出调用栈

9. calculation调用 addTwo,将addTwo添加到调用栈中

10. addTwo调用addOne,将addOne添加到调用栈

11. addOne没有调用任何函数,当它执行完毕,将其弹出调用栈

12. addTwo获取addOne的执行结果,并执行完毕,将其弹出调用栈

13. calculation获得addThree和addTwo的结果,返回和,执行完毕,弹出调用栈

14. 文件中没有执行语句和函数调用,main函数退出调用栈

可以通过chrome的开发者工具 source tab 看到当前代码执行的调用栈

超出最大调用栈大小(Maximum call stack size exceeded)

这可能是在日常会碰到的错误,如果看到这个错误,说明函数调用了太多的函数,最大调用栈大概在5万-10万个调用,如果超出了这个范围,代码可能处在死循环中

可以从输出的调用栈 查看导致此错误的函数调用

浏览器通过限制调用栈的大小,来防止代码冻结页面

总结:调用栈用来跟踪代码中的函数调用,它遵循先进后出的原则,js是单线程的,这也意味着它只有一个调用栈

浏览器中的事件循环

单线程的js

javascript语言的一大特点就是单线程,也就是说同一时间只能做一件事情,为什么是单线程,则与它的用途有关,js 的主要用途是与用户互动,以及操作dom,这决定了它只能是单线程的,否则就会带来很复杂的同步问题,假如js同时有两个线程,一个线程在某个dom节点上添加了内容,一个线程删除了这个节点,那么该以谁为准呢?

为了更好的利用多核cpu的计算能力,html5提出了web worker 的启用子线程的能力,允许js脚本创建多个线程,但是子线程完全受主线程控制,且不得操作dom,所以这个标准并没有改变js单线程的本质

什么是异步

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量很大,倒也算了,但是大多数时候cpu是闲着的,因为I/O操作(比如 ajax操作从网络读取数据)不得不等出结果,才能往下执行

这样的情况下单线程的方式,就会导致执行率低下,cpu利用率很低,

所以就出现了所谓的异步,一段js脚本可以分为同步代码异步代码,如下

1 var a = '123'; 2 3 function hello() { 4 console.log('hello'); 5 } 6 setTimeout(() => { // 异步代码 7 console.log('-----setTimeout'); 8 }, 1000); 9 hello(); 10 console.log('a'); 11 12 // hello 13 // a 14 // setTimeout 15

异步可以理解为 这段代码暂时被挂起,等到该执行的时候才会执行

这段代码执行如下:

1. 声明 变量 a

2. 声明函数 hello

3. 调用setTimeOut,传入回调,此时回调代码被挂起,等到合适的时间执行,

4. 调用函数hello,执行函数 hello

5. 输出 变量 a

6. 定时器触发 输出 setTimeout

异步任务有:定时器、ajax请求,用户事件等

需要注意的是,只有同步代码执行完毕,才会执行异步代码

也就是说,同步的代码一定先执行于异步代码:

1 setTimeout(() => { 2 console.log('hello'); 3 }, 0); 4 for (let index = 0; index < 100000000; index++) { 5 6 7 } 8 console.log('a'); 9 10 // a 11 // hello 12

所以: 下边的两个写法是一样的:

1 var req = new XMLHttpRequest(); 2 req.open('GET', url); 3 req.onload = function (){}; 4 req.onerror = function (){}; 5 req.send(); 6
1 var req = new XMLHttpRequest(); 2 req.open('GET', url); 3 req.send(); 4 req.onload = function (){}; 5 req.onerror = function (){}; 6

需要注意的一点是,如果同步代码执行时间过长,定时器是不准确的,比如上一个例子 我们输出一下时间

1 setTimeout(() => { 2 console.log('hello'); 3 }, 0); 4 console.time(1) 5 for (let index = 0; index < 100000000; index++) { 6 7 8 } 9 console.timeEnd(1) 10 console.log('a'); 11 12 // 82.30517578125 ms 13 // a 14 // hello 15

可以看到 执行for循环就已经执行了82+ms

这里再插一句,其实所谓的异步,只是浏览器又开了一个专门执行异步操作的线程,比如执行一个http请求,此时浏览器会有一个专门执行http请求的线程,当http请求完成之后通知js主线程,但是对于js开发者来说js确实是单线程的,是没有问题的

事件循环(event loop)

异步是被浏览器挂起的代码,这些代码通常都是一个回调函数,即这些回调函数被浏览器挂起,等到合适的时机执行,那么什么是合适的时机呢,这就要说到事件循环了,

事件循环可以理解为浏览器执行js代码的顺序机制,

heap, call stack 即为堆内存和调用堆栈,

web apis 即为 浏览器的异步 api 比如:ajax setTimeout 用户事件等

callback queue(回调队列) 即为异步操作到了 合适的时机(比如 定时器到时间了,ajax返回了数据,用户触发了click),此时,把之前被挂起的代码(回调),放入回调队列中,队列为一个先进先出的结构

event loop:当js脚本中同步代码执行完毕,event loop会从回调队列中依次读取 异步操作的回调,放入 调用栈中执行

我们再次来分析一下上边的第一个例子

1 var a = '123'; 2 3 function hello() { 4 console.log('hello'); 5 } 6 setTimeout(() => { 7 console.log('-----setTimeout'); 8 }, 0); 9 hello(); 10 console.log('a'); 11

...

3. 调用web api, 传入回调 和 0的参数,触发定时器,把回调放入 回调队列

...

6. 同步代码执行完毕,event loop 从回调队列中取出定时器的回调,执行放入调用堆栈

这里需要注意的是,当启用定时器的时候,就已经开始计时了,而不是等待同步代码执行完毕才计时(定时器线程进行计时操作)

这里有一个示例代码和一个视频演示

1 const a = () => console.log('a'); 2 const b = () => setTimeout(() => console.log('b'), 100); 3 const c = () => console.log('c'); 4 5 a(); 6 b(); 7 c(); 8 9 // a 10 // c 11 // b 12

此外还应该注意的一点为:event loop 每次只会从回调队列中读取一个调用到 调用堆栈,并直到调用堆栈再次为空(这里的空不包含上下文),才会再次从回调队列中读取

Promise

es5引入的promise与回调队列略有不同,它拥有自己的队列,并且优先级高于回调队列,就是说:event loop读取调用都会先查看 promise队列,如果有的话就会读取promise的回调,放入调用堆栈,如果没有的话才会去读取callback queue

示例如下:

1 console.log('a'); 2 setTimeout(() => console.log('b')); 3 new Promise((resolve, reject) => { 4 resolve(); 5 }) 6 .then(() => { 7 console.log('c'); 8 }); 9 console.log('d'); 10 11 // a 12 // d 13 // c 14 // b 15

因为promise的优先级高于setTimeout 所以他会先执行

promise的语法糖:async await 是一样的

1 console.log('a'); 2 setTimeout(() => console.log('setTimeout')); 3 async function hello() { 4 await new Promise((resolve) => { 5 resolve(123); 6 }); 7 console.log('await 之后'); 8 } 9 hello(); 10 console.log('d'); 11 12 // a 13 // d 14 // await 之后 15 // b 16

此外需要注意的是 promise构造函数里的代码为同步代码(当然一般都会在异步代码中执行 reslove ),即:

1 console.log('a'); 2 setTimeout(() => console.log('b'), 0); 3 new Promise((resolve, reject) => { 4+ console.log('promise constructor'); 5 resolve(); 6 }) 7 .then(() => { 8 console.log('c'); 9 }); 10 console.log('d'); 11 12 // a 13 // promise constructor 14 // d 15 // c 16 // b 17

这一点同样体现在 async await

1 setTimeout(() => { 2 console.log('setTimeout'); 3 }); 4 5 6 async function hello() { 7 console.log('async '); 8 await new Promise((resolve) => { 9 console.log('promise'); 10 setTimeout(() => { 11 resolve(123); 12 }); 13 }); 14 console.log('------'); 15 } 16 hello(); 17 console.log('a'); 18 19 // async 20 // promise 21 // a 22 // setTimeout 23 // ------ 24

参考链接:

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

https://felixgerschau.com/javascript-event-loop-call-stack/#how-javascript-works-in-the-browser