我们知道JavaScript是单线程,那它的工作流程到底是怎样的呢?
JS引擎
我们用的最多的就是谷歌的V8引擎。
内存堆
当我们写let a = '123'
时,引擎会在内存分配一个位置存放这个变量,这个区域叫内存堆Memory Heap
。
调用栈
当我们调用function printA () { console.log(a) }
时,引擎会把这个要执行的代码放入栈中,这个地方叫调用栈Call Stack
。
调用栈中最后入栈的代码会最先执行,执行完后会被移除。
因此这段代码的执行顺序是:printA
入栈 -> console.log
入栈 -> 执行console.log
-> console.log
出栈 -> printA
出栈,这段代码就执行完成了。
当我们写了个死循环时,调用栈中函数的调用次数超过了调用栈的大小,我们会看到这样的错误:
Web API
其中console
是浏览器提供的Web API
,还有比如DOM
,AJAX
,setTimeout
都属于Web API
。
异步
如果调用栈中的函数需要长时间执行,那么浏览器会被阻塞,于是引入了异步的概念,异步函数中有一个回调函数,意思是,当异步函数执行和其他阻塞结束后再执行回调函数。AJAX
和setTimeout
就是这样的。
事件循环
有了异步函数,事件循环就是用来监控调用栈和回调队列,调度执行回调函数的。
回调队列Callback Queue
比如我在某个元素上绑定了onClick
和onMouseEnter
的回调函数:
js
function onMouseEnter() {}
function onClick() {}
document.addEventListener('click', onMouseEnter);
document.addEventListener('click', onClick);
在浏览器中我的鼠标进入了这个元素的区域并点击了这个元素 ,那么Web API
会把这两个回调函数按鼠标的行为顺序放入回调队列中.
当调用栈为空时,事件循环会从回调队列中取第一个进入调用栈准备执行。事件循环遍历一遍回调队列称为一个tick
。
当我们写setTimeout(myCallback, 5000)
,其实意味着5秒后Web API
会把myCallback
放入回调队列,而只有当调用栈为空时,myCallback
才会被事件循环放入调用栈执行,因此如果调用栈有阻塞或回调队列中还有微任务Micro Task时,myCallback
都会等待更长时间才会被执行。
任务队列Task Queue
ES6引入了任务队列的概念,也就是升级版的回调队列。任务队列中有宏任务队列MacroTask Queue
和微任务队列MicroTask Queue
,事件循环会先执行当前宏任务队列,执行完后检查宏任务当中是否生成有微任务队列,执行完微任务队列后后再取下一个宏任务执行。
宏任务Macro Task
宏任务是颗粒度较大的任务,包括:
- 所有同步任务
- I/O操作,如文件读写、数据库数据读写等
setTimeout
、setInterval
setImmediate
(Node.js环境)requestAnimationFrame
- 事件监听回调函数等
- ...
微任务Micro Task
微任务是颗粒度较小,优先级较高的任务,包括:
Promise
的then
、catch
、finally
async/await
中的代码Generator
函数MutationObserver
process.nextTick
(Node.js 环境)- ...
举例
下面我们在某个元素上绑定onClick
和onMouseUp
,并在这两个回调函数中都加入异步函数:
js
function onMouseUp() {
setTimeout(() => {
console.log(1);
}, 0);
console.log(2)
}
function onClick () {
console.log(3);
new Promise(resolve => {
console.log(4);
}).then(() => {
console.log(5);
});
}
要想知道console
的顺序,我们先在代码中做出标记,参考注释:
js
function onMouseUp() { // <=== 回调函数,宏任务
setTimeout(() => { // <=== 回调函数中的异步函数,宏任务
console.log(1); // <=== 回调函数中的同步函数,宏任务
}, 0);
console.log(2) // <=== 回调函数中的同步函数,宏任务
}
function onClick () { // <=== 回调函数,宏任务
console.log(3); // <=== 回调函数中的同步函数,宏任务
new Promise(resolve => { // <=== 回调函数中的同步步函数,宏任务(新建promise是宏任务)
console.log(4); // <=== 回调函数中的同步函数,宏任务
resolve();
console.log(5);
}).then(() => { // <=== promise中的then是微任务
console.log(6); // <=== 回调函数中的同步函数,宏任务
});
console.log(7);
}
- 首先浏览器上我们点击那个元素,
dom
会先处理mouseUp
行为再处理click
行为:
- 由此我们可以得出一个任务队列:
Web API
会根据setTimeout
创建一个timer
:
- 任务队列中的任务会被依次入调用栈-执行-出调用栈:
此时console
台有:2,3.
new Promise
中的回调是立即执行的,是同步函数,而then
中的回调是异步函数,是微任务,此时任务队列如下:
此时console
台有:2,3.
- 任务队列的任务依次执行后,
timer
中的任务将最后执行:
此时console
台有:2,3,4,5,7,6.
- 随着最后一个任务执行,
console
出的数字依次为:2,3,4,5,7,6,1。