古叶峰 2020-11-16
JavaScript执行模型
引言
JavaScript是一个单线程(Single-threaded)异步(Asynchronous)非阻塞(Non-blocking)并发(Concurrent)语言,这些语言效果通过一个调用栈(Call Stack)、一个事件循环(Event Loop)、一个回调队列(Callback Queue)有些时候也叫任务队列(Task Queue)与跟运行环境相关的API组成。
概念
调用栈 Call Stack
调用栈是一个LIFO后进先出数据结构的函数运行栈,它内部的数据结构为函数帧。当在JavaScript中调用一个函数时,它将被压入栈中,当这个函数内部还有另一个函数被调用时,另一个函数将会被压入栈顶,直到其内部没有更多调用,栈顶函数将会被以单线程方式执行并出栈,直到最后一个函数帧出栈。JavaScript语言特性中的单线程就是指的调用栈的单线程运行。
function multiply(a, b) { return a * b; } function square(n) { return multiply(n, n) } function printSquare(n) { console.log(square(n)); } printSquare(4);
首先调用栈压入main()
,扫描到printSquare()
函数调用调用栈压入printSquare(4)
,printSquare
函数内部调用square(n)
该函数被压入栈,同理multiply(n, n)
函数也被压入栈且没有更多调用,JavaScript引擎开始执行栈顶函数multiply(n, n)
返回结果并出栈,以此类推直到main()函数出栈。
调用栈有一个意外情况,当函数递归调用其自身时调用栈将溢出,执行环境将报错。
function foo() { foo(); } foo();
任务队列 Task Queue
任务队列是WebAPI的一部分,也就是说它本身并不是ECMAScript标准的一部分,而是运行环境自行实现的。任务队列是所有回调函数排队执行的FIFO先进先出队列,它的单位是任务(Task),每个任务都关联着一个用于处理这个任务的回调函数。在事件循环(Event Loop)中会将任务队列内的函数压入调用栈执行并出队列,直至为空。
任务队列在浏览器的实现中被分为了宏任务队列(macrotask queue)和微任务队列(microtask queue),它们分别个自承载宏任务(macrotask)和微任务(microtask)的排队,其中宏任务队列与宏任务又被默认为常规的任务队列与任务。
当调用栈内所有调用都完成执行后,事件轮询会在每次处理宏任务队列的一个宏任务后处理微任务队列的全部微任务,也就是微任务基本会在宏任务处理之前被处理。微任务处理中间不会被UI或网络事件处理被执行,微任务执行是连续的。
会被添加到宏任务的方法的回调有:
会被添加到微任务的Web API方法有:
会被添加到微任务的Web API方法有:
微任务只会从我们编写的代码中产生,宏任务既可能从我们编写的代码中产生也可能从浏览器本身事件、渲染、IO产生。
事件循环 Event Loop
事件循环是JavaScript的事件处理机制,它会一直轮询消息队列,当满足调用栈为空且消息队列不为空时,它将把消息队列队头的消息压入执行栈。这样的机制保证了函数不会被中断,不会有线程切换带来的数据不一致等情况
事件循环在调用栈为空时轮询,顺序为
1.找到任务队列(宏任务队列)的最早被添加的任务并将其添加到调用栈执行
2.执行所有微任务队列内的任务
3.渲染所有变化
4.如果宏任务队列为空等待宏任务出现
5.返回步骤1
JavaScript运行时 Runtime
浏览器的JavaScript代码执行也就是调用栈与堆(用于储存变量对象等)由JavaScript引擎提供,用的比较多的是谷歌的V8引擎,Chrome、Edge浏览器、Nodejs均使用该引擎。
事件循环Event Loop、任务队列Task Queue(回调队列Callback Queue)、WebAPI或Node API由运行环境提供。