前端異步編程系列之何爲異步編程(1/4)

1.什麼是同步和異步

同步,也就是你在執行代碼時,他會等待代碼返回結果,不管這代碼執行多久,只有代碼返回結果瞭然後再代碼纔會繼續往下執行。而異步指的是:我要執行一段代碼A,我不等待他出結果,我會爲他設置一個處理代碼,當A出結果時,直接去調用那個處理代碼去處理他,而我本身就不會再去管代碼A了,代碼會繼續往下執行,等到A出結果了,直接讓他執行之前設置好的處理代碼就行了。比如,前端的請求Ajax接口就是一個異步操作。

所以同步和異步的不同之處就在於處理問題時流程上的不同。同步比較符合人們的線性思維,代碼一步一步往下走,不會亂。而異步需要就需要把思維轉化爲事件驅動的思路上:我要做一件事,只是告訴計算機開始做這件事就行了,然後我就繼續去做別的事了,而不是傻傻等着計算機做完。只要讓計算機做完了這件事後,告訴我這件事做完了。我才繼續回來去處理結果就行了。

所謂異步執行,不同於同步執行(程序的執行順序與任務的排列順序是一致的、同步的),每一個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,所以程序的執行順序與任務的排列順序是不一致的。

2.爲什麼要學習前端異步編程

JavaScript的執行環境是單線程的,單線程的好處是執行環境簡單,不用去考慮諸如資源同步,死鎖等多線程阻塞式編程等所需要面對的惱人的問題。但問題也很明顯,如果一個任務運行時間很長,那麼其他的任務就會一直等待。而最難受的是,客戶端瀏覽器的UI渲染和js執行是共享一個線程的,如果js代碼運行很長,那麼UI就會假死,頁面就會沒有反應出現類似卡死的情況。這種用戶體驗肯定很差。

高性能JavaScript一書中曾總結過:如果腳本的執行時間超過100毫秒,那麼用戶會感到明顯的卡頓,以爲頁面停止響應。在B/S模型(瀏覽器/服務器模型)中,網絡速度的限制給網頁的實時體驗造成很大影響。

如果網頁獲取一個網絡資源耗費很長時間,那麼如果採用同步的方式加載,那麼JavaScript將需要等待網絡資源完全從服務器獲取後才能繼續執行。這期間UI將卡死,不會響應用戶的交互行爲(因爲瀏覽器UI和js是共用一個線程的)這時候用戶體驗會很差。而採用異步請求,這是js和UI執行都不會處於等待狀態,可以繼續響應用戶的交互行爲,給用戶一個鮮活的頁面。這也是Ajax如此流行的主要原因之一。

3.異步編程有哪些問題呢?
說了這麼多,異步編程帶給我們的吸引力是足夠大的,但不得不說的是,所有事物都具有兩面性。我們只有去面對異步編程所面臨的問題,並積極去解決他,我們才能真正享受異步編程所帶來的優勢。

而對於前端來說,瀏覽器的事件和ajax的回調函數的異步請求是前端最爲廣泛的異步編程的例子了。不過啊,如果僅僅是這兩種的話,距離解決異步編程中的問題以及寫出優美的異步編程代碼來說還是不夠的。那麼我們就來說說異步編程都有哪些問題值得我們困擾:

1.異常處理
一般我們如果想要捕獲異常,通常都是使用try catch來捕獲的,但是對於異步編程來說不一定適用。爲什麼???比如:

function readFile(callback) {
    setTimeout(() => {
        callback("我是數據")
    },100)
}
// 執行(無法正確捕獲異常,程序會報錯,並且直接崩潰退出)
try {
    readFile((data) => {
        throw new Error();//假設出錯了
        console.log(data);
    })
} catch(err) {
    conosle.log("捕獲錯誤")
}


爲啥看似我們把異常給包裹進去,但是程序確還是崩潰?因爲異步操作通常包括兩個步驟:1.發出異步請求,2.處理結果。

ps:關於js的事件循環調度:js有一個事件處理隊列,用來存放需要完成的一個個的任務,js通過事件調度來一個一個的處理隊列中的任務。一次js只能執行隊列中的一個任務,只有噹噹前任務處理完了之後纔開始執行隊列中的下一個任務。

但是通常:

異步請求的發起和處理結果的處理任務通常都不在一個事件循環調度中(也就是指通常發出異步請求和處理結果在兩個任務隊列中,是分開執行的)。從上面代碼來看:

