前端學習筆記——異步上

這個前端學習筆記是學習gitchat上的一個課程,這個課程的質量非常好,價格也不貴,非常時候前端入門的小夥伴們進階。
在這裏插入圖片描述
筆記不會涉及很多,主要是提取一些知識點,詳細的大家最好去過一遍教程,相信你一定會有很大的收穫

異步

JS是單線程的,瀏覽器是如何進行異步處理的?宏任務和微任務的區別是什麼?

從 callback 到 promise,從 generator 到 async/await,到底應該如何更優雅地實現異步操作?

我們來使用異步完成一個需求

移動頁面上元素 target(document.querySelectorAll(’#man’)[0])

先從原點出發,向左移動 20px,之後再向上移動 50px,最後再次向左移動 30px,請把運動動畫實現出來。

  1. 使用回調函數來實現

    它是一個上下左右移動的需求,所以分析下需要的參數:

    • 方向
    • 移動的距離
    • 完成之後的回調
    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)
        })
    })
    
  2. 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))
    
  3. 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之後,手動執行即可

  4. 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');
}
  1. 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()
    
  2. 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()
    
  3. 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()
}

複雜的真實案例

  1. 請求圖片的預加載

    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))
    }
    
  2. 某公司的面試題目

    假設現在後端有一個服務,支持批量返回書籍信息,它接受一個數組作爲請求數據,數組儲存了需要獲取書目信息的書目 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 方法,以便在批量數據返回時進行對應處理
  • 錯誤處理

參考鏈接

ES6 ASYNC/AWAIT完爆promise

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章