Event Loop 事件循環(huán)

名詞解釋

"event-loop": 事件循環(huán)
"non-blocking": 非堵塞
"callback": 回調(diào)函數(shù)
"asynchronous": 異步
"single-threaded": 單線程
"concurrency": 并發(fā)
"web-api": DOM, ajax, setTimeout...

JS在瀏覽器中的環(huán)境

先看一張圖

圖片出自

V8引擎內(nèi)的JS

根據(jù)上圖,首先可以得到的JS在V8引擎中有一個堆(heap)和棧(stack)的概念
堆(heap): 對象被分配的區(qū)域
棧(stack): 函數(shù)調(diào)用形成的棧幀

問題1: 執(zhí)行JS時候發(fā)生了什么

代碼1

var a, b
function foo () {
  return a +=1
}
function bar () {
  return b += 2
}
function baz () {
  bar ()
  foo ()
  console.log( a + b )
}
baz()

解釋1

棧內(nèi):
1執(zhí)行baz() 進(jìn)入棧
2執(zhí)行bar() 進(jìn)入棧 - bar() return 退出棧
3執(zhí)行foo() 進(jìn)入棧 - foo() return 退出棧
4執(zhí)行console.log 進(jìn)入棧 無return并退出棧
5baz() 執(zhí)行完畢退出棧

JS操作WebApi

根據(jù)圖中WebApi所在的位置我們發(fā)現(xiàn)它并沒有在V8引擎內(nèi),而是由stack內(nèi)執(zhí)行后再V8資源外層出現(xiàn)然后進(jìn)入回調(diào)隊(duì)列,并進(jìn)行了一次event loop的事件

問題2: JS操作WebApi發(fā)生了什么?WebApi的執(zhí)行不在V8內(nèi)那在哪里?

代碼2

console.log('hi')
setTimeout(function () {
  console.log('ha')
}, 5000)
console.log('heng')

解釋2

棧內(nèi):
1執(zhí)行console.log('hi') 進(jìn)入棧 - 退出棧
2執(zhí)行setTimout 進(jìn)入棧 - 把回調(diào)函數(shù)cb放入瀏覽器資源內(nèi)(相對V8) - 退出棧
3執(zhí)行console.log('heng')進(jìn)入棧 - 退出棧
4當(dāng)前棧清空當(dāng)前事件循環(huán)(event loop)結(jié)束

棧外:
5通過while(queue.length)不停的檢查隊(duì)列(queue)是否為空
6存放在瀏覽器資源內(nèi)的setTimeout回調(diào)cb在5秒完成后進(jìn)入隊(duì)列(queue)
7事件循環(huán)while(queue.length)檢查到隊(duì)列(queue)有回調(diào)cb
8在當(dāng)前循環(huán)內(nèi)把cb推入棧內(nèi)

棧內(nèi):
9執(zhí)行cb,console.log('ha')進(jìn)入棧 - 退出棧
10清空棧

問題3: 如果setTimeout(cb, 0) 會是什么情況?

代碼3

console.log('hi')
setTimeout(function () {
  console.log('ha')
}, 0)
console.log('heng')

解釋3

同解釋2,但是再第6步: 存放在瀏覽器資源內(nèi)的setTimeout回調(diào)cb在5秒完成后進(jìn)入隊(duì)列(queue)
應(yīng)該變?yōu)閏b直接進(jìn)入隊(duì)列(queue)

問題4: ajax是什么情況?

代碼4

console.log('hi')
$.get(url, function (data) {
  console.log(data)
})
console.log('heng')

解釋4

同解釋2,但是再第6步: 存放在瀏覽器資源內(nèi)的setTimeout回調(diào)cb在5秒完成后進(jìn)入隊(duì)列(queue)
應(yīng)該變?yōu)閍jax取得數(shù)據(jù)后cb進(jìn)入隊(duì)列()
所以這也解釋了為什么使用setTimeout來模擬ajax

問題5: WebApi中Event事件是什么情況?

代碼5

console.log('start')
$el.on('click', function fn() {
    console.log('clicked')
})
setTimeout(function cb() {
    console.log('timeout')
}, 5000)
console.log('done')

解釋5

棧內(nèi):
1執(zhí)行console.log('start')進(jìn)入棧 - 退出棧
2執(zhí)行$el.on('click')進(jìn)入棧 - 整個click事件包括回調(diào)函數(shù)fn放入瀏覽器資源內(nèi) - 退出棧
3執(zhí)行setTImeout 進(jìn)入棧 - 把回調(diào)函數(shù)cb放入瀏覽器資源內(nèi) - 退出棧
4執(zhí)行console.log('done')進(jìn)入棧 - 退出棧

