并發(fā)(concurrency)和并行(parallelism)區(qū)別
涉及面試題:并發(fā)與并行的區(qū)別?
并發(fā)是宏觀概念,我分別有任務(wù) A 和任務(wù) B,在一段時(shí)間內(nèi)通過任務(wù)間的切換完成了這兩個(gè)任務(wù),這種情況就可以稱之為并發(fā)。
并行是微觀概念,假設(shè) CPU 中存在兩個(gè)核心,那么我就可以同時(shí)完成任務(wù) A、B。同時(shí)完成多個(gè)任務(wù)的情況就可以稱之為并行。
回調(diào)函數(shù)(Callback)
涉及面試題:什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點(diǎn)?如何解決回調(diào)地獄問題?
回調(diào)函數(shù)應(yīng)該是大家經(jīng)常使用到的,以下代碼就是一個(gè)回調(diào)函數(shù)的例子:
ajax(url, () => {
// 處理邏輯
})
但是回調(diào)函數(shù)有一個(gè)致命的弱點(diǎn),就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個(gè)請(qǐng)求存在依賴性,你可能就會(huì)寫出如下代碼:
ajax(url, () => {
// 處理邏輯
ajax(url1, () => {
// 處理邏輯
ajax(url2, () => {
// 處理邏輯
})
})
})
以上代碼看起來不利于閱讀和維護(hù),當(dāng)然,你可能會(huì)想說解決這個(gè)問題還不簡(jiǎn)單,把函數(shù)分開來寫不就得了
function firstAjax() {
ajax(url1, () => {
// 處理邏輯
secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
// 處理邏輯
})
}
ajax(url, () => {
// 處理邏輯
firstAjax()
})
以上的代碼雖然看上去利于閱讀了,但是還是沒有解決根本問題。
回調(diào)地獄的根本問題就是:
- 嵌套函數(shù)存在耦合性,一旦有所改動(dòng),就會(huì)牽一發(fā)而動(dòng)全身
- 嵌套函數(shù)一多,就很難處理錯(cuò)誤
當(dāng)然,回調(diào)函數(shù)還存在著別的幾個(gè)缺點(diǎn),比如不能使用 try catch 捕獲錯(cuò)誤,不能直接 return。
Generator
涉及面試題:你理解的 Generator 是什么?
Generator 算是 ES6 中難理解的概念之一了,Generator 最大的特點(diǎn)就是可以控制函數(shù)的執(zhí)行。
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}
你也許會(huì)疑惑為什么會(huì)產(chǎn)生與你預(yù)想不同的值,接下來就讓我為你逐行代碼分析原因
- 首先
Generator函數(shù)調(diào)用和普通函數(shù)不同,它會(huì)返回一個(gè)迭代器 - 當(dāng)執(zhí)行第一次
next時(shí),傳參會(huì)被忽略,并且函數(shù)暫停在yield (x + 1)處,所以返回5 + 1 = 6 - 當(dāng)執(zhí)行第二次
next時(shí),傳入的參數(shù)等于上一個(gè)yield的返回值,如果你不傳參,yield永遠(yuǎn)返回undefined。此時(shí)let y = 2 * 12,所以第二個(gè)yield等于2 * 12 / 3 = 8 - 當(dāng)執(zhí)行第三次
next時(shí),傳入的參數(shù)會(huì)傳遞給z,所以z = 13, x = 5, y = 24,相加等于42
Generator 函數(shù)一般見到的不多,其實(shí)也于他有點(diǎn)繞有關(guān)系,并且一般會(huì)配合co 庫去使用。當(dāng)然,我們可以通過 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
涉及面試題:Promise 的特點(diǎn)是什么,分別有什么優(yōu)缺點(diǎn)?什么是 Promise 鏈?Promise 構(gòu)造函數(shù)執(zhí)行和 then 函數(shù)執(zhí)行有什么區(qū)別?
Promise翻譯過來就是承諾的意思,這個(gè)承諾會(huì)在未來有一個(gè)確切的答復(fù),并且該承諾有三種狀態(tài),分別是:
- 等待中(pending)
- 完成了 (resolved)
- 拒絕(rejected)
這個(gè)承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠(yuǎn)不能更改狀態(tài)了,也就是說一旦狀態(tài)變?yōu)?resolved 后,就不能再次改變
new Promise((resolve, reject) => {
resolve('success')
// 無效
reject('reject')
})
當(dāng)我們?cè)跇?gòu)造 Promise 的時(shí)候,構(gòu)造函數(shù)內(nèi)部的代碼是立即執(zhí)行的
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
Promise 實(shí)現(xiàn)了鏈?zhǔn)秸{(diào)用,也就是說每次調(diào)用 then 之后返回的都是一個(gè) Promise,并且是一個(gè)全新的 Promise,原因也是因?yàn)闋顟B(tài)不可變。如果你在 then 中 使用了 return,那么 return 的值會(huì)被 Promise.resolve() 包裝
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包裝成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
當(dāng)然了,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)點(diǎn)和特點(diǎn),其實(shí)它也是存在一些缺點(diǎn)的,比如無法取消 Promise,錯(cuò)誤需要通過回調(diào)函數(shù)捕獲。
async 及 await
涉及面試題:async 及 await 的特點(diǎn),它們的優(yōu)點(diǎn)和缺點(diǎn)分別是什么?await 原理是什么?
一個(gè)函數(shù)如果加上 async ,那么該函數(shù)就會(huì)返回一個(gè) 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)勢(shì)在于處理 then的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼,畢竟寫一大堆then 也很惡心,并且也能優(yōu)雅地解決回調(diào)地獄問題。當(dāng)然也存在一些缺點(diǎn),因?yàn)?await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒有依賴性卻使用了await會(huì)導(dǎo)致性能上的降低。
async function test() {
// 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式
// 如果有依賴性的話,其實(shí)就是解決回調(diào)地獄的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}
下面來看一個(gè)使用 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 (先輸出)
對(duì)于以上代碼你可能會(huì)有疑惑,讓我來解釋下原因
- 首先函數(shù)
b先執(zhí)行,在執(zhí)行到await 10之前變量a還是0,因?yàn)?await內(nèi)部實(shí)現(xiàn)了generator,generator會(huì)保留堆棧中東西,所以這時(shí)候a = 0被保存了下來 - 因?yàn)?
await是異步操作,后來的表達(dá)式不返回Promise的話,就會(huì)包裝成Promise.reslove(返回值),然后會(huì)去執(zhí)行函數(shù)外的同步代碼 - 同步代碼執(zhí)行完畢后開始執(zhí)行異步代碼,將保存下來的值拿出來使用,這時(shí)候
a = 0 + 10
上述解釋中提到了await 內(nèi)部實(shí)現(xiàn)了 generator ,其實(shí) await 就是 generator 加上 Promise 的語法糖,且內(nèi)部實(shí)現(xiàn)了自動(dòng)執(zhí)行 generator 。如果你熟悉 co 的話,其實(shí)自己就可以實(shí)現(xiàn)這樣的語法糖。
常用定時(shí)器函數(shù)
涉及面試題:setTimeout、setInterval、requestAnimationFrame 各有什么特點(diǎn)?
異步編程當(dāng)然少不了定時(shí)器了,常見的定時(shí)器函數(shù)有 setTimeout、setInterval、requestAnimationFrame。我們先來講講最常用的setTimeout,很多人認(rèn)為 setTimeout 是延時(shí)多久,那就應(yīng)該是多久后執(zhí)行。
其實(shí)這個(gè)觀點(diǎn)是錯(cuò)誤的,因?yàn)?JS 是單線程執(zhí)行的,如果前面的代碼影響了性能,就會(huì)導(dǎo)致 setTimeout不會(huì)按期執(zhí)行。當(dāng)然了,我們可以通過代碼去修正 setTimeout,從而使定時(shí)器相對(duì)準(zhǔn)確
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í)行所消耗的時(shí)間
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)所消耗的時(shí)間
currentInterval = interval - offset
console.log('時(shí):'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時(shí)間:'+offset, '下次循環(huán)間隔'+currentInterval)
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)
接下來我們來看 setInterval,其實(shí)這個(gè)函數(shù)作用和 setTimeout 基本一致,只是該函數(shù)是每隔一段時(shí)間執(zhí)行一次回調(diào)函數(shù)。
通常來說不建議使用 setInterval。第一,它和 setTimeout 一樣,不能保證在預(yù)期的時(shí)間執(zhí)行任務(wù)。第二,它存在執(zhí)行累積的問題,請(qǐng)看以下偽代碼
function demo() {
setInterval(function(){
console.log(2)
},1000)
sleep(2000)
}
demo()
以上代碼在瀏覽器環(huán)境中,如果定時(shí)器執(zhí)行過程中出現(xiàn)了耗時(shí)操作,多個(gè)回調(diào)函數(shù)會(huì)在耗時(shí)操作結(jié)束以后同時(shí)執(zhí)行,這樣可能就會(huì)帶來性能上的問題。
如果你有循環(huán)定時(shí)器的需求,其實(shí)完全可以通過 requestAnimationFrame 來實(shí)現(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í)效果是精確的,沒有其他定時(shí)器時(shí)間不準(zhǔn)的問題,當(dāng)然你也可以通過該函數(shù)來實(shí)現(xiàn) setTimeout。