代碼中try他捕獲的是執行readFile這個函數時拋出的錯誤。而readFIle這個函數執行的時候並沒有拋出錯誤,所以這個catch當然不會捕獲到了錯誤了,因爲錯誤的拋出是在100ms後下一次事件任務執行callback時才被拋出的。(ps:setTimeout定時器他的任務不是在到達時間指定時間時立即執行回調函數,而是在到達指定時間時,向js事件處理隊列中添加一個新的處理任務,所以即使setTimeout(() => {},0)也不會在本次事件任務處理時執行,而是下次任務時才執行)

而解決方法只能在回調函數中自己再try catch一次捕獲一下異常,並且處理了。這樣就有點難受了。

但是這裏還可能會出現一個失誤就是:切記不要捕獲用戶傳入的callback回調函數,因爲如果是node的風格的回調函數,在處理時,會把err傳遞給callback時(即只有一個回調函數的情況下),可能會這樣寫:

bad code:    

try {
    //其他操作。
    callback("我是數據")

}catch(err) {

    callback(err)

}

這段代碼的本意是爲了捕獲其他操作時的錯誤,但卻把callback也包括了進去。這時候,如果其他操作沒有問題,而在callback中拋出了錯誤,那麼callback會被執行兩次。可能這不是想要的。正確的應該是:    

try {
    //其他操作。
}catch(err) {

    callback(err)

    return
}

callback("我是數據");

如果是隻有一個回調函數,如同node中那樣err和callback,那麼callback中的錯誤,應該讓也只能讓定義這個callbac的人自己去在callback中捕獲。但是如果是兩個回調函數,一個成功一個失敗,也可以考慮這樣:

try {

    //其他操作。

    callback("我是數據")

}catch(err) {

    errCallback(err)
}

但是問題在於:你不知道到底是哪個地方拋出的錯誤,而且,代碼的流程不夠清晰,並且可能callback和errCallback同時調用了。而且應該也違背了一個執行異步操作時,是隻有執行成功時調用成功回調,執行失敗時才調用失敗回調的準則。

這是異步回調的問題之一。

問題二:代碼嵌套

當然:看了這麼多的無聊東西,那麼來娛樂一下,見識一下傳說中的回調地獄:

當然了,現實中沒這麼誇張,真正寫出和這圖一樣嵌套代碼的,估計墳頭草都和我一樣高了。不過還是能說明問題的。像我之前微信小程序中的一個商城功能api的調用。就需要:

1.wx.login登陸獲取code,

2.使用code請求openid,

3.使用openid獲取用戶綁定的門店,還需要通過openid獲取這個用戶的團購id(此處是爲了舉例)

4.再用通過門店和團購id,請求該門店的團購商品。

如果這異步回調真寫出來也是有點難看的(事實當然不是真的一股腦全部嵌套的)。所以,嵌套過多,代碼也是不好看的。但是,這不是最主要的問題,因爲

上面寫出來的代碼雖然難看,但是,問題是第3步中,獲取門店和團購id他們都是兩個不同的異步操作。但是,我的下一步4中的操作要依賴這兩個異步的結果,那麼一般最好最方便的寫法自然就是把獲取團購id的異步請求放在獲取門店的異步回調中了,這樣,本來兩個不相干的異步操作,卻需要串行起來進行,這無法利用異步操作代碼的並行優勢。這是異步的一個典型問題。

3.異步轉同步

有時候,我們在編寫代碼時,習慣了異步編程時,可以從容的面對異步編程帶來的回調函數以及業務分散等副產品。但有時候確實也會需要同步api來編寫代碼。比如小程序中,對於獲取localhost來說,就有異步和同步的api,有時候節省了很多問題。所以,有時候如何將異步api轉換爲同步api就是我們需要解決的問題()。

4.其他
          前端的異步編程還有一些其他問題,比如多線程問題?提到這裏,肯定知道說的就是js是單線程的,無法充分利用多核cpu。而HTML5提出了web workers。他在JavaScript單線程執行的基礎上,開啓一個子線程,進行程序處理,而不影響主線程的執行,當子線程執行完畢之後再回到主線程上,在這個過程中並不影響主線程的執行過程。可以用來承擔js的一些比較大的計算任務。不過不能分擔UI渲染任務。而且這一塊由於我很少用,我也不太清楚,有興趣的同志可以研究一下。

結語:

這第一篇文章主要是爲了讓大家瞭解什麼是異步編程,以及異步編程的優劣,不過這篇文章主要還是爲了後面異步編程文章做的鋪墊,後續我會詳細介紹該如何解決上述提出的關於異步編程的問題。

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