淺析JavaScript異步

一直以來都知道JavaScript是一門單線程語言,在筆試過程當中不斷的遇到一些輸出結果的問題,考量的是對異步編程掌握狀況。通常被問到異步的時候腦子裏第一反應就是AjaxsetTimseout...這些東西。在平時作項目過程當中,基本大多數操做都是異步的。JavaScript異步都是經過回調形式完成的,開發過程當中一直在處理回調,可能不知不覺中本身就已經處在回調地獄中。html

瀏覽器線程

在開始以前簡單的說一下瀏覽器的線程,對瀏覽器的做業有個基礎的認識。以前說過JavaScript是單線程做業,可是並不表明瀏覽器就是單線程的。前端

JavaScript引擎中負責解析和執行JavaScript代碼的線程只有一個。可是除了這個主進程之外,還有其餘不少輔助線程。那麼諸如onclick回調,setTimeoutAjax這些都是怎麼實現的呢?即瀏覽器搞了幾個其餘線程去輔助JavaScript線程的運行。java

瀏覽器有不少線程,例如:ajax

  1. GUI渲染線程 - GUI渲染線程處於掛起狀態的,也就是凍結狀態
  2. JavaScript引擎線程 - 用於解析JavaScript代碼
  3. 定時器觸發線程 - 瀏覽器定時計數器並非 js引擎計數
  4. 瀏覽器事件線程 - 用於解析BOM渲染等工做
  5. http線程 - 主要負責數據請求
  6. EventLoop輪詢處理線程 - 事件被觸發時該線程會把事件添加到待處理隊列的隊尾
  7. 等等等

從上面來看能夠得出,瀏覽器其實也作了不少事情,遠遠的沒有想象中的那麼簡單,上面這些線程中GUI渲染線程,JavaScript引擎線程,瀏覽器事件線程是瀏覽器的常駐線程。chrome

當瀏覽器開始解析代碼的時候,會根據代碼去分配給不一樣的輔助線程去做業。編程

進程設計模式

進程是指在操做系統中正在運行的一個應用程序瀏覽器

線程服務器

線程是指進程內獨立執行某個任務的一個單元。線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧)。網絡

進程中包含線程,一個進程中能夠有N個進程。咱們能夠在電腦的任務管理器中查看到正在運行的進程,能夠認爲一個進程就是在運行一個程序,好比用瀏覽器打開一個網頁,這就是開啓了一個進程。可是好比打開3個瀏覽器,那麼就開啓了3個進程。

同步&異步

既然要了解同步異步固然要簡單的說一下同步和異步。說到同步和異步最有發言權的真的就屬Ajax了,爲了讓例子更加明顯沒有使用Ajax舉例。(●ˇ∀ˇ●)

同步

同步會逐行執行代碼,會對後續代碼形成阻塞,直至代碼接收到預期的結果以後,纔會繼續向下執行。

console.log(1);
alert("同步");
console.log(2);

//  結果:
//  1
//  同步
//  2

異步

若是在函數返回的時候,調用者還不可以獲得預期結果,而是未來經過必定的手段獲得結果(例如回調函數),這就是異步。

console.log(1);
setTimeout(() => {
   alert("異步"); 
},0);
console.log(2);

//  結果:
//  1
//  2
//  異步
爲何JavaScript要採用異步編程

一開始就說過,JavaScript是一種單線程執行的腳本語言(這多是因爲歷史緣由或爲了簡單而採起的設計)。它的單線程表如今任何一個函數都要從頭至尾執行完畢以後,纔會執行另外一個函數,界面的更新、鼠標事件的處理、計時器(setTimeout、setInterval等)的執行也須要先排隊,後串行執行。假若有一段JavaScript從頭至尾執行時間比較長,那麼在執行期間任何UI更新都會被阻塞,界面事件處理也會中止響應。這種狀況下就須要異步編程模式,目的就是把代碼的運行打散或者讓IO調用(例如AJAX)在後臺運行,讓界面更新和事件處理可以及時地運行。

JavaScript語言的設計者意識到,這時主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。

異步運行機制:

  1. 全部同步任務都在主線程上執行,造成一個執行棧。
  2. 主線程以外,還存在一個任務隊列。只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
  3. 一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。

舉個例子:

<button onclick="updateSync()">同步</button>
<button onclick="updateAsync()">異步</button>
<div id="output"></div>
<script>
function updateSync() {
  for (var i = 0; i < 1000000; i++) {
    document.getElementById('output').innerHTML = i;
  }
}
function updateAsync() {
  var i = 0;
  function updateLater() {
    document.getElementById('output').innerHTML = (i++);
    if (i < 1000000) {
      setTimeout(updateLater, 0);
    }
  }
  updateLater();
}
</script>

