JavaScript異步編程常見面試題匯總
在上一節(jié)中我們了解了常見的es6語法的一些知識點。這一章節(jié)我們將會學習異步編程這一塊內(nèi)容,鑒于異步編程是js中至關(guān)重要的內(nèi)容,所以我們將會用三個章節(jié)來學習異步編程涉及到的重點和難點,同時這一塊內(nèi)容也是面試??挤秶?。
并發(fā)(concurrency)和并行(parallelism)的區(qū)別
面試題 并發(fā)和并行的區(qū)別?
異步和這一小節(jié)的知識點其實并不是一個概念,但是這個兩個名詞確實是很多人混淆的知識點,其實混淆的原因可能只是兩個名詞在中文的相似,在英文上來說完全是不同的單詞。
并發(fā)是宏觀概念,我分別有任務(wù)A和任務(wù)B,在一段時間內(nèi)通過任務(wù)間的切換完成了這兩個任務(wù),這種情況就可以成為并發(fā)。
并行是微觀概念,假設(shè)cpu中存在兩個核心,那么我就可以同時完成任務(wù)A,B。同時完成多個任務(wù)的情況就可以稱之為并行。
回調(diào)函數(shù)(callback)
面試題: 什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點?如何解決回調(diào)地獄問題?
回調(diào)函數(shù)應(yīng)該是大家經(jīng)常使用到的,以下代碼是回調(diào)函數(shù)的例子:
ajax(url,()=>{ //處理邏輯 })
但是回調(diào)函數(shù)有個致命的弱點,就是容易寫出回調(diào)地獄,假設(shè)多個請求存在依賴性,你可能就會寫出如下代碼:
ajax(url,()=>{ ajax(url,()=>{}) })
以上代碼看起來不利于閱讀和維護,當然你可能會說解決這個問題還不簡單,把函數(shù)分開來寫不就得了
function firstAjax(){ ajax(url1,()=>{ secondAjax() }) } function second(){ ajax(url2,()=>{ }) } ajax(url,()=>{ firstAjax() })
以上代碼看上去有利于閱讀了,但是還是沒有解決根本問題
回調(diào)地獄得根本問題是:
- 嵌套函數(shù)存在耦合性,一旦有改動,就會牽一發(fā)而動全身
- 嵌套函數(shù)一多就很難處理錯誤
當然,回調(diào)函數(shù)還存在著別的缺點,比如不能使用try catch捕獲錯誤,不能直接return。
Generator
面試題:你理解的generator是什么?
Generator算是es6中難理解的概念之一了,Generator最大的特點就是可以控制函數(shù)的執(zhí)行。在這一小節(jié)中我們不會講什么是Generator,而把重點放在Generator的一些容易困惑的地方。
function *foo(){ let y = 2*(yield(x+1)) let z = yield(y/3) return (x+y+z) } let it = foo(5) console.log(it.next()) console.log(it.next(12)) console.log(it.next(13))
你也許會疑惑為什么會產(chǎn)生與你預(yù)想不同的值,接下來就讓我為你逐行代碼分析原因
- 首先 Generator 函數(shù)調(diào)用和普通函數(shù)不同,它會返回一個迭代器
- 當執(zhí)行第一次 next 時,傳參會被忽略,并且函數(shù)暫停在 yield (x + 1) 處,所以返回 5 + 1 = 6
- 當執(zhí)行第二次 next 時,傳入的參數(shù)等于上一個 yield 的返回值,如果你不傳參,yield 永遠返回 undefined。此時 let y = 2 12,所以第二個 yield 等于 2 12 / 3 = 8
- 當執(zhí)行第三次 next 時,傳入的參數(shù)會傳遞給 z,所以 z = 13, x = 5, y = 24,相加等于 42
Generator 函數(shù)一般見到的不多,其實也于他有點繞有關(guān)系,并且一般會配合 co 庫去使用。當然,我們可以通過 Generator 函數(shù)解決回調(diào)地獄的問題,可以把之前的回調(diào)地獄例子改寫為如下代碼:
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
翻譯過來就是承諾的意思,這個承諾會在未來有一個確切的答復,并且該承諾有三種狀態(tài),分別是:
- 等待中(pending)
- 完成了 (resolved)
- 拒絕了(rejected)
這個承諾一旦從等待狀態(tài)變成其他狀態(tài)就永遠不能更改狀態(tài)了,也就是說一旦狀態(tài)編為resolved后就不能再次改變
new Promise((resolve, reject) => { resolve('success') // 無效 reject('reject') })
當我們在構(gòu)造 Promise 的時候,構(gòu)造函數(shù)內(nèi)部的代碼是立即執(zhí)行的
new Promise((resolve, reject) => { console.log('new Promise') resolve('success') }) console.log('finifsh') // new Promise -> finifsh
Promise 實現(xiàn)了鏈式調(diào)用,也就是說每次調(diào)用 then 之后返回的都是一個 Promise,并且是一個全新的 Promise,原因也是因為狀態(tài)不可變。如果你在 then 中 使用了 return,那么 return 的值會被 Promise.resolve() 包裝
Promise.resolve(1) .then(res => { console.log(res) // => 1 return 2 // 包裝成 Promise.resolve(2) }) .then(res => { console.log(res) // => 2 })
當然了,Promise 也很好地解決了回調(diào)地獄的問題,可以把之前的回調(diào)地獄例子改寫為如下代碼:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))
前面都是在講述 Promise 的一些優(yōu)點和特點,其實它也是存在一些缺點的,比如無法取消 Promise,錯誤需要通過回調(diào)函數(shù)捕獲。
async 及 await
面試題:async 及 await 的特點,它們的優(yōu)點和缺點分別是什么?await 原理是什么?
一個函數(shù)如果加上 async ,那么該函數(shù)就會返回一個 Promise
async function test() { return "1" } console.log(test()) // -> Promise {<resolved>: "1"}
async 就是將函數(shù)返回值使用 Promise.resolve() 包裹了下,和 then 中處理返回值一樣,并且 await 只能配套 async 使用
async function test() { let value = await sleep() }
async 和 await 可以說是異步終極解決方案了,相比直接使用 Promise 來說,優(yōu)勢在于處理 then 的調(diào)用鏈,能夠更清晰準確的寫出代碼,畢竟寫一大堆 then 也很惡心,并且也能優(yōu)雅地解決回調(diào)地獄問題。當然也存在一些缺點,因為 await 將異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await 會導致性能上的降低。
async function test() { // 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式 // 如果有依賴性的話,其實就是解決回調(diào)地獄的例子了 await fetch(url) await fetch(url1) await fetch(url2) }
下面來看一個使用 await 的例子:
let a = 0 let b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 } b() a++ console.log('1', a) // -> '1' 1
對于以上代碼你可能會有疑惑,讓我來解釋下原因
- 首先b先執(zhí)行,在執(zhí)行await 10之前變量a還是0,因為await內(nèi)部實現(xiàn)了generator,generator會保留堆棧中東西,所以這個時候a = 0被保存下來
- 因為await是異步操作,后來的表達式不返回promise的話,就會包裝成Promise.resolve(返回值),然后去執(zhí)行函數(shù)外的同步代碼
- 同步代碼執(zhí)行完畢后開始執(zhí)行異步代碼,將保存下來的值拿出來使用,這時候 a = 0 + 10
上述解釋中提到了 await 內(nèi)部實現(xiàn)了 generator,其實 await 就是 generator 加上 Promise 的語法糖,且內(nèi)部實現(xiàn)了自動執(zhí)行 generator。如果你熟悉 co 的話,其實自己就可以實現(xiàn)這樣的語法糖。
常用定時器
面試題: setTimeout,setInterval,requestAnimationFrame 各有什么特點?
異步編程當然少不了定時器,常見的定時器函數(shù)有setTimeout,setInterval,requestAnimationFrame。我們先來講講最常用的setTimeout,很多人認為setTimeout是延遲多久,那就應(yīng)該是多久后執(zhí)行。
其實這個觀點是錯誤的,因為js是單線程執(zhí)行的,如果前面的代碼影響了性能,就會導致setTimeout不會按期執(zhí)行。當然了,我們可以通過代碼修正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 = interval function loop() { count++ // 代碼執(zhí)行所消耗的時間 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) // 得到下一次循環(huán)所消耗的時間 currentInterval = interval - offset console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時間:'+offset, '下次循環(huán)間隔'+currentInterval) setTimeout(loop, currentInterval) } setTimeout(loop, currentInterval)
接下來我們來看 setInterval,其實這個函數(shù)作用和 setTimeout 基本一致,只是該函數(shù)是每隔一段時間執(zhí)行一次回調(diào)函數(shù)。
通常來說不建議使用 setInterval。第一,它和 setTimeout 一樣,不能保證在預(yù)期的時間執(zhí)行任務(wù)。第二,它存在執(zhí)行累積的問題,請看以下偽代碼
function demo() { setInterval(function(){ console.log(2) },1000) sleep(2000) } demo()
以上代碼在瀏覽器環(huán)境中,如果定時器執(zhí)行過程中出現(xiàn)了耗時操作,多個回調(diào)函數(shù)會在耗時操作結(jié)束以后同時執(zhí)行,這樣可能就會帶來性能上的問題。
如果你有循環(huán)定時器的需求,其實完全可以通過 requestAnimationFrame 來實現(xiàn)
function setInterval(callback, interval) { let timer const now = Date.now let startTime = now() let endTime = startTime const 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 自帶函數(shù)節(jié)流功能,基本可以保證在 16.6 毫秒內(nèi)只執(zhí)行一次(不掉幀的情況下),并且該函數(shù)的延時效果是精確的,沒有其他定時器時間不準的問題,當然你也可以通過該函數(shù)來實現(xiàn) setTimeout。
以上就是JavaScript異步編程常見面試題匯總的詳細內(nèi)容,更多關(guān)于JavaScript異步編程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入理解JavaScript系列(27):設(shè)計模式之建造者模式詳解
這篇文章主要介紹了深入理解JavaScript系列(27):設(shè)計模式之建造者模式詳解,建造者模式可以將一個復雜對象的構(gòu)建與其表示相分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示,需要的朋友可以參考下2015-03-03JS中改變this指向的方法(call和apply、bind)
this是javascript的一個關(guān)鍵字,隨著函數(shù)使用場合不同,this的值會發(fā)生變化。但是總有一個原則,那就是this指的是調(diào)用函數(shù)的那個對象,通過本文給大家介紹JS中改變this指向的方法(call和apply、bind),需要的朋友參考下2016-03-03js定時調(diào)用方法成功后并停止調(diào)用示例
這篇文章主要介紹了js定時調(diào)用方法成功后并停止調(diào)用的實現(xiàn),需要的朋友可以參考下2014-04-04D3.js 從P元素的創(chuàng)建開始(顯示可加載數(shù)據(jù))
D3是一個基于數(shù)據(jù)操作的可視化js庫,認識d3,就從最基礎(chǔ)的顯示可加載數(shù)據(jù)談起,需要的朋友可以參考下2014-10-10使用element-plus時重寫樣式不起作用的問題及解決方法
這篇文章給大家介紹使用element-plus時重寫樣式不起作用的問題及解決方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-09-09