We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
当我在看节流函数的时候,碰到了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)
sign
那么为什么在执行了 if判断的过程中,碰到了setTimeout,我们的sign并没有被改为true,从而一直的执行 if判断呢?那么就需要聊一聊js的运行机制了。终于要进正题了,真不容易...
先看一下阮一峰大佬的
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 (4)主线程不断重复上面的第三步。
(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
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
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
遇到这种嵌套式的Promise不要慌,首先要心中有一个队列,能够将这些函数放到相对应的队列之中。
Ready GO
第一轮
[promise1]
第二轮
then11
promise2
第三轮
then21
then12
第四轮
then23
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 的语法糖这个大家应该都知道,但是打印之后跟我预期的不太一样,顿时有点儿蒙圈,后来一分析,原来如此。
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();
首先输出async1 start
async1 start
执行到await async2();,会从右向左执行,先执行async2(),打印async2,看见await,会阻塞代码去执行同步任务。
await async2();
async2()
async2
await
async/await仅仅影响的是函数内的执行,而不会影响到函数体外的执行顺序。也就是说async1()并不会阻塞后续程序的执行,await async2()相当于一个Promise,console.log("async1 end");相当于前方Promise的then之后执行的函数。
await async2()
console.log("async1 end");
如此一来,就可以得出上面的结果了。
但是,你也许打印出来会是下面这样的结果:
这个就跟V8有关系了(在chrome 71版本中,我打印出的是图片中的结果)。至于async/await和promise到底谁会先执行,这里偷个懒,大家看 小美娜娜:Eventloop不可怕,可怕的是遇上Promise里的版本5有非常详细的解读。
安歌:浅谈js防抖和节流
阮一峰:JavaScript 运行机制详解:再谈Event Loop
前端小姐姐:彻底搞懂浏览器Event-loop
小美娜娜:Eventloop不可怕,可怕的是遇上Promise
隆金岑:js事件循环机制(浏览器端Event Loop) 以及async/await的理解
The text was updated successfully, but these errors were encountered:
No branches or pull requests
引子
当我在看节流函数的时候,碰到了setTimtout,于是从js运行机制挖到了event-loop。那么咱们就先从这个简单的节流函数看起。
那么这个节流函数是怎么实现的节流呢?
让我们来看一下它的执行步骤(假设我们一直不停的在滚动):
window.onscroll = throttle(foo, 1000)
就会直接执行 throttle函数,定义了一个变量sign
为 true,然后碰到了 return 跳出 throttle函数,并返回另一个匿名函数。那么为什么在执行了 if判断的过程中,碰到了setTimeout,我们的sign并没有被改为true,从而一直的执行 if判断呢?那么就需要聊一聊js的运行机制了。终于要进正题了,真不容易...
js运行机制
先看一下阮一峰大佬的
我自己归类就是js中有:
同步任务和异步任务
宏任务(macrotask)和微任务(microtask)
主线程(同步任务) - 所有同步任务都在主线程上执行,形成一个执行栈。
任务队列(异步任务):当异步任务有了结果,就在任务队列中放一个事件。
JS运行机制:当"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"
其中宏任务包括:script(主代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver
这里我们注意到,宏任务里有 script,也就是我们的正常执行的主代码。
事件循环 event-loop
我又给总结了一下笼统的过程:script(宏任务) - 清空微任务队列 - 执行一个宏任务 - 清空微任务队列 - 执行一个宏任务, 如此往复。
要做到心中有队列,有先进先出的概念
借用前端小姐姐的一张图来解释:
现在再看开头的节流函数,就明白为什么碰到了setTimeout,我们的sign并没有被改为true了把。
那我们继续,看一下最近看到的爆款题。
开始闯关
第一关
看这段代码
对照这上面的执行过程不难得出结论,
script start -> script end -> Promise1 -> Promise2 -> setTimeout1
就算 setTimeout 不延时执行,它也会在 Promise之后执行,谁让js就是先执行同步代码,然后去找微任务再去找宏任务了呢。
懂了这里,那我们继续咯。
第二关
根据前面的流程
Promise1
。setTimeout1
。又发现了 一个 setTimeout,放进任务队列。看见了 Promise.then() ,打印setTimeout 里的 Promise
。setTimeout2
。setTimeout3
。搞清楚了这个,那我们再继续玩儿玩儿?
第三关
再来看看这个代码的执行结果呢。
script start -> Promise3 -> script end -> Promise1 -> Promise2 -> setTimeout1
有些朋友可能会说,不是说好了 Promise 是微任务,要在主代码执行以后才执行嘛,你个 Promise3 咋叛变了。
其实 Promise3 没有叛变,之前说的 Promise微任务是.then()执行的代码。而在new Promise的回调函数里的代码是同步任务。
第四关
我们继续看关于promise的
这个输出 2 -> 5 -> 3 -> 4 -> 1。你想对了嘛?
第五关
promise继续进化
直接上解释吧。
可能有人会对第二轮的队列表示疑问,为什么是 ”新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
的语法糖这个大家应该都知道,但是打印之后跟我预期的不太一样,顿时有点儿蒙圈,后来一分析,原来如此。这段代码也算是网红代码了,我已经不下三个地方见过了...
先仔细想一想应该输出什么,然后打印一下看看。(chrome 73版本打印结果)
直接从async开始看起吧。
当程序执行到了
async1();
的时候首先输出
async1 start
执行到
await async2();
,会从右向左执行,先执行async2()
,打印async2
,看见await
,会阻塞代码去执行同步任务。如此一来,就可以得出上面的结果了。
但是,你也许打印出来会是下面这样的结果:
这个就跟V8有关系了(在chrome 71版本中,我打印出的是图片中的结果)。至于async/await和promise到底谁会先执行,这里偷个懒,大家看 小美娜娜:Eventloop不可怕,可怕的是遇上Promise里的版本5有非常详细的解读。
参考文章:
安歌:浅谈js防抖和节流
阮一峰:JavaScript 运行机制详解:再谈Event Loop
前端小姐姐:彻底搞懂浏览器Event-loop
小美娜娜:Eventloop不可怕,可怕的是遇上Promise
隆金岑:js事件循环机制(浏览器端Event Loop) 以及async/await的理解
The text was updated successfully, but these errors were encountered: