1. 程式人生 > >前端非同步程式設計系列之何為非同步程式設計(1/4)

前端非同步程式設計系列之何為非同步程式設計(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渲染任務。而且這一塊由於我很少用,我也不太清楚,有興趣的同志可以研究一下。

結語:

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