首先,JS是单线程语言,也就意味着同一个时间只能作一件事,那么html
假定JS同时有两个线程,一个线程在某个DOM节点上编辑了内容,而另外一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪一个操做呢?
因此,设计者把JS设计成单线程应该就很好理解了,为了不相似上述操做的复杂性,这一特征未来也不会变。编程
可是单线程有一个问题:一旦这个线程被阻塞就没法继续工做了,这确定是不行的segmentfault
因为异步编程能够实现“非阻塞”的调用效果,引入异步编程天然就是瓜熟蒂落的事情了,那么promise
今天的主咖登场——事件循环(Event Loop),JS异步是经过的事件循环实现的,理解了Event Loop机制,就理解
了JS的执行机制。浏览器
先来段代码:多线程
console.log(1) setTimeout(()=>{ console.log(2) }, 0) for(let i = 3; i < 10000; i++){ console.log(i) }
执行结果:1 3 4 5 6 7 ... 9997 9998 9999 2
setTimeout里的函数并无当即执行,咱们都知道这部分叫异步处理模块,延迟了一段时间,知足必定条件后才执行并发
仔细想一想,咱们在JS里一般把任务分为“同步任务”和“异步任务”,它们有如下的执行顺序:异步
- 判断任务是同步的仍是异步的,若是是同步任务就进入主线程执行栈中,若是是异步任务就进入Event Table并注册函数,当知足触发条件后,进入Event Queue
- 只有等到主线程的同步任务执行完后,才会去Event Queue中查找是否有可执行的异步任务,若有,则进入主线程执行
以上两步循环执行,就是所谓的Event Loop,因此上述代码里:异步编程
console.log(1) 是同步任务,进入主线程,当即执行
setTimeout 是异步任务,进入Event Table,0ms后(实际时间可能有出入,见注文)进入Event Queue,等待进入主线程
for 是同步任务,进入主线程,当即执行
全部主线程任务执行完后,setTimeout从Event Queue进入主线程执行
*注:HTML5规范规定最小延迟时间不能小于4ms,即x若是小于4,会被当作4来处理。 不过不一样浏览器的实现不同,好比,Chrome能够设置1ms,IE11/Edge是4ms
这就是我以前对Event Loop的理解,可是自从看了这篇文章深刻理解JS引擎的执行机制颠覆了我对Event Loop认识三观,看下面的代码函数
console.log("start") setTimeout(()=>{ console.log("setTimeout") }, 0) new Promise((resolve)=>{ console.log("promise") resolve() }).then(()=>{ console.log("then") }) console.log("end")
尝试按照咱们上面的JS执行机制去分析:
console.log("start")是同步任务,进入主线程,当即执行 setTimeout是异步任务,进入Event
Table,知足触发条件后进入Event Queue
new Promise是同步任务,进入主线程,当即执行
.then是异步任务,进入Event Table,知足触发条件后进入Event Queue,排在Event Queue队尾 console.log("end")是同步任务,进入主线程,当即执行
因此执行结果是:start > promise > end > setTimeout > then
But可是,亲自跑了代码结果倒是:start > promise > end > then > setTimeout
对比结果发现,难道Event Queue里面的顺序不是队列的先进先出的顺序吗?仍是这块执行时有什么改变,事实就是,前面按照同步和异步任务划分的方式并不许确,那么怎么划分才是准确的呢,先看图(转自谷雨JavaScript 异步、栈、事件循环、任务队列):
咣咣咣~敲黑板,知识点,知识点,知识点:
Js 中,有两类任务队列: 宏任务队列(macro tasks)和 微任务队列(micro tasks)
宏任务队列能够有多个,微任务队列只有一个。那么什么任务,会分到哪一个队列呢?
宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
微任务:process.nextTick, Promise的then或catch, Object.observer, MutationObserver.
那么结合上面的流程图和最初理解的执行机制,总结了一下更为准确的JS执行机制:
- 取且仅取一个宏任务来执行(第一个宏任务就是script任务)。执行过程当中判断是同步仍是异步任务,若是是同步任务就进入主线程执行栈中,若是是异步任务就进入异步处理模块,这些异步处理模块的任务当知足触发条件后,进入任务队列,进入任务队列后,按照宏任务和微任务进行划分,划分完毕后,执行下一步。
- 若是微任务队列不为空,则依次取出微任务来执行,直到微任务队列为空(即当前loop全部微任务执行完),执行下一步。
- 进入下一轮loop或更新UI渲染。
Event Loop就是循环执行上面三步,接下来使用上面的结论分析个例子帮助理解
console.log('第一轮'); setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout1 console.log('第二轮'); Promise.resolve().then(() => { //为了便于叙述时区分,标记为 then1 console.log('A'); }) }, 0); setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout2 console.log('第三轮'); console.log('B'); }, 0); new Promise((resolve)=>{ //为了便于叙述时区分,标记为 Promise1 console.log("C") resolve() }).then(() => { //为了便于叙述时区分,标记为 then2 Promise.resolve().then(() => { //为了便于叙述时区分,标记为 then3 console.log("D") setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout3 console.log('第四轮'); console.log('E'); }, 0); }); });
执行结果:第一轮 > C > D > 第二轮 > A > 第三轮 > B > 第四轮 > E
分析:
loop1:
第一步:首先执行全局宏任务,里面同步任务有下面两个,都当即进入主线程执行完后出栈
1.console.log('第一轮')
2.Promise1
输出 “第一轮” > “C”
异步任务有三个,分别进入相应的任务队列:
1.setTimeout1,该任务按照划分标准是 宏任务 setTimeout(() => { console.log('第二轮'); Promise.resolve().then(() => { console.log('A'); }) }, 0);
2.setTimeout2,该任务按照划分标准是 宏任务 setTimeout(() => { console.log('第三轮'); console.log('B'); }, 0);
3.then2,该任务按照划分标准是 微任务 .then(() => { Promise.resolve().then(() => { console.log("D") setTimeout(() => { console.log('第四轮'); console.log('E'); }, 0); }); });
因此此时宏任务队列为:setTimeout1,setTimeout2
微任务队列为:then2
第二步:loop1 微任务队列不为空,then2
出队列并执行,而后这个微任务里的 then3
继续进入微任务队列 ,setTimeout3
进入宏任务队列队尾
那么此时微任务队列为:then3
宏任务队列为:setTimeout1,setTimeout2,setTimeout3
可是此时第二步并无完,由于微任务队列只要不为空,就一直执行当前loop的微任务,因此从微任务队列取出 then3
执行输出 “D”
此时微任务队列为:空
宏任务队列为:setTimeout1,setTimeout2,setTimeout3
到目前为止,当前loop的微任务对列为空,进入下一个loop,输出状况是“第一轮” > “C” > “D”
loop2:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout1
执行输出“第二轮”
,并then1
进入微任务队列
此时微任务队列为:then1
宏任务队列为:setTimeout2,setTimeout3
第二步:loop2 微任务队列不为空,则从微任务队列取出then1
执行,输出“A”
此时微任务队列为:空
宏任务队列为:setTimeout2,setTimeout3
到目前为止,当前loop的微任务对列为空,进入下一个loop,输出状况是“第一轮” > “C” > “D” > “第二轮” > “A”
loop3:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout2
执行输出“第三轮” > “B”
此时微任务队列为:空
宏任务队列为:setTimeout3
第二步:因为loop3 微任务队列为空,则直接进入下一轮loop,输出状况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B”
loop4:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout3
执行输出“第四轮” > “E”
此时微任务队列为:空
宏任务队列为:空
第二步:因为loop4 微任务队列为空,宏任务队列也为空,则这次Event Loop结束,最终输出状况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B” > “第四轮” > “E”
上面的整个过程就是更为准确的Event Loop,下面还有个不一样的例子供读者自行尝试
console.log('第一轮'); setTimeout(() => { console.log('第二轮'); Promise.resolve().then(() => { console.log('A'); }) }, 0); setTimeout(() => { console.log('第三轮'); console.log('B'); }, 0); new Promise((resolve) => { console.log("C") resolve() }).then(() => { //注意,这个函数改动啦 setTimeout(() => { console.log('第四轮'); console.log('E'); Promise.resolve().then(() => { console.log("D") }); }, 0); });
执行结果:第一轮 > C > 第二轮 > A > 第三轮 > B > 第四轮 > E > D
深刻理解JS引擎的执行机制
JavaScript 异步、栈、事件循环、任务队列
JavaScript 运行机制详解:深刻理解Event Loop
JavaScript:并发模型与Event Loop
JavaScript 运行机制详解:再谈Event Loop[阮一峰]