這個前端學習筆記是學習gitchat上的一個課程,這個課程的質量非常好,價格也不貴,非常時候前端入門的小夥伴們進階。
筆記不會涉及很多,主要是提取一些知識點,詳細的大家最好去過一遍教程,相信你一定會有很大的收穫
文章目錄
異步
JS是單線程的,瀏覽器是如何進行異步處理的?宏任務和微任務的區別是什麼?
從 callback 到 promise,從 generator 到 async/await,到底應該如何更優雅地實現異步操作?
我們來使用異步完成一個需求
移動頁面上元素 target(document.querySelectorAll(’#man’)[0])
先從原點出發,向左移動 20px,之後再向上移動 50px,最後再次向左移動 30px,請把運動動畫實現出來。
-
使用回調函數來實現
它是一個上下左右移動的需求,所以分析下需要的參數:
- 方向
- 移動的距離
- 完成之後的回調
var targetDom = { x:0, y:0 } const walk = (direction, distantce, callback) =>{ setTimeout(()=>{ //0.1 秒移動1px if(distantce <= 0) { // 結束 callback && callback() }else { distantce-=1 switch(direction) { case 'left':targetDom.x--;break; case 'right':targetDom.x++;break; case 'up':targetDom.y++;break; case 'down':targetDom.y--;break; default:break; } } },100) } walk('left', 20, () => { walk('top', 50, () => { walk('left', 30, Function.prototype) }) })
-
promise方案
var targetDom = { x:0, y:0 } const walk = (dir, distance)=>{ return new Promise((res, rej)=>{ const innerWalk = () => { setTimeout(()=>{ //0.1 秒移動1px if(distantce <= 0) { // 結束 res() }else { distantce-=1 switch(direction) { case 'left':targetDom.x--;break; case 'right':targetDom.x++;break; case 'up':targetDom.y++;break; case 'down':targetDom.y--;break; default:break; } innerWalk()// 循環 直到結束 } },100) } innerWalk() }) } walk('left', 20) .then(() => walk('top', 50)) .then(() => walk('left', 30))
-
generator方案
var targetDom = { x:0, y:0 } const walk = (dir, distance)=>{ return new Promise((res, rej)=>{ const innerWalk = () => { setTimeout(()=>{ //0.1 秒移動1px if(distantce <= 0) { // 結束 res() }else { distantce-=1 switch(direction) { case 'left':targetDom.x--;break; case 'right':targetDom.x++;break; case 'up':targetDom.y++;break; case 'down':targetDom.y--;break; default:break; } innerWalk()// 循環 直到結束 } },100) } innerWalk() }) } function *taskGenerator() { yield walk('left', 20) yield walk('top', 50) yield walk('left', 30) } const gen = taskGenerator() gen.next() gen.next() gen.next()
我們設置好generator之後,手動執行即可
-
async/await 方案
var targetDom = { x:0, y:0 } const walk = (dir, distance)=>{ return new Promise((res, rej)=>{ const innerWalk = () => { setTimeout(()=>{ //0.1 秒移動1px if(distantce <= 0) { // 結束 res() }else { distantce-=1 switch(direction) { case 'left':targetDom.x--;break; case 'right':targetDom.x++;break; case 'up':targetDom.y++;break; case 'down':targetDom.y--;break; default:break; } innerWalk()// 循環 直到結束 } },100) } innerWalk() }) } const task = async function () { await walk('left', 20) await walk('top', 50) await walk('left', 30) } task()
紅綠燈題
我們繼續來用例題熟悉異步
紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重複亮燈?
三個亮燈函數已經存在
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
-
callback方案
const light = (time, color, callback) =>{ setTimeout(()=>{ switch(color) { case 'red':red();break; case 'green':green();break; case 'yellow':yellow();break; default:break; } callback&&callback() },time) } const step = () => { light(3000, 'red', () => { light(1000, 'green', () => { light(2000, 'yellow', step) }) }) } step()
-
promise方案
const light => (timer, color)=> new Promise((res, rej)=> { setTimeout(()=>{ switch(color) { case 'red':red();break; case 'green':green();break; case 'yellow':yellow();break; default:break; } res() },timer) }) const step = () => { light(3000, 'red') .then(() => task(1000, 'green')) .then(() => task(2000, 'yellow')) .then(step) } step()
-
async/awit方案
const light => (timer, color)=> new Promise((res, rej)=> { setTimeout(()=>{ switch(color) { case 'red':red();break; case 'green':green();break; case 'yellow':yellow();break; default:break; } res() },timer) }) const taskRunner = async () => { await light(3000, 'red') await light(1000, 'green') await light(2000, 'yellow') taskRunner() } taskRunner()
async/awai如何使用?
async/awai
本身是一個語法糖,爲了改善promise
的寫法和提高可讀性。
我們處理一個異步獲得的數據的使用對比下使用promise的寫法。
// promise寫法
const request = () =>{
getData().then((data)=>{
console.log(data)
})
}
request()
// async/awai
const makeRequest = async () => {
console.log(await getData())
return "done"
}
makeRequest()
對比純Promise而言,需要使用then
來獲取返回的異步數據。
而使用async/awati
的話,await
後的promise直接返回異步處理的數據,可讀性和寫法都是更佳的。
當遇到await
的時候,會等待後面的函數執行完成後,纔會繼續執行後面的函數。
await
一般是接promise函數的,如果不是promise
函數會通過promise.resolve()
轉換成
Async/await究竟好在哪裏?
簡約而乾淨Concise and clean
我們看一下上面兩處代碼的代碼量,就可以直觀地看出使用Async/await對於代碼量的節省是很明顯的。對比Promise,我們不需要書寫.then,不需要新建一個匿名函數處理響應,也不需要再把數據賦值給一個我們其實並不需要的變量。同樣,我們避免了耦合的出現。這些看似很小的優勢其實是很直觀的,在下面的代碼示例中,將會更加放大。
錯誤處理Error handling
使用try/catch
更加方便
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getdata())
console.log(data)
}
catch (err) {
console.log(err)
}
}
條件判別Conditionals
雖然promise解決了回調地獄,但是遇到多層promise嵌套的時候,promise的可讀性也不行。
但是如果使用async/await
,就可以避免多層嵌套的寫法。
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
}
else {
console.log(data)
return data
}
}
中間值
一個經常出現的場景是,我們先調起promise1,然後根據返回值,調用promise2,之後再根據這兩個Promises得值,調取promise3。使用Promise,我們不難實現:
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
使用promise.all
更加優雅地寫
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}
Promise.all
這個方法犧牲了語義性,但是得到了更好的可讀性。
但是其實,把value1 & value2一起放到一個數組中,是很“蛋疼”的,某種意義上也是多餘的。
同樣的場景,使用async/await
會非常簡單:
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
錯誤堆棧信息Error stacks
如果多層嵌套promise中有一個出了錯誤,我們是無法判斷哪個出了問題。並且,不能保證後續的promise沒有問題。
如果使用async/await
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
})
也許這樣的對比,對於在本地開發階段區別不是很大。但是想象一下在服務器端,線上代碼的錯誤日誌情況下,將會變得非常有意義。你一定會覺得上面這樣的錯誤信息,比“錯誤出自一個then的then的then。。。”有用的多。
調試Debugging
最後一點,但是也是很重要的一點,使用async/await
來debug會變得非常簡單。
在一個返回表達式的箭頭函數中,我們不能設置斷點,這就會造成下面的局面:
const makeRequest = () => {
return callAPromise()
.then(()=>callAPromise())
.then(()=>callAPromise())
.then(()=>callAPromise())
.then(()=>callAPromise())
}
我們無法在每一行設置斷點。但是使用async/await
時:
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
}
複雜的真實案例
-
請求圖片的預加載
const loadImg = urlId => { const url = `https://www.image.com/${urlId}` return new Promise((resolve, reject) => { const img = new Image() img.onerror = function() { reject(urlId) } img.onload = function() { resolve(urlId) } img.src = url }) }
該方法進行 promise 化(promisify),在圖片成功加載時進行 resolve,加載失敗時 reject。
const urlIds = [1, 2, 3, 4, 5] urlIds.reduce((prevPromise, urlId) => { return prevPromise.then(() => loadImg(urlId)) }, Promise.resolve())
如果想要一次性請求全部圖片,如何請求?
const urlIds = [1, 2, 3, 4, 5] const promiseArray = urlIds.map(urlId => loadImg(urlId)) Promise.all(promiseArray) .then(() => { console.log('finish load all') }) .catch(() => { console.log('promise all catch') })
如果限制一次併發請求爲3個,如何做到呢?
const loadByLimit = (urlIds, loadImg, limit) => { const urlIdsCopy = […urlIds] if (urlIdsCopy.length <= limit) { // 如果數組長度小於最大併發數,直接全部請求 const promiseArray = urlIds.map(urlId => loadImg(urlId)) return Promise.all(promiseArray) } // 注意 splice 方法會改變 urlIdsCopy 數組 const promiseArray = urlIdsCopy.splice(0, limit).map(urlId => loadImg(urlId)) urlIdsCopy.reduce( (prevPromise, urlId) => prevPromise .then(() => Promise.race(promiseArray)) .catch(error => {console.log(error)}) .then(resolvedId => { // 將 resolvedId 剔除出 promiseArray 數組 // 這裏的刪除只是僞代碼,具體刪除情況要看後端 Api 返回結果 let resolvedIdPostion = promiseArray.findIndex(id => resolvedId === id)// 找到異步成功的結果 刪除指定位置 promiseArray.splice(resolvedIdPostion, 1) // 將一個新的 promiseArray.push(loadImg(urlId)) }) , Promise.resolve() ) .then(() => Promise.all(promiseArray)) }
-
某公司的面試題目
假設現在後端有一個服務,支持批量返回書籍信息,它接受一個數組作爲請求數據,數組儲存了需要獲取書目信息的書目 id,這個服務 fetchBooksInfo 大概是這個樣子:
const fetchBooksInfo = bookIdList => { // ... return ([{ id: 123, // ... }, { id: 456 // ... }, // ... ]) }
fetchBooksInfo 已經給出,但是這個接口最多隻支持 100 個 id 的查詢。
現在需要開發者實現 getBooksInfo 方法,該方法:
- 支持調用單個書目信息:
getBooksInfo(123).then(data => {console.log(data.id)}) // 123
- 短時間(100 毫秒)內多次連續調用,只發送一個請求,且獲得各個書目信息:
getBooksInfo(123).then(data => {console.log(data.id)}) // 123 getBooksInfo(456).then(data => {console.log(data.id)}) // 456
注意這裏必須只發送一個請求,也就是說調用了一次 fetchBooksInfo。
- 要考慮服務端出錯的情況,比如批量接口請求 [123, 446] 書目信息,但是服務端只返回了書目 123 的信息。此時應該進行合理的錯誤處理。
- 對 id 重複進行處理
我們來將思路清理一下:
- 100 毫秒內的連續請求,要求進行合併,只觸發一次網絡請求。因此需要一個 bookIdListToFetch 數組,並設置 100 毫秒的定時。在 100 毫秒以內,將所有的書目 id push 到 bookIdListToFetch 中,bookIdListToFetch 長度爲 100 時,進行 clearTimeout,並調用 fetchBooksInfo 發送請求
- 因爲服務端可能出錯,返回的批量接口結果可能缺少某個書目信息。我們需要對相關的調用進行拋錯,比如 100 毫秒內連續調用:
getBooksInfo(123).then(data => {console.log(data.id)}) // 123
getBooksInfo(456).then(data => {console.log(data.id)}) // 456
我們要歸併只調用一次 fetchBooksInfo:
fetchBooksInfo(123, 456)
如果返回有問題,只返回了:
[{
id: 123
//...
}]
沒有返回 id 爲 456 的書信息,需要:
getBooksInfo(456).then(data => {console.log(data.id)}).catch(error => {
console.log(error)
})
捕獲錯誤。
這樣一來,我們要對每一個 getBooksInfo 對應的 promise 實例的 reject 和 resolve 方法進行存儲,存儲在內存 promiseMap 中,以便在合適的時機進行 reject 或 resolve 對應的 promise 實例。
請看代碼(對邊界 case 的處理省略),我加入了關鍵註釋:
// 儲存將要請求的 id 數組
let bookIdListToFetch = []
// 儲存每個 id 請求 promise 實例的 resolve 和 reject
// key 爲 bookId,value 爲 resolve 和 reject 方法,如:
// { 123: [{resolve, reject}]}
// 這裏之所以使用數組存儲 {resolve, reject},是因爲可能存在重複請求同一個 bookId 的情況。其實這裏我們進行了濾重,沒有必要用數組。在需要支持重複的場景下,記得要用數組存儲
let promiseMap = {}
// 用於數組去重
const getUniqueArray = array => Array.from(new Set(array))
// 定時器 id
let timer
const getBooksInfo = bookId => new promise((resolve, reject) => {
promiseMap[bookId] = promiseMap[bookId] || []
promiseMap[bookId].push({
resolve,
reject
})
const clearTask = () => {
// 清空任務和存儲
bookIdListToFetch = []
promiseMap = {}
}
if (bookIdListToFetch.length === 0) {
bookIdListToFetch.push(bookId)
timer = setTimeout(() => {
handleFetch(bookIdListToFetch, promiseMap)
clearTask()
}, 100)
}
else {
bookIdListToFetch.push(bookId)
bookIdListToFetch = getUniqueArray(bookIdListToFetch)
if (bookIdListToFetch.length >= 100) {
clearTimeout(timer)
handleFetch(bookIdListToFetch, promiseMap)
clearTask()
}
}
})
const handleFetch = (list, map) => {
fetchBooksInfo(list).then(resultArray => {
const resultIdArray = resultArray.map(item => item.id)
// 處理存在的 bookId
resultArray.forEach(data => promiseMap[data.id].forEach(item => {
item.resolve(data)
}))
// 處理失敗沒拿到的 bookId
let rejectIdArray = []
bookIdListToFetch.forEach(id => {
// 返回的數組中,不含有某項 bookId,表示請求失敗
if (!resultIdArray.includes(id)) {
rejectIdArray.push(id)
}
})
// 對請求失敗的數組進行 reject
rejectIdArray.forEach(id => promiseMap[id].forEach(item => {
item.reject()
}))
}, error => {
console.log(error)
})
}
做出這道題的關鍵是:
- 準確理解題意,因爲這個題目完全貼近實際場景需求,準確把控出題者的意圖是第一步
- 對 Promise 熟練掌握
- 進行 setTimeout 合併 100 毫秒內的請求
- 存儲每個 bookId 的請求 promise 實例,存儲該 promise 實例的 resolve 和 reject 方法,以便在批量數據返回時進行對應處理
- 錯誤處理