js中的同步及非同步
一、單執行緒
(1)單執行緒的概念
如果大家熟悉java,應該都知道,java是一門多執行緒語言,我們常常可以利用java的多執行緒處理各種各樣的事,比如說檔案上傳,下載等,而JavaScript是否也可以支援多執行緒呢?
答案是否定的,JavaScript是一門單執行緒的語言,因此,JavaScript在同一個時間只能做一件事,單執行緒意味著,如果在同個時間有多個任務的話,這些任務就需要進行排隊,前一個任務執行完,才會執行下一個任務,比如說下面這段程式碼
// 同步程式碼 function fun1() { console.log(1); } function fun2() { console.log(2); } fun1(); fun2(); // 輸出 1 2
很容易可以看出,輸出會依次輸入1,2,因為程式碼是從上到下依次執行,執行完fun1(),才繼續執行fun2(),但是如果fun1()中的程式碼執行的是讀取檔案或者ajax操作,檔案的讀取和資料的獲取都需要一定時間,難道我們需要完全等到fun1()執行完才能繼續執行fun2()麼?為了解決這個問題,後面我們會介紹同步和非同步的概念
(2)為什麼是單執行緒
其實,JavaScript的單執行緒,與它的用途是有很大關係,我們都知道,JavaScript作為瀏覽器的指令碼語言,主要用來實現與使用者的互動,利用JavaScript,我們可以實現對DOM的各種各樣的操作,如果JavaScript是多執行緒的話,一個執行緒在一個DOM節點中增加內容,另一個執行緒要刪除這個DOM節點,那麼這個DOM節點究竟是要增加內容還是刪除呢?這會帶來很複雜的同步問題,因此,JavaScript是單執行緒的
二、同步任務和非同步任務
(1)為什麼會有同步和非同步
因為JavaScript的單執行緒,因此同個時間只能處理同個任務,所有任務都需要排隊,前一個任務執行完,才能繼續執行下一個任務,但是,如果前一個任務的執行時間很長,比如檔案的讀取操作或ajax操作,後一個任務就不得不等著,拿ajax來說,當用戶向後臺獲取大量的資料時,不得不等到所有資料都獲取完畢才能進行下一步操作,使用者只能在那裡乾等著,嚴重影響使用者體驗 因此,JavaScript在設計的時候,就已經考慮到這個問題,主執行緒可以完全不用等待檔案的讀取完畢或ajax的載入成功,可以先掛起處於等待中的任務,先執行排在後面的任務,等到檔案的讀取或ajax有了結果後,再回過頭執行掛起的任務,因此,任務就可以分為同步任務和非同步任務
(2)同步任務
同步任務是指在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能繼續執行下一個任務,當我們開啟網站時,網站的渲染過程,比如元素的渲染,其實就是一個同步任務
(3)非同步任務
非同步任務是指不進入主執行緒,而進入任務佇列的任務,只有任務佇列通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒,當我們開啟網站時,像圖片的載入,音樂的載入,其實就是一個非同步任務
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
function fun3() {
console.log(3);
}
fun1();
setTimeout(function(){
fun2();
},0);
fun3();
// 輸出
1
3
2
有了非同步,就算fun2()裡面是檔案的讀取或ajax這種需要耗時的任務,也不怕fun3()要等到fun2()執行完才能執行啦
(4)非同步機制
那麼,JavaScript中的非同步是怎麼實現的呢?那要需要說下回調和事件迴圈這兩個概念啦
首先要先說下任務佇列,我們在前面也介紹了,非同步任務是不會進入主執行緒,而是會先進入任務佇列,任務佇列其實是一個先進先出的資料結構,也是一個事件佇列,比如說檔案讀取操作,因為這是一個非同步任務,因此該任務會被新增到任務佇列中,等到IO完成後,就會在任務佇列中新增一個事件,表示非同步任務完成啦,可以進入執行棧啦~但是這時候呀,主執行緒不一定有空,當主執行緒處理完其它任務有空時,就會讀取任務佇列,讀取裡面有哪些事件,排在前面的事件會被優先進行處理,如果該任務指定了回撥函式,那麼主執行緒在處理該事件時,就會執行回撥函式中的程式碼,也就是執行非同步任務啦
單執行緒從從任務佇列中讀取任務是不斷迴圈的,每次棧被清空後,都會在任務佇列中讀取新的任務,如果沒有任務,就會等到,直到有新的任務,這就叫做任務迴圈,因為每個任務都是由一個事件觸發的,因此也叫作事件迴圈
總的來說,JavaScript的非同步機制包括以下幾個步驟:
(1)所有同步任務都在主執行緒上執行,行成一個執行棧
(2)主執行緒之外,還存在一個任務佇列,只要非同步任務有了結果,就會在任務佇列中放置一個事件
(3)一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務佇列,看看裡面還有哪些事件,那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行
(4)主執行緒不斷的重複上面的第三步
三、非同步程式設計
那麼,怎麼才能實現非同步程式設計,寫出效能更好的程式碼呢,下面有幾種方式
(1)回撥函式
回撥函式是實現非同步程式設計最簡單的方法啦,回撥函式我們在使用ajax時應該用的很多啦,其實在使用ajax時,我們就用到了非同步
var req = new XMLHttpRequest();
req.open("GET",url);
req.send(null);
req.onreadystatechange=function(){}
req.send()方法是 AJAX 向伺服器發生資料,它是一個非同步任務,而 req.onreadystatechange()屬於事件回撥,藉由瀏覽器的HTTP請求執行緒發起對伺服器的請求,在請求得到響應之後觸發請求完成事件,將回調函式推入事件佇列等待執行
其實像setTimeout,還有我們平時為元素繫結監聽事件,和上面說的道理也是一樣的
回撥函式的優點是簡單、容易理解和部署,缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回撥函式
(2)Promise
一直以來,JavaScript處理非同步都是以callback的方式,在前端開發領域callback機制幾乎深入人心,近幾年隨著JavaScript開發模式的逐漸成熟,CommonJS規範順勢而生,其中就包括提出了Promise規範,Promise完全改變了js非同步程式設計的寫法,讓非同步程式設計變得十分的易於理解,同時Promise也已經納入了ES6,而且高版本的chrome、firefox瀏覽器都已經原生實現了Promise,只不過和現如今流行的類Promise類庫相比少些API
Promise包括以下幾個規範
-
一個promise可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
-
一個promise的狀態只可能從“等待”轉到“完成”態或者“拒絕”態,不能逆向轉換,同時“完成”態和“拒絕”態不能相互轉換
-
promise必須實現then方法(可以說,then就是promise的核心),而且then必須返回一個promise,同一個promise的then可以呼叫多次,並且回撥的執行順序跟它們被定義時的順序一致
-
then方法接受兩個引數,第一個引數是成功時的回撥,在promise由“等待”態轉換到“完成”態時呼叫,另一個是失敗時的回撥,在promise由“等待”態轉換到“拒絕”態時呼叫,同時,then可以接受另一個promise傳入,也接受一個“類then”的物件或方法,即thenable物件
在使用Promise時,我們需要檢測一些瀏覽器是否支援Promise
if(typeof(Promise)==="function") {
console.log("支援");
}
else {
console.log("不支援");
}
我們可以使用new Promise進行Promise的建立
function wait(time) {
return new Promise(function(resolve,reject) {
setTimeout(resolve,time);
});
}
這個時候我們就可以使用Promise處理非同步任務啦
wait(1000).then(function(){
console.log(1);
})
上面這個例子表示1秒後輸出1,同樣的道理,我們可以使用Promise進行更加複雜的操作,關於更多的操作,就不繼續說啦,關於非同步的實現,其實還有其它的一些方法,但是因為上面說的這兩種方法用的比較多,所以就只說上面這兩種了