點擊同步按鈕會調用updateSync的同步函數,邏輯很是簡單,循環體內每次更新output結點的內容爲i。若是在其餘多線程模型下的語言,你可能會看到界面上以很是快的速度顯示從0999999後中止。可是在JavaScript中,你會感受按鈕按下去的時候卡了一下,而後看到一個最終結果999999,而沒有中間過程,這就是由於在updateSync函數運行過程當中UI更新被阻塞,只有當它結束退出後纔會更新UI。反之,當點擊異步的時候,會明顯的看到Dom在逐步更新的過程。

從上面的例子中能夠明顯的看出,異步編程對於JavaScript來講是多麼多麼的重要。

異步編程有什麼好處

從編程方式來說固然是同步編程的方式更爲簡單,可是同步有其侷限性一是假如是單線程那麼一旦遇到阻塞調用,會形成整個線程阻塞,致使cpu沒法獲得有效利用,而瀏覽器的JavaScript執行和瀏覽器渲染是運行在單線程中,一旦遇到阻塞調用不只意味JavaScript的執行被阻塞更意味整個瀏覽器渲染也被阻塞這就致使界面的卡死,如果多線程則不可避免的要考慮互斥和同步問題,而互斥和同步帶來複雜度也很大,實際上瀏覽器下由於同時只能執行一段JavaScript代碼這意味着不存在互斥問題,可是同步問題仍然不可避免,以往回調風格中異步的流程控制(其實就是同步問題)也比較複雜。瀏覽器端的編程方式也便是GUI編程,其本質就是事件驅動的(鼠標點擊,Http請求結束等)異步編程更爲天然。

忽然有個疑問,既然如此爲何JavaScript沒有使用多線程做業呢?就此就去Google了一下JavaScript多線程,在HTML5推出以後是提供了多線程只是比較侷限。在使用多線程的時候沒法使用window對象。若JavaScript使用多線程,在A線程中正在操做DOM,可是B線程中已經把該DOM已經刪除了(只是簡單的小栗子,可能還有不少問題,至於這些歷史問題無從考究了)。會給編程做業帶來很大的負擔。就我而言我想這也就說明了爲何JavaScript沒有使用多線程的緣由吧。

異步與回調

回調到底屬於異步麼?會想起剛剛開始學習JavaScript的時候經常吧這兩個概念混合在一塊兒。在搞清楚這個問題,首先要明白什麼是回調函數。

百科:回調函數是一個函數,它做爲參數傳遞給另外一個函數,並在父函數完成後執行。回調的特殊之處在於,出如今「父類」以後的函數能夠在回調執行以前執行。另外一件須要知道的重要事情是如何正確地傳遞迴調。這就是我常常忘記正確語法的地方。

經過上面的解釋能夠得出,回調函數本質上其實就是一種設計模式,例如咱們熟悉的JQuery也只不過是遵循了這個設計原則而已。在JavaScript中,回調函數具體的定義爲:函數A做爲參數(函數引用)傳遞到另外一個函數B中,而且這個函數B執行函數A。咱們就說函數A叫作回調函數。若是沒有名稱(函數表達式),就叫作匿名回調函數。

簡單的舉個小例子:

function test (n,fn){
    console.log(n);
    fn && fn(n);
}
console.log(1);
test(2);
test(3,function(n){
    console.log(n+1)
});
console.log(5)

//  結果
//  1
//  2
//  3
//  4
//  5

經過上面的代碼輸出的結果能夠得出回調函數不必定屬於異步,通常同步會阻塞後面的代碼,經過輸出結果也就得出了這個結論。回調函數,通常在同步情境下是最後執行的,而在異步情境下有可能不執行,由於事件沒有被觸發或者條件不知足。

回調函數應用場景

  1. 資源加載:動態加載js文件後執行回調,加載iframe後執行回調,ajax操做回調,圖片加載完成執行回調,AJAX等等。
  2. DOM事件及Node.js事件基於回調機制(Node.js回調可能會出現多層回調嵌套的問題)。
  3. setTimeout的延遲時間爲0,這個hack常常被用到,settimeout調用的函數其實就是一個callback的體現
  4. 鏈式調用:鏈式調用的時候,在賦值器(setter)方法中(或者自己沒有返回值的方法中)很容易實現鏈式調用,而取值器(getter)相對來講很差實現鏈式調用,由於你須要取值器返回你須要的數據而不是this指針,若是要實現鏈式方法,能夠用回調函數來實現。
  5. setTimeout、setInterval的函數調用獲得其返回值。因爲兩個函數都是異步的,即:調用時序和程序的主流程是相對獨立的,因此沒有辦法在主體裏面等待它們的返回值,它們被打開的時候程序也不會停下來等待,不然也就失去了setTimeout及setInterval的意義了,因此用return已經沒有意義,只能使用callback。callback的意義在於將timer執行的結果通知給代理函數進行及時處理。
