目錄
- JS 異步編程
- 并發(concurrency)和并行(parallelism)區別
- 回調函數(Callback)
- Generator
- Promise
- async 及 await
- 常用定時器函數
JS 異步編程
并發(concurrency)和并行(parallelism)區別
涉及面試題:并發與并行的區別?
這兩個名詞確實是很多人都常會混淆的知識點。其實混淆的原因可能只是兩個名詞在中文上的相似,在英文上來說完全是不同的單詞。
并發是宏觀概念,我分別有任務 A 和任務 B,在一段時間內通過任務間的切換完成了這兩個任務,這種情況就可以稱之為并發。
并行是微觀概念,假設 CPU 中存在兩個核心,那么我就可以同時完成任務 A、B。同時完成多個任務的情況就可以稱之為并行。
回調函數(Callback)
涉及面試題:什么是回調函數?回調函數有什么缺點?如何解決回調地獄問題?
回調函數應該是大家經常使用到的,以下代碼就是一個回調函數的例子:
ajax(url, () => {// 處理邏輯
})
但是回調函數有一個致命的弱點,就是容易寫出回調地獄(Callback hell)。假設多個請求存在依賴性,你可能就會寫出如下代碼:
ajax(url, () => {// 處理邏輯ajax(url1, () => {// 處理邏輯ajax(url2, () => {// 處理邏輯})})
})
以上代碼看起來不利于閱讀和維護,當然,你可能會想說解決這個問題還不簡單,把函數分開來寫不就得了
function firstAjax() {ajax(url1, () => {// 處理邏輯secondAjax()})
}
function secondAjax() {ajax(url2, () => {// 處理邏輯})
}
ajax(url, () => {// 處理邏輯firstAjax()
})
以上的代碼雖然看上去利于閱讀了,但是還是沒有解決根本問題。
回調地獄的根本問題就是:
- 嵌套函數存在耦合性,一旦有所改動,就會牽一發而動全身
- 嵌套函數一多,就很難處理錯誤
當然,回調函數還存在著別的幾個缺點,比如不能使用 try catch
捕獲錯誤,不能直接 return
。在接下來的幾小節中,我們將來學習通過別的技術解決這些問題。
Generator
涉及面試題:你理解的 Generator 是什么?
Generator
算是 ES6 中難理解的概念之一了,Generator
最大的特點就是可以控制函數的執行。在這一小節中我們不會去講什么是 Generator
,而是把重點放在 Generator
的一些容易困惑的地方。
function *foo(x) {let y = 2 * (yield (x + 1))let z = yield (y / 3)return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
你也許會疑惑為什么會產生與你預想不同的值,接下來就讓我為你逐行代碼分析原因
- 首先
Generator
函數調用和普通函數不同,它會返回一個迭代器 - 當執行第一次
next
時,傳參會被忽略,并且函數暫停在yield (x + 1)
處,所以返回5 + 1 = 6
- 當執行第二次
next
時,傳入的參數等于上一個yield
的返回值,如果你不傳參,yield
永遠返回undefined
。此時let y = 2 * 12
,所以第二個yield
等于2 * 12 / 3 = 8
- 當執行第三次
next
時,傳入的參數會傳遞給z
,所以z = 13, x = 5, y = 24
,相加等于42
Generator
函數一般見到的不多,其實也于他有點繞有關系,并且一般會配合 co 庫去使用。當然,我們可以通過 Generator
函數解決回調地獄的問題,可以把之前的回調地獄例子改寫為如下代碼:
function *fetch() {yield ajax(url, () => {})yield ajax(url1, () => {})yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
Promise
涉及面試題:Promise 的特點是什么,分別有什么優缺點?什么是 Promise 鏈?Promise 構造函數執行和 then 函數執行有什么區別?
Promise
翻譯過來就是承諾的意思,這個承諾會在未來有一個確切的答復,并且該承諾有三種狀態,分別是:
- 等待中(pending)
- 完成了 (resolved)
- 拒絕了(rejected)
這個承諾一旦從等待狀態變成為其他狀態就永遠不能更改狀態了,也就是說一旦狀態變為 resolved 后,就不能再次改變
new Promise((resolve, reject) => {resolve('success')// 無效reject('reject')
})
當我們在構造 Promise
的時候,構造函數內部的代碼是立即執行的
new Promise((resolve, reject) => {console.log('new Promise')resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
Promise
實現了鏈式調用,也就是說每次調用 then
之后返回的都是一個 Promise
,并且是一個全新的 Promise
,原因也是因為狀態不可變。如果你在 then
中 使用了 return
,那么 return
的值會被 Promise.resolve()
包裝
Promise.resolve(1).then(res => {console.log(res) // => 1return 2 // 包裝成 Promise.resolve(2)}).then(res => {console.log(res) // => 2})
當然了,Promise
也很好地解決了回調地獄的問題,可以把之前的回調地獄例子改寫為如下代碼:
ajax(url).then(res => {console.log(res)return ajax(url1)}).then(res => {console.log(res)return ajax(url2)}).then(res => console.log(res))
前面都是在講述 Promise
的一些優點和特點,其實它也是存在一些缺點的,比如無法取消 Promise
,錯誤需要通過回調函數捕獲。
async 及 await
涉及面試題:async 及 await 的特點,它們的優點和缺點分別是什么?await 原理是什么?
一個函數如果加上 async
,那么該函數就會返回一個 Promise
async function test() {return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}
async
就是將函數返回值使用 Promise.resolve()
包裹了下,和 then
中處理返回值一樣,并且 await
只能配套 async
使用
async function test() {let value = await sleep()
}
async
和 await
可以說是異步終極解決方案了,相比直接使用 Promise
來說,優勢在于處理 then
的調用鏈,能夠更清晰準確的寫出代碼,畢竟寫一大堆 then
也很惡心,并且也能優雅地解決回調地獄問題。當然也存在一些缺點,因為 await
將異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await
會導致性能上的降低。
async function test() {// 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式// 如果有依賴性的話,其實就是解決回調地獄的例子了await fetch(url)await fetch(url1)await fetch(url2)
}
下面來看一個使用 await
的例子:
let a = 0
let b = async () => {a = a + await 10console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
對于以上代碼你可能會有疑惑,讓我來解釋下原因
- 首先函數
b
先執行,在執行到await 10
之前變量a
還是 0,因為await
內部實現了generator
,generator
會保留堆棧中東西,所以這時候a = 0
被保存了下來 - 因為
await
是異步操作,后來的表達式不返回Promise
的話,就會包裝成Promise.reslove(返回值)
,然后會去執行函數外的同步代碼 - 同步代碼執行完畢后開始執行異步代碼,將保存下來的值拿出來使用,這時候
a = 0 + 10
上述解釋中提到了 await
內部實現了 generator
,其實 await
就是 generator
加上 Promise
的語法糖,且內部實現了自動執行 generator
。如果你熟悉 co 的話,其實自己就可以實現這樣的語法糖。
常用定時器函數
涉及面試題:setTimeout、setInterval、requestAnimationFrame 各有什么特點?
異步編程當然少不了定時器了,常見的定時器函數有 setTimeout
、setInterval
、requestAnimationFrame
。我們先來講講最常用的setTimeout
,很多人認為 setTimeout
是延時多久,那就應該是多久后執行。
其實這個觀點是錯誤的,因為 JS 是單線程執行的,如果前面的代碼影響了性能,就會導致 setTimeout
不會按期執行。當然了,我們可以通過代碼去修正 setTimeout
,從而使定時器相對準確
let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = intervalfunction loop() {count++// 代碼執行所消耗的時間let offset = new Date().getTime() - (startTime + count * interval);let diff = end - new Date().getTime()let h = Math.floor(diff / (60 * 1000 * 60))let hdiff = diff % (60 * 1000 * 60)let m = Math.floor(hdiff / (60 * 1000))let mdiff = hdiff % (60 * 1000)let s = mdiff / (1000)let sCeil = Math.ceil(s)let sFloor = Math.floor(s)// 得到下一次循環所消耗的時間currentInterval = interval - offset console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執行時間:'+offset, '下次循環間隔'+currentInterval) setTimeout(loop, currentInterval)
}setTimeout(loop, currentInterval)
接下來我們來看 setInterval
,其實這個函數作用和 setTimeout
基本一致,只是該函數是每隔一段時間執行一次回調函數。
通常來說不建議使用 setInterval
。第一,它和 setTimeout
一樣,不能保證在預期的時間執行任務。第二,它存在執行累積的問題,請看以下偽代碼
function demo() {setInterval(function(){console.log(2)},1000)sleep(2000)
}
demo()
以上代碼在瀏覽器環境中,如果定時器執行過程中出現了耗時操作,多個回調函數會在耗時操作結束以后同時執行,這樣可能就會帶來性能上的問題。
如果你有循環定時器的需求,其實完全可以通過 requestAnimationFrame
來實現
function setInterval(callback, interval) {let timerconst now = Date.nowlet startTime = now()let endTime = startTimeconst loop = () => {timer = window.requestAnimationFrame(loop)endTime = now()if (endTime - startTime >= interval) {startTime = endTime = now()callback(timer)}}timer = window.requestAnimationFrame(loop)return timer
}let a = 0
setInterval(timer => {console.log(1)a++if (a === 3) cancelAnimationFrame(timer)
}, 1000)
首先 requestAnimationFrame
自帶函數節流功能,基本可以保證在 16.6 毫秒內只執行一次(不掉幀的情況下),并且該函數的延時效果是精確的,沒有其他定時器時間不準的問題,當然你也可以通過該函數來實現 setTimeout
。