事件循环(Event Loop)
JavaScript
是一门单线程的语言,在同一时间只能做一件事,js里面的代码要按照顺序逐行执行代码?比如说我们浏览新闻想要获取新闻图片,如果网络卡顿,获取了很长时间,难道就这样干等着吗?那肯定是不会的,给到用户的体验也是不好的
在js里所有的任务可以被分为同步任务 和异步任务 ,异步任务里又有微任务 和宏任务,
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务
事件循环运行机制
这种事件循环机制是由 JavaScript 的宿主环境来实现的,在浏览器运行环境中由浏览器内核引擎实现,而在 NodeJS
中则由 libuv 引擎实现。
主线程运行时候,产生堆(Heap
)和栈(Stack
),栈中的代码调用各种外部 API,它们在任务队列中加入各种事件。只要栈中的代码执行完毕,主线程就会通过事件循环机制读取任务队列,依次执行那些事件所对应的回调函数。
运行机制:
- 所有同步任务都在主线程上执行,形成一个 执行栈(Execution Context Stack)
- 主线程之外,还存在一个 任务队列 (Task Queue)。只要异步任务有了运行结果,就在 任务队列 之中放置一个事件
- 一旦 执行栈 中的所有同步任务执行完毕,系统就会读取 任务队列,看看里面有哪些待执行事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
- 主线程不断重复上面的第三步
浏览器环境
JavaScript
的异步任务根据事件分类分为两种:宏任务(MacroTask
)和微任务(MicroTask
)
微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
markdown
* Promise.then
* MutaionObserver
* Object.observe(已废弃;Proxy 对象替代)
* process.nextTick(Node.js)
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
markdown
* setTimeout/setInterval
* UI rendering/UI事件
* postMessage、MessageChannel
* setImmediate、I/O(Node.js)
微任务与宏任务的区别
宏任务与微任务的区别在于队列中事件的执行优先级。进入整体代码(宏任务)后,开始首次事件循环,当执行上下文栈清空后,事件循环机制会优先检测微任务队列中的事件并推至主线程执行,当微任务队列清空后,才会去检测宏任务队列中的事件,再将事件推至主线程中执行,而当执行上下文栈再次清空后,事件循环机制又会检测微任务队列,如此反复循环。
宏任务与微任务的优先级
- 宏任务的优先级高于微任务
- 每个宏任务执行完毕后都必须将当前的微任务队列清空
- 第一个
<script>
标签的代码是第一个宏任务 process.nextTick
优先级高于Promise.then
示例代码
js
setTimeout(function() {
console.log('1');
})
new Promise(function(resolve) {
console.log('2');
}).then(function() {
console.log('3');
})
console.log('4'); // 2 4 3 1
- 这段代码作为宏任务,进入主线程。
- 先遇到
setTimeout
,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述) - 接下来遇到了
Promise
,new Promise
立即执行,then
函数分发到微任务Event Queue。 - 遇到
console.log()
,立即执行。 - 好啦,整体代码
script
作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then
在微任务Event Queue里面,执行。 - ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务
Event Queue
中setTimeout
对应的回调函数,立即执行。 - 结束。
async与awite
async关键字是asynchronous(异步)的简写,用来声明一个函数是异步函数,写在函数的最前面,他会返回一个promise
对象
awite
可以理解为asynchronous waite(等待异步),他会等待一个异步任务返回的结果
下面这两种方法是等效的
js
function fn() {
return Promise.resolve('TEST');
}
async function asyncFn() {
return 'TEST';
}
awite
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
js
async function fn(){
// 等同于
// return 123
return await 123
}
fn().then(a => console.log(a)) // 123
不管await
后面跟着的是什么,await
都会阻塞后面的代码
js
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
//1 fn2 3 2
上面的例子中,await
会阻塞下面的代码(即加入微任务队列),先执行 async
外面的同步代码,同步代码执行完,再回到 async
函数中,再执行之前阻塞的代码