(82)Wangdao.com第十六天1017__ JavaScript 非同步操作
阿新 • • 發佈:2018-11-11
非同步操作
- 單執行緒模型
- 指的是,JavaScript 只在一個執行緒上執行
- 也就是說,JavaScript 同時只能執行一個任務,其他任務都必須在後面排隊等待
- 注意,JavaScript 只在一個執行緒上執行,不代表 JavaScript 引擎只有一個執行緒。
- 事實上,JavaScript 引擎有多個執行緒,單個指令碼只能在一個執行緒上執行(稱為主執行緒),其他執行緒都是在後臺配合
-
- JavaScript 之所以採用單執行緒,而不是多執行緒,跟歷史有關係。
- JavaScript 從誕生起就是單執行緒,原因是不想讓瀏覽器變得太複雜,
- 因為多執行緒需要共享資源、且有可能修改彼此的執行結果,對於一種網頁尾本語言來說,這就太複雜了。
- 好處:
- 實現起來比較簡單,執行環境相對單純
- Node 可以用很少的資源,應付大流量訪問的原因
- 壞處:
- 只要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。
- 常見的瀏覽器無響應(假死),往往就是因為某一段 JavaScript 程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行
- JavaScript 語言本身並不慢,慢的是讀寫外部資料,比如等待 Ajax 請求返回結果。這個時候,如果對方伺服器遲遲沒有響應,或者網路不通暢,就會導致指令碼的長時間停滯。
- JavaScript 之所以採用單執行緒,而不是多執行緒,跟歷史有關係。
-
- JavaScript 語言的設計者意識到,這時 CPU 完全可以不管 IO 操作,掛起處於等待中的任務,先執行排在後面的任務。等到 IO 操作返回了結果,再回過頭,把掛起的任務繼續執行下去。這種機制就是 JavaScript 內部採用的 “事件迴圈”機制(Event Loop)
- 為了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,允許 JavaScript 指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作 DOM。所以,這個新標準並沒有改變 JavaScript 單執行緒的本質。
-
- 同步任務和非同步任務
程式裡面所有的任務,可以分成兩類:同步任務(synchronous)和非同步任務(asynchronous)
-
-
- 同步任務
- 是那些沒有被引擎掛起、在主執行緒上排隊執行的任務。
- 只有前一個任務執行完畢,才能執行後一個任務。
- 同步任務
-
-
-
- 非同步任務
- 是那些被引擎放在一邊,不進入主執行緒、而進入任務佇列的任務。
- 只有引擎認為某個非同步任務可以執行了(比如 Ajax 操作從伺服器得到了結果),該任務(採用回撥函式的形式)才會進入主執行緒執行。
- 排在非同步任務後面的程式碼,不用等待非同步任務結束會馬上執行,也就是說,非同步任務不具有”堵塞“效應。
- 舉例來說
-
- Ajax 操作可以當作同步任務處理,也可以當作非同步任務處理,由開發者決定。
- 如果是同步任務,主執行緒就等著 Ajax 操作返回結果,再往下執行;
- 如果是非同步任務,主執行緒在發出 Ajax 請求以後,就直接往下執行,等到 Ajax 操作有了結果,主執行緒再執行對應的回撥函式
- Ajax 操作可以當作同步任務處理,也可以當作非同步任務處理,由開發者決定。
-
- 非同步任務
-
-
- 任務佇列和時間迴圈
JavaScript 執行時,除了一個正在執行的主執行緒,引擎還提供一個任務佇列(task queue),
裡面是各種需要當前程式處理的非同步任務。(實際上,根據非同步任務的型別,存在多個任務佇列。為了方便理解,這裡假設只存在一個佇列。)
- 首先,主執行緒會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務佇列裡面的非同步任務。
- 如果滿足條件,那麼非同步任務就重新進入主執行緒開始執行,這時它就變成同步任務了。
- 等到執行完,下一個非同步任務再進入主執行緒開始執行。一旦任務佇列清空,程式就結束執行。
- 非同步任務的寫法通常是回撥函式。
- 一旦非同步任務重新進入主執行緒,就會執行對應的回撥函式。
- 如果一個非同步任務沒有回撥函式,就不會進入任務佇列,也就是說,不會重新進入主執行緒,因為沒有用回撥函式指定下一步的操作。
- JavaScript 引擎怎麼知道非同步任務有沒有結果,能不能進入主執行緒呢?
- 答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,引擎就會去檢查那些掛起來的非同步任務,是不是可以進入主執行緒了。
- 這種迴圈檢查的機制,就叫做事件迴圈(Event Loop)。
- 維基百科的定義是:“事件迴圈是一個程式結構,用於等待和傳送訊息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
- 非同步操作的模式
- 回撥函式
- 是非同步操作最基本的方法
- 下面是兩個函式
f1
和f2
,程式設計的意圖是f2
必須等到f1
執行完成,才能執行function f1() { // ... } function f2() { // ... } f1(); f2();
上面程式碼的問題在於,如果
f1
是非同步操作,f2
會立即執行,不會等到f1
結束再執行- 這時,可以考慮改寫
f1
,把f2
寫成f1
的回撥函式function f1(callback) { // ... callback(); } function f2() { // ... } f1(f2);
- 這時,可以考慮改寫
- 下面是兩個函式
- 是非同步操作最基本的方法
-
-
-
- 回撥函式的優點:
- 簡單、容易理解和實現
- 回撥函式的缺點:
- 不利於程式碼的閱讀和維護,
- 各個部分之間高度耦合(coupling),使得程式結構混亂、流程難以追蹤(尤其是多個回撥函式巢狀的情況),
- 而且每個任務只能指定一個回撥函式
- 回撥函式的優點:
-
-
-
-
- 事件監聽
- 採用事件驅動模式。
- 非同步任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。
- 以 f1 和 f2 為例。首先,為 f1 繫結一個事件(這裡採用的 jQuery 的寫法)
-
f1.on('done', f2); // 當 f1 發生 done 事件,就執行 f2
對
f1
進行改寫:-
function f1() { setTimeout(function () { // ... f1.trigger('done'); // 表示,執行完成後,立即觸發
done
事件,從而開始執行f2
}, 1000); }
-
-
- 優點:
- 比較容易理解,可以繫結多個事件,
- 每個事件可以指定多個回撥函式,
- 而且可以”去耦合“(decoupling),有利於實現模組化
- 缺點:
- 整個程式都要變成事件驅動型,執行流程會變得很不清晰。
- 閱讀程式碼的時候,很難看出主流程。
-
- ”釋出/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)
- 事件完全可以理解成”訊號“,如果存在一個”訊號中心“,
- 某個任務執行完成,就向訊號中心 ”釋出“(publish)一個訊號,
- 其他任務可以向訊號中心”訂閱“(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。
-
-
-
- 可以用多種方式實現這個模式
- 採用的是 Ben Alman 的 Tiny Pub/Sub,這是 jQuery 的一個外掛
- 首先,f2 向訊號中心 jQuery 訂閱 done 訊號
-
jQuery.subscribe('done', f2);
-
- 然後,f1 進行如下改寫
-
function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
f1 執行完成後,向訊號中心 jQuery 釋出 done 訊號,從而引發 f2 的執行
-
- f2 完成執行後,可以取消訂閱(unsubscribe)
-
jQuery.unsubscribe('done', f2);
-
- 性質與“事件監聽”類似,但是明顯優於後者。
- 因為可以通過檢視“訊息中心”,瞭解存在多少訊號、每個訊號有多少訂閱者,從而監控程式的執行
- 可以用多種方式實現這個模式
-
-
- 非同步操作的流程控制
- 如果有多個非同步操作,就存在一個流程控制的問題:如何確定非同步操作執行的順序,以及如何保證遵守這種順序。
- 序列執行:
- 我們可以編寫一個流程控制函式,讓它來控制非同步任務,一個任務完成以後,再執行另一個。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('引數為 ' + arg +' , 1秒後返回結果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } function series(item) { if(item) { async( item, function(result) { results.push(result); return series(items.shift()); }); } else { return final(results[results.length - 1]); } } series(items.shift());
- 函式 series() 就是序列函式,它會依次執行非同步任務,所有任務都完成後,才會執行final函式。
- items[] 陣列儲存每一個非同步任務的引數,results[] 陣列儲存每一個非同步任務的執行結果。
- 注意,上面的寫法需要六秒,才能完成整個指令碼。
- 我們可以編寫一個流程控制函式,讓它來控制非同步任務,一個任務完成以後,再執行另一個。
-
- 並行執行
- 流程控制函式也可以並行執行,即所有非同步任務同時執行,等到全部完成以後,才執行 final 函式
-
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('引數為 ' + arg +' , 1秒後返回結果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } items.forEach(function(item) { async(item, function(result){ results.push(result); if(results.length === items.length) { final(results[results.length - 1]); } }) });
-
- 流程控制函式也可以並行執行,即所有非同步任務同時執行,等到全部完成以後,才執行 final 函式
- 並行執行
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 2