JavaScript中的那些異步操做

JavaScript既然有不少的輔助線程,不可能全部的工做都是經過主線程去作,既然分配給輔助線程去作事情。

XMLHttpRequest

XMLHttpRequest對象應該不是很陌生的,主要用於瀏覽器的數據請求與數據交互。XMLHttpRequest對象提供兩種請求數據的方式,一種是同步,一種是異步。能夠經過參數進行配置。默認爲異步。

對於XMLHttpRequest這裏就不做太多的贅述了。

var xhr = new XMLHttpRequest();
xhr.open("GET", url, false);    //同步方式請求 
xhr.open("GET", url, true);     //異步
xhr.send();

同步Ajax請求:

當請求開始發送時,瀏覽器事件線程通知主線程,讓Http線程發送數據請求,主線程收到請求以後,通知Http線程發送請求,Http線程收到主線程通知以後就去請求數據,等待服務器響應,過了N年以後,收到請求回來的數據,返回給主線程數據已經請求完成,主線程把結果返回給了瀏覽器事件線程,去完成後續操做。

異步Ajax請求:

當請求開始發送時,瀏覽器事件線程通知,瀏覽器事件線程通知主線程,讓Http線程發送數據請求,主線程收到請求以後,通知Http線程發送請求,Http線程收到主線程通知以後就去請求數據,並通知主線程請求已經發送,主進程通知瀏覽器事件線程已經去請求數據,則
瀏覽器事件線程,只須要等待結果,並不影響其餘工做。

setInterval&setTimeout

setIntervalsetTimeout同屬於異步方法,其異步是經過回調函數方式實現。其二者的區別則setInterval會連續調用回調函數,則setTimeout會延時調用回調函數只會執行一次。

setInterval(() => {
    alert(1)
},2000)
//  每隔2s彈出一次1
setTimeout(() => {
    alert(2)
},2000)
//  進入頁面後2s彈出2,則不會再次彈出

requestAnimationFarme

requestAnimationFrame字面意思就是去請求動畫幀,在沒有API以前都是基於setInterval,與setInterval相比,requestAnimationFrame最大的優點是由系統來決定回調函數的執行時機。具體一點講,若是屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,若是刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟着系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。

舉個小例子:

var progress = 0;
//回調函數
function render() {
    progress += 1; //修改圖像的位置
    if (progress < 100) {
        //在動畫沒有結束前,遞歸渲染
        window.requestAnimationFrame(render);
    }
}
//第一幀渲染
window.requestAnimationFrame(render);

Object.observe - 觀察者

Object.observe是一個提供數據監視的API,在chrome中已經可使用。是ECMAScript 7 的一個提案規範,官方建議的是謹慎使用級別,可是我的認爲這個API很是有用,例如能夠對如今流行的MVVM框架做一些簡化和優化。雖然標準還沒定,可是標準每每是滯後於實現的,只要是有用的東西,確定會有愈來愈多的人去使用,愈來愈多的引擎會支持,最終促使標準的生成。從observe字面意思就能夠知道,這玩意兒就是用來作觀察者模式之類。

var obj = {a: 1};
Object.observe(obj, output);
obj.b = 2;
obj.a = 2;
Object.defineProperties(obj, {a: { enumerable: false}}); //修改屬性設定
delete obj.b;
function output(change) {
    console.log(1)
}

Promise

Promise是對異步編程的一種抽象。它是一個代理對象,表明一個必須進行異步處理的函數返回的值或拋出的異常。也就是說Promise對象表明了一個異步操做,能夠將異步對象和回調函數脫離開來,經過then方法在這個異步操做上面綁定回調函數。

在Promise中最直觀的例子就是Promise.all統一去請求,返回結果。

