当我在看节流函数的时候,碰到了setTimtout,因而从js运行机制挖到了event-loop。那么我们就先从这个简单的节流函数看起。html
// 节流:若是短期内大量触发同一事件,那么在函数执行一次以后,该函数在指定的时间期限内再也不工做,直至过了这段时间才从新生效。 function throttle (fn, delay) { let sign = true; return function () { // 闭包,保存变量的值,防止每次执行次函数,值都被重置 if (sign) { sign = false; setTimeout (() => { fn(); sign = true; }, delay); } else { return false; } } } window.onscroll = throttle(foo, 1000); 复制代码
那么这个节流函数是怎么实现的节流呢?前端
让咱们来看一下它的执行步骤(假设咱们一直不停的在滚动):node
window.onscroll = throttle(foo, 1000)
就会直接执行 throttle函数,定义了一个变量 sign
为 true,而后碰到了 return 跳出 throttle函数,并返回另外一个匿名函数。那么为何在执行了 if判断的过程当中,碰到了setTimeout,咱们的sign并无被改成true,从而一直的执行 if判断呢?那么就须要聊一聊js的运行机制了。终于要进正题了,真不容易...chrome
先看一下阮一峰大佬的segmentfault
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。promise
(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。浏览器
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。bash
(4)主线程不断重复上面的第三步。markdown
我本身归类就是js中有:闭包
同步任务和异步任务
宏任务(macrotask)和微任务(microtask)
主线程(同步任务) - 全部同步任务都在主线程上执行,造成一个执行栈。
任务队列(异步任务):当异步任务有告终果,就在任务队列中放一个事件。
JS运行机制:当"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列"
其中宏任务包括:script(主代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver
这里咱们注意到,宏任务里有 script,也就是咱们的正常执行的主代码。
主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)。此机制具体以下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去任务队列中取下一个任务执行。
我又给总结了一下笼统的过程:script(宏任务) - 清空微任务队列 - 执行一个宏任务 - 清空微任务队列 - 执行一个宏任务, 如此往复。
要作到心中有队列,有先进先出的概念
借用前端小姐姐的一张图来解释:
如今再看开头的节流函数,就明白为何碰到了setTimeout,咱们的sign并无被改成true了把。
那咱们继续,看一下最近看到的爆款题。
看这段代码
console.log('script start'); setTimeout(() => { console.log('setTimeout1'); }, 0); new Promise((resolve) => { resolve('Promise1'); }).then((data) => { console.log(data); }); new Promise((resolve) => { resolve('Promise2'); }).then((data) => { console.log(data); }); console.log('script end'); 复制代码
对照这上面的执行过程不可贵出结论,script start -> script end -> Promise1 -> Promise2 -> setTimeout1
就算 setTimeout 不延时执行,它也会在 Promise以后执行,谁让js就是先执行同步代码,而后去找微任务再去找宏任务了呢。
懂了这里,那咱们继续咯。
setTimeout(() => { console.log('setTimeout1'); setTimeout(() => { console.log('setTimeout3'); }, 0); Promise.resolve().then(data=>{ console.log('setTimeout 里的 Promise'); }); }, 0); setTimeout(() => { console.log('setTimeout2'); }, 0); Promise.resolve().then(() => { console.log('Promise1'); }); 复制代码
根据前面的流程
Promise1
。setTimeout1
。又发现了 一个 setTimeout,放进任务队列。看见了 Promise.then() ,打印setTimeout 里的 Promise
。setTimeout2
。setTimeout3
。搞清楚了这个,那咱们再继续玩儿玩儿?
console.log('script start'); setTimeout(() => { console.log('setTimeout1'); }, 0); new Promise((resolve) => { console.log('Promise3'); resolve(); }).then(() => { console.log('Promise1'); }); new Promise((resolve) => { resolve(); }).then(() => { console.log('Promise2'); }); console.log('script end'); 复制代码
再来看看这个代码的执行结果呢。
script start -> Promise3 -> script end -> Promise1 -> Promise2 -> setTimeout1
有些朋友可能会说,不是说好了 Promise 是微任务,要在主代码执行之后才执行嘛,你个 Promise3 咋叛变了。
其实 Promise3 没有叛变,以前说的 Promise微任务是.then()执行的代码。而在new Promise的回调函数里的代码是同步任务。
咱们继续看关于promise的
setTimeout(()=>{ console.log(1) },0); let a=new Promise((resolve)=>{ console.log(2) resolve() }).then(()=>{ console.log(3) }).then(()=>{ console.log(4) }); console.log(5); 复制代码
这个输出 2 -> 5 -> 3 -> 4 -> 1。你想对了嘛?
这个要从Promise的实现来讲,Promise的executor是一个同步函数,即非异步,当即执行的一个函数,所以他应该是和当前的任务一块儿执行的。而Promise的链式调用then,每次都会在内部生成一个新的Promise,而后执行then,在执行的过程当中不断向微任务(microtask)推入新的函数,所以直至微任务(microtask)的队列清空后才会执行下一波的macrotask。
promise继续进化
new Promise((resolve,reject)=>{ console.log("promise1") resolve() }).then(()=>{ console.log("then11") new Promise((resolve,reject)=>{ console.log("promise2") resolve() }).then(()=>{ console.log("then21") }).then(()=>{ console.log("then23") }) }).then(()=>{ console.log("then12") }) 复制代码
直接上解释吧。
遇到这种嵌套式的Promise不要慌,首先要心中有一个队列,可以将这些函数放到相对应的队列之中。
Ready GO
第一轮
- current task: promise1是当之无愧的当即执行的一个函数,参考上一章节的executor,当即执行输出
[promise1]
- micro task queue: [promise1的第一个then]
第二轮
- current task: then1执行中,当即输出了
then11
以及新promise2的promise2
- micro task queue: [新promise2的then函数,以及promise1的第二个then函数]
第三轮
- current task: 新promise2的then函数输出
then21
和promise1的第二个then函数输出then12
。- micro task queue: [新promise2的第二then函数]
第四轮
- current task: 新promise2的第二then函数输出
then23
- micro task queue: []
END
可能有人会对第二轮的队列表示疑问,为何是 ”新promise2的then函数“ 先进了队列,而后才是 ”promise1的第二个then函数“ 进入队列?”新promise2的第二then函数“ 为何有没有在这一轮中进入到队列中来呢?
看不懂不要紧,咱们来调试一下代码:
在打印完 promise2
之后,19行先执行到了 })
这里,而后到了then这里。
再下一步,到了 promise1的第二个})
这里了。并无执行20行的console.log。
由此看出:promise2的第一个then进入任务队列中了。并无被执行.then()。
继续执行,打印 then21
。
由此得出:promise1的第二个then放入异步队列中,并无被执行。程序执行到这里,宏任务算是执行完了。检查微任务,此时队列中放着 [ '新promise2的then函数', 'promise1的第二个then函数'] ,也就是第二轮所写的队列。
这一步,到了promise2的二个then前面的})
。
往下执行到了这里,又碰到了异步,放入队列中去。
此时队列: [ 'promise1的第二个then函数' ,'promise2的第二个then函数' ]
打印 promise1 的 then12
。
先进先出,因此先执行了 'promise1的第二个then函数' 。
此时队列: [ 'promise2的第二个then函数' ]
最后才输出了 then23
。
截至到上一关,我本觉得我已经彻底掌握了event-loop。后来我看到了 async/await , async await是generator
和 Promise
的语法糖这个你们应该都知道,可是打印以后跟我预期的不太同样,顿时有点儿蒙圈,后来一分析,原来如此。
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"); },0); async1(); new Promise(function (resolve) { console.log("promise1"); resolve(); }).then(function () { console.log("promise2"); }); console.log('script end'); 复制代码
这段代码也算是网红代码了,我已经不下三个地方见过了...
先仔细想想应该输出什么,而后打印一下看看。(chrome 73版本打印结果)
script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
复制代码
直接从async开始看起吧。
当程序执行到了async1();
的时候
首先输出async1 start
执行到await async2();
,会从右向左执行,先执行async2()
,打印async2
,看见await
,会阻塞代码去执行同步任务。
async/await仅仅影响的是函数内的执行,而不会影响到函数体外的执行顺序。也就是说async1()并不会阻塞后续程序的执行,
await async2()
至关于一个Promise,console.log("async1 end");
至关于前方Promise的then以后执行的函数。
如此一来,就能够得出上面的结果了。
可是,你也许打印出来会是下面这样的结果:
这个就跟V8有关系了(在chrome 71版本中,我打印出的是图片中的结果)。至于async/await和promise到底谁会先执行,这里偷个懒,你们看 小美娜娜:Eventloop不可怕,可怕的是赶上Promise里的版本4有很是详细的解读。
先看第一个代码,思考一下答案
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'); 复制代码
再看下面的代码,思考一下答案,会是不同的吗?
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') }, 1000) 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
process
async1 end
promise2
setTimeout
setImmediate
复制代码
第二个
script start
async1 start
async2
promise1
script end
process
async1 end
promise2
setImmediate
setTimeout
复制代码
阿勒?setTimeout和setImmediate顺序竟然不同了。这是为啥呢
由于 setImmediate 是在I/O回调只有当即执行。
对于以上代码来讲,setTimeout 可能执行在前,也可能执行在后。 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 进入事件循环也是须要成本的,若是在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调 若是准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
我的测试了一下,setTimeout(fn, 2)的时候,也是先执行setTimeout,若是设置为3ms或者以上的时候,会先执行setImmediate。
但当两者在异步i/o callback内部调用时,老是先执行setImmediate,再执行setTimeout
const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }) // immediate // timeout 复制代码
这个函数实际上是独立于 Event Loop 以外的,它有一个本身的队列,当每一个阶段完成后,若是存在 nextTick 队列,就会清空队列中的全部回调函数,而且优先于其余 microtask 执行。
setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) }) }) // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1 复制代码
阮一峰:JavaScript 运行机制详解:再谈Event Loop
小美娜娜:Eventloop不可怕,可怕的是赶上Promise