名詞解釋
"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)
})