棧外:
5通過while(queue.length)不停的檢查隊(duì)列(queue)是否為空
6存放在瀏覽器資源內(nèi)的setTimeout回調(diào)cb在5秒完成后進(jìn)入隊(duì)列(queue)
7事件循環(huán)while(queue.length)檢查到隊(duì)列(queue)有回調(diào)cb
8在當(dāng)前循環(huán)內(nèi)把cb推入棧內(nèi)

棧內(nèi):
9執(zhí)行cb,console.log('ha')進(jìn)入棧 - 退出棧
10清空棧

瀏覽器中:
11用戶點(diǎn)擊$el觸發(fā)'click' 事件,回調(diào)函數(shù)fn進(jìn)入隊(duì)列中
12事件循環(huán)while(queue.length)檢查到隊(duì)列(queue)有回調(diào)fn
13在當(dāng)前循環(huán)內(nèi)把fn推入棧內(nèi)執(zhí)行并清空

問題6 - 列表滾動優(yōu)化與Debounce去抖函數(shù)

從問題5中可以知道,當(dāng)我們連續(xù)不停的點(diǎn)擊$el觸發(fā)click時,隊(duì)列(queue)內(nèi)將會排滿回調(diào)函數(shù),這就是頁面造成卡頓的原因。
造成這種情況出現(xiàn)最多的就是列表滾動scroll事件, 窗口resize事件。
常用的優(yōu)化方法就是使用debounce去抖函數(shù), 先看一下他的實(shí)現(xiàn)方法:

function debounce(fn, delay) {
    var timer
    return function() {
        var context = this
        var args = arguments
        clearTimeout(timer)
        timer = setTimeout(function() {
            fn.apply(context, args)
        }, delay)
    }
}

分析debounce
debounce函數(shù)里有一個重點(diǎn),就是clearTimeout(timer)
現(xiàn)在模擬一個綁定事件

document.addEventListener('scroll', debounce(
function() {
    console.log('scroll')
}, 1000), false);

當(dāng)scroll事件在棧內(nèi)執(zhí)行回調(diào)函數(shù)被注冊到瀏覽器資源后,當(dāng)我們觸發(fā)scroll事件時,我們都會把debounce(function(){console.log('scroll')}, 1000)排到隊(duì)列(queue)里,在通過事件循環(huán)放入棧內(nèi)執(zhí)行。

如果1秒內(nèi)只觸發(fā)1次,那么debounce函數(shù)的回調(diào)就會因?yàn)閮?nèi)部的setTimeout放入瀏覽器資源等到1秒到后排如隊(duì)列內(nèi)在推入棧內(nèi)執(zhí)行。?

但1秒內(nèi)我們不停的觸發(fā)scroll事件呢,那么debounce函數(shù)內(nèi)部的clearTimeout(timer)將起到關(guān)鍵作用: 把前一次觸發(fā)scroll事件放入瀏覽器資源的setTimeout回調(diào)給清空掉并放入新的setTimeout回調(diào)直到最后一次觸發(fā)scroll,把瀏覽器資源內(nèi)的setTimeout回調(diào)都清空只留下最后一個,等待1秒后回調(diào)排入隊(duì)列(queue)等待推入棧內(nèi)執(zhí)行。

此方法相比問題5中的情況大大減少了瀏覽器資源的占用,使得在固定時間內(nèi)隊(duì)列(queue)內(nèi)都只有一個回調(diào)在等待而不是一大堆。

異步執(zhí)行

代碼1
[1,2,3,4].forEach(function (i) {
    console.log(i)
})
代碼2
[1,2,3,4].forEach(function (i) {
    setTimeout(function (i) {
        console.log(i)
    }, 0, i)
})

分析
代碼1中打印1,2,3,4 很明顯它們都是直接在棧內(nèi)執(zhí)行console.log()輸出的
代碼2頁打印相同的結(jié)果,但是不同的是每次console.log的執(zhí)行都是通過setTImeout放入隊(duì)列(queue)內(nèi)再推入棧內(nèi)執(zhí)行的,這就通過瀏覽器資源和V8資源的區(qū)別實(shí)現(xiàn)了一段異步執(zhí)行的代碼
我們可以第二段代碼改寫成這樣, 制作一個異步執(zhí)行的forEach

function asyncForEach(arr, cb) {
    arr.forEach(function (i) {
        setTimeout(cb, 0, i)
    })
}
asyncForEach([1,2,3,4], function(i) {
    console.log(i)
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容