欢迎关注我的掘金鸭~
当我在看节流函数的时候,碰到了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函数,并返回另一个匿名函数。那么为什么在执行了 if判断的过程中,碰到了setTimeout,我们的sign并没有被改为true,从而一直的执行 if判断呢?那么就需要聊一聊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(事件循环)。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查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里的版本5有非常详细的解读。
安歌:浅谈js防抖和节流
阮一峰:JavaScript 运行机制详解:再谈Event Loop
前端小姐姐:彻底搞懂浏览器Event-loop
小美娜娜:Eventloop不可怕,可怕的是遇上Promise
隆金岑:js事件循环机制(浏览器端Event Loop) 以及async/await的理解