EventLoop
eventLoop 是由 JS 的宿主环境(浏览器)来实现的;事件循环可以简单的描述为以下四个步骤:
/*
1. 函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
2. 此期间WebAPIs完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
3. 执行栈为空时,Event Loop把微任务队列执行清空;
4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入Stack(栈)中执行,回到第1步。
我们不禁要问了,那怎么知道主线程执行栈为空呢?js 引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数
js 是单线程,就像学生排队上厕所,学生需要排队一个一个上厕所,同理 js 任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:
同步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制
异步任务
异步任务又分为宏任务和微任务。微任务:then 、messageChannel 、mutationObersve;宏任务:setTimeout、setInterval、setTmmediate(只兼容 ie)。
js 异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入 eventqueue,然后在执行微任务,将微任务放入 eventqueue 最骚的是,这两个 queue 不是一个 queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的 queue 上拿宏任务的回掉函数。 我当时看到这我就服了还有这种骚操作。
微任务(Microtasks)
then 、messageChannel 、mutationObersve
- Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。
setTimeout(() => {
console.log("setTimeout1");
}, 0);
let p = new Promise((resolve, reject) => {
console.log("Promise1");
resolve();
});
p.then(() => {
console.log("Promise2");
});
// Promise1->Promise2->setTimeout1
Promise 参数中的 Promise1 是同步执行的 其次是因为 Promise 是 microtasks,会在同步任务执行完后会去清空 microtasks queues, 最后清空完微任务再去宏任务队列取值。
- process.nextTick(callback)
process.nextTick(callback)类似 node.js 版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("settimeout");
});
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("process");
});
console.log("script end");
// script start->async1 start->async2->promise1->script end->async1 end->process->promise2->settimeout->setImmediate
process.nextTick 会在微任务将执行时执行,所以会比.then 先一步执行。
宏任务(task)
包括整体代码 setTimeout、setInterval、setTmmediate(只兼容 ie)。
- setTimeout
setTimeout(() => {
task();
}, 3000);
sleep(10000000);
乍一看其实差不多嘛,但我们把这段代码在 chrome 执行一下,却发现控制台执行 task()需要的时间远远超过 3 秒,说好的延时三秒,为啥现在需要这么长时间啊? 这时候我们需要重新理解 setTimeout 的定义。我们先说上述代码是怎么执行的:
/*
task()进入Event Table并注册,计时开始。
执行sleep函数,很慢,非常慢,计时仍在继续。
3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
- setTimeout(fn,0)
我们还经常遇到 setTimeout(fn,0)这样的代码,0 秒后执行又是什么意思呢?是不是可以立即执行呢?
//代码1
console.log("先执行这里");
setTimeout(() => {
console.log("执行啦");
}, 0);
答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:
- setInterval
上面说完了 setTimeout,当然不能错过它的孪生兄弟 setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval 会每隔指定的时间将注册的函数置入 Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于 setInterval(fn,ms)来说,我们已经知道不是每过 ms 秒会执行一次 fn,而是每过 ms 秒,会有 fn 进入 Event Queue。一旦 setInterval 的回调函数 fn 执行时间超过了延迟时间 ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。
Node 的 EventLoop
Node 环境中微任务是插缝执行,(如果执行宏任务的时候发现了微任务, 不会像浏览器一样执行了,而是将为微任务放到微任务队列中,等待整个宏 任务队列执行完毕或者达到执行上线后,下一个阶段开始的时候先执行 完微任务队列中的任务)。
- 认识 Node 中的任务源(task)
/*
微任务: then 、nextTick 、 messageChannel 、mutationObersve
宏任务:setTimeout 、setInterval 、setImmediate 、io 文件操作