JS是单线程的,执行顺序是从上到下执行。事件循环是目前浏览器和NodeJS处理JavaScript代码的一种机制。JS的顺序执行会存在一些问题,比如当一个语句需要执行很长时间的话,如请求数据、定时器、读取文件等,后面的语句就得一直等着前面的语句执行结束后才会开始执行。所以JS将所有执行任务分为了同步任务和异步任务。每个任务都是在做两件事情,就是发起调用和得到结果。
JS是单线程的原因
假设JS同时有两个线程,一个是操作A节点,一个是删除A节点,这时候浏览器就不知道要以哪个线程为准了。因此为了避免这类型的问题,JS从一开始就是单线程的语言。
调用栈(执行栈)
在JavaScript运行的时候,主线程会形成一个栈,这个栈被称为调用栈或者执行栈。具有后进先出的特性。调用栈内存放的是代码执行期间的所有执行上下文。
- 每调用一个函数,解释器就会把该函数的执行上下文添加到调用栈并开始执行。
- 正在调用栈中执行的函数,如果还调用了其他函数,那么新函数也会被添加到调用栈,并立即执行。
- 当前函数执行完毕后,解释器会将其执行上下文清除调用栈,继续执行剩余执行上下文中的剩余代码。
- 当分配的调用栈空间被占满,会引发“堆栈溢出”的报错。
同步任务
同步任务发起调用后,很快就可以得到结果。同步任务的执行会按照代码顺序调用,进入调用栈中并执行,执行结束后从调用栈移除。
异步任务
异步任务是无法立即得到结果,比如请求接口、定时器。异步任务的执行,首先它依旧会进入调用栈中,然后发起调用,然后解释器会将其回调任务放入一个任务队列,紧接着调用栈会将这个任务移除。当主线程清空后,即所有同步任务结束后(也就是setTimeout时间有延误的原因,当同步任务十分耗时的时候,setTimeout并不能及时执行),解释器会读取任务队列,并依次将已完成的异步任务加入调用栈中并执行。
异步任务回调进入任务队列
异步任务回调进入任务队列,其实会利用到浏览器的其他线程。虽然说JavaScript是单线程语言,但是浏览器不是单线程的。而不同的线程就会对不同的事件进行处理,当对应事件可以执行的时候,对应线程就会将其放入任务队列。
- js引擎线程:用于解释执行js代码、用户输入、网络请求等。
- GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果)。
- http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列。
- 定时触发器线程:setInterval、setTimeout等待时间结束后,会把执行函数推入任务队列中。
- 浏览器事件处理线程:将click、mouse等UI交互事件发生后,将要执行的回调函数放入到任务队列中。
// 后者的定时器会先被推进宏任务队列,而前者会在之后再被推入宏任务队列
setTimeout(() => {
console.log('a');
}, 10000);
setTimeout(() => {
console.log('b');
}, 100);
宏任务和微任务
在任务队列中,其实还分为宏任务队列和微任务队列,对应的里面存放的就是宏任务和微任务。
- macro-task(宏任务):包括整体js代码script,setTimeout,setInterval,setImmediate,I/O 操作,UI 渲染。
- micro-task(微任务):Promise的then方法,process.nextTick,MutationObserver。
script(整体代码块)是宏任务
如果同时存在两个script代码块,会首先执行第一个script代码块中的同步代码,如果这个过程中创建了微任务并进入了微任务队列,第一个script同步代码执行完之后,会首先去清空微任务队列,再去开启第二个script代码块的执行。所以就可以理解script(整体代码块)是宏任务。
Promise.then微任务的注册和执行过程
- .then的执行顺序是先注册的先执行, .then的注册微任务队列和执行是分离的, .then对应的同步代码执行完之后则开始注册.then。
- .then的链式调用的注册时机是依赖前一个.then的执行完成的, 而非链式的调用的注册时机则是同步注册。
事件循环流程
- 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行。
- 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止。
- 当微任务队列清空后,一个事件循环结束。
- 接着从宏任务队列中,找到下一个执行的宏任务,开始第二次事件循环,直至宏任务队列清空为止。
事件循环注意点
- 第一次执行的时候,解释器会将整体代码script放入宏任务队列中,因此事件循环是从script这个宏任务开始的。
- 一次事件循环中,宏任务永远在微任务之前执行。完成一个宏任务后,执行余下的所有微任务。
- 微任务按放入队列的顺序执行,先放入的先执行,如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。
页面渲染响应
- 当一次事件循环结束后,即一个宏任务执行完成后以及微任务队列被清空后,浏览器就会进行一次页面更新渲染。
<body>
<div id="demo"></div>
</body>
<script>
// innerText时并不会直接更新渲染当promise的then执行完alert('开始渲染!'),一次事件循环结束才会更新渲染。
const demoEl = document.getElementById('demo');
console.log('a');
setTimeout(() => {
alert('渲染完成!')
console.log('b');
}, 0)
new Promise(resolve => {
console.log('c');
resolve()
}).then(() => {
console.log('d');
alert('开始渲染!')
})
console.log('e');
demoEl.innerText = 'Hello World!';
</script>
- 页面不再响应
// 一直在执行微任务
function foo() {
return Promise.resolve().then(foo);
};
promise.then + setTimeout示例
- 第一轮事件循环执行script宏任务。
- setTimeout会作为第二轮的宏任务执行。
- 打印promise,promise的then方法会作为第一轮的微任务执行。
- 打印console。
- 执行第一轮的微任务打印then。
- 执行第二轮的宏任务打印setTimeout。
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise(resolve => {
console.log('promise');
resolve('dyx');
}).then(res => {
console.log('then');
})
console.log('console');
// 执行结果
- promise
- console
- then
- setTimeout
- 第一轮事件循环执行script宏任务。
- promise的then方法作为第一轮的微任务执行。
- setTimeout1会作为第二轮事件循环的宏任务。
- 然后执行第一轮的微任务打印promise1,然后将setTimeout2作为第三轮事件循环的宏任务(每一次事件循环只能执行一个宏任务)。
- 执行第二轮的宏任务,打印setTimeout1。
- 此时将promise2作为第二轮的微任务执行,所以打印promise2。
- 然后执行第三轮宏任务打印setTimeout2。
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => {
console.log('setTimeout2');
}, 0);
})
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
// 执行结果
- promise1
- setTimeout1
- promise2
- setTimeout2
- 第一次事件循环
- 1
- 6
- 8
- 10
- 7 // 微任务
- 第二次事件循环
- 2
- 3
- 5
- 4 // 微任务
- 第三次事件循环
- 9
console.log(1);
setTimeout(() => {
console.log(2);
new Promise(resolve => {
console.log(3);
resolve(4);
console.log(5);
}).then(res => {
console.log(res);
});
}, 0);
new Promise(resolve => {
console.log(6);
resolve(7);
console.log(8);
}).then(res => {
console.log(res);
});
setTimeout(() => {
console.log(9);
}, 0);
console.log(10);