引子
当我在看节流函数的时候,碰到了setTimtout,于是从js运行机制挖到了event-loop。那么咱们就先从这个简单的节流函数看起。
// 节流:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。
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);
复制代码
那么这个节流函数是怎么实现的节流呢?
让我们来看一下它的执行步骤(假设我们一直不停的在滚动):
- 当我们打开页面,代码执行到
window.onscroll = throttle(foo, 1000)
就会直接执行 throttle函数,定义了一个变量sign
为 true,然后碰到了 return 跳出 throttle函数,并返回另一个匿名函数。 - 然后我们滚动页面,那么就会触发 onscroll 事件,执行 throttle函数。而此时我们的 throttle函数,实际就是执行 return 的那个匿名函数。因为闭包的缘故,保存了 sign的值(感觉还要填个闭包的坑...),此时的sign 是 true。就执行 if判断,把sign 改为 false。然后碰到了定时器,我们现在不用管定时器的回调函数的内容。
- 我们还一直在滚动,那么又触发了 onscroll事件,于是继续进行 if else 判断。此时 sign 已经是false了,什么都没有发生。
- 继续,我们一直不停的在滚动,还是触发了 onscroll事件,因为 sign 还是false,所以还是什么都没有发生。
- 一直重复步骤4,直到1s以后的那个 onscroll事件执行完成后,我们的setTimeout被执行了,首先执行了我们的需要被执行的fn()函数,然后把 sign置为 true。又开始跟前面一样,执行 if判断了。
那么为什么在执行了 if判断的过程中,碰到了setTimeout,我们的sign并没有被改为true,从而一直的执行 if判断呢?那么就需要聊一聊js的运行机制了。终于要进正题了,真不容易...
js运行机制
先看一下阮一峰大佬的
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
我自己归类就是js中有:
-
同步任务和异步任务
-
宏任务(macrotask)和微任务(microtask)
-
主线程(同步任务) - 所有同步任务都在主线程上执行,形成一个执行栈。
-
任务队列(异步任务):当异步任务有了结果,就在任务队列中放一个事件。
-
JS运行机制:当"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"
其中宏任务包括:script(主代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver
这里我们注意到,宏任务里有 script,也就是我们的正常执行的主代码。
事件循环 event-loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去任务队列中取下一个任务执行。
我又给总结了一下笼统的过程:script(宏任务) - 清空微任务队列 - 执行一个宏任务 - 清空微任务队列 - 执行一个宏任务, 如此往复。
- 先执行script里的同步代码(此时是宏任务)。碰到异步任务,放到任务队列。
- 查找任务队列有没有微任务,有就把此时的微任务全部按顺序执行 (这就是为什么promise会比setTimeout先执行,因为先执行的宏任务是同步代码,setTimeout被放进任务队列了,setTimeout又是宏任务,在它之前先得执行微任务(就比如promise))。
- 执行一个宏任务(先进到队列中的那个宏任务),再把这次宏任务里的宏任务和微任务放到任务队列。
- ...一直重复2、3步骤
要做到心中有队列,有先进先出的概念
借用前端小姐姐的一张图来解释:
现在再看开头的节流函数,就明白为什么碰到了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');
});
复制代码
根据前面的流程
- 执行script,看到了第一个 setTimeout 放入任务队列,看到了第二个 setTimeout 放到任务队列。看到了Promise.then() 放到任务队列,并没有同步代码。
- 检查微任务,发现了 Promise.then() 打印
Promise1
。 - 检查发现没有别的微任务了,检查宏任务,此时有两个宏任务(两个setTimeout),但是规则告诉我们,只执行一个宏任务,因为队列是先进先出的原则,执行先进入队列的那个 setTimeout,打印
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
。
第六关 async/await
截至到上一关,我本以为我已经完全掌握了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里的版本5有非常详细的解读。
参考文章:
阮一峰:JavaScript 运行机制详解:再谈Event Loop