var p1 = Promise.resolve(3);
var p2 = 42;
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, 'foo');
});
Promise.all([p1, p2, p3]).then(function(values) {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

Generator&Async/Await

ES6Generator卻給異步操做又提供了新的思路,立刻就有人給出瞭如何用Generator來更加優雅的處理異步操做。Generator函數是協程在ES6的實現,最大特色就是能夠交出函數的執行權(即暫停執行)。整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用yield語句註明。Generator函數的執行方法以下。

function * greneratorDome(){
    yield "Hello";
    yield "World";
    return "Ending";
}
let grenDome = greneratorDome();
console.log(grenDome.next());
// {value: "Hello", done: false}
console.log(grenDome.next());
// {value: "World", done: false}
console.log(grenDome.next());
// {value: "Ending", done: true}
console.log(grenDome.next());
// {value: undefined, done: true}

粗略實現Generator

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

Async/AwaitGenerator相似,Async/awaitJavascript編寫異步程序的新方法。以往的異步方法無外乎回調函數和Promise。可是Async/await創建於Promise之上,我的理解是使用了Generator函數作了語法糖。async函數就是隧道盡頭的亮光,不少人認爲它是異步操做的終極解決方案。

function a(){
    return new Promise((resolve,reject) => {
        console.log("a函數")
        resolve("a函數")
    })
}
function b (){
    return new Promise((resolve,reject) => {
        console.log("b函數")
        resolve("b函數")
    })
}
async function dome (){
    let A = await a();
    let B = await b();
    return Promise.resolve([A,B]);
}
dome().then((res) => {
    console.log(res);
});

Node.js異步I/O

當咱們發起IO請求時,調用的是各個不一樣平臺的操做系統內部實現的線程池內的線程。這裏的IO請求可不只僅是讀寫磁盤文件,在*nix中,將計算機抽象了一層,磁盤文件、硬件、套接字等幾乎全部計算機資源都被抽象爲文件,常說的IO請求就是抽象後的文件。完成Node整個異步IO環節的有事件循環、觀察者、請求對象。

事件循環機制

單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。因而就有一個概念,任務隊列。若是排隊是由於計算量大,CPU忙不過來,倒也算了,可是不少時候CPU是閒着的,由於IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),不得不等着結果出來,再往下執行。

事件循環是Node的自身執行模型,正是事件循環使得回調函數得以在Node中大量的使用。在進程啓動時Node會建立一個while(true)死循環,這個和Netty也是同樣的,每次執行循環體,都會完成一次Tick。每一個Tick的過程就是查看是否有事件等待被處理。若是有,就取出事件及相關的回調函數,並執行關聯的回調函數。若是再也不有事件處理就退出進程。

u=3205663441,2452156941&fm=26&gp=0.jpg

線程只會作一件事情,就是從事件隊列裏面取事件、執行事件,再取事件、再事件。當消息隊列爲空時,就會等待直到消息隊列變成非空。並且主線程只有在將當前的消息執行完成後,纔會去取下一個消息。這種機制就叫作事件循環機制,取一個消息並執行的過程叫作一次循環。

while(true) {
    var message = queue.get();
    execute(message);
}

咱們能夠把整個事件循環想象成一個事件隊列,在進入事件隊列時開始對事件進行彈出操做,直至事件爲0爲止。

process.nextTick

process.nextTick()方法能夠在當前"執行棧"的尾部-->下一次Event Loop(主線程讀取"任務隊列")以前-->觸發process指定的回調函數。也就是說,它指定的任務老是發生在全部異步任務以前,當前主線程的末尾。(nextTick雖然也會異步執行,可是不會給其餘io事件執行的任何機會);

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});
setTimeout(function C() {
  console.log(3');
}, 0);
// 1
// 2
// 3
異步過程的構成要素

異步函數實際上很快就調用完成了,可是後面還有工做線程執行異步任務,通知主線程,主線程調用回調函數等不少步驟。咱們把整個過程叫作異步過程,異步函數的調用在整個異步過程當中只是一小部分。

一個異步過程的整個過程:主線程發一塊兒一個異步請求,相應的工做線程接收請求並告知主線程已收到通知(異步函數返回);主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;工做線程完成工做後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。

它能夠叫作異步過程的發起函數,或者叫作異步任務註冊函數。args是這個函數須要的參數,callbackFn(回調函數)也是這個函數的參數,可是它比較特殊因此單獨列出來。因此,從主線程的角度看,一個異步過程包括下面兩個要素:

  1. 發起函數;
  2. 回調函數callbackFn

它們都是主線程上調用的,其中註冊函數用來發起異步過程,回調函數用來處理結果。

舉個具體的栗子:

setTimeout(function,1000);

其中setTimeout就是異步過程的發起函數,function是回調函數。

注:前面說得形式A(args...,callbackFn)只是一種抽象的表示,並不表明回調函數必定要做爲發起函數的參數,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx;
xhr.open('GET', url);
xhr.send();  
總結

JavaScript的異步編程模式不只是一種趨勢,並且是一種必要,所以做爲HTML5開發者是很是有必要掌握的。採用第三方的異步編程庫和異步同步化的方法,會讓代碼結構相對簡潔,便於維護,推薦開發人員掌握一二,提升團隊開發效率。

若是你和我同樣喜歡前端的話,能夠加Qq羣:135170291,期待你們的加入。

相關文章
相關標籤/搜索