JS異步開發總結

1 前言

衆所周知,JS語言是單線程的。在實際開發過程當中都會面臨一個問題,就是同步操做會阻塞整個頁面乃至整個瀏覽器的運行,只有在同步操做完成以後才能繼續進行其餘處理,這種同步等待的用戶體驗極差。因此JS中引入了異步編程,主要特色就是不阻塞主線程的繼續執行,用戶直觀感覺就是頁面不會卡住。css

2 概念說明

2-1 瀏覽器的進程和線程

首先能夠肯定一點是瀏覽器是多進程的,好比打開多個窗口可能就對應着多個進程,這樣能夠確保的是頁面之間相互沒有影響,一個頁面卡死也並不會影響其餘的頁面。一樣對於瀏覽器進程來講,是多線程的,好比咱們前端開發人員最須要了解的瀏覽器內核也就是瀏覽器的渲染進程,主要負責頁面渲染,腳本執行,事件處理等任務。爲了更好的引入JS單線程的概念,咱們將瀏覽器內核中經常使用的幾個線程簡單介紹一下:html

  1. GUI渲染線程 負責渲染瀏覽器頁面,解析html+css,構建DOM樹,進行頁面的佈局和繪製操做,同事頁面須要重繪或者印發迴流時,都是該線程負責執行。前端

  2. JS引擎線程 JS引擎,負責解析和運行JS腳本,一個頁面中永遠都只有一個JS線程來負責運行JS程序,這就是咱們常說的JS單線程。node

    注意:JS引擎線程和GUI渲染線程永遠都是互斥的,因此當咱們的JS腳本運行時間過長時,或者有同步請求一直沒返回時,頁面的渲染操做就會阻塞,就是咱們常說的卡死了web

  3. 事件觸發線程 接受瀏覽器裏面的操做事件響應。如在監聽到鼠標、鍵盤等事件的時候, 若是有事件句柄函數,就將對應的任務壓入隊列。ajax

  4. 定時觸發器線程 瀏覽器模型定時計數器並非由JavaScript引擎計數的, 由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確, 它必須依賴外部來計時並觸發定時。編程

  5. 異步http請求線程 在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。api

2-2 JS單線程

由於只有JS引擎線程負責處理JS腳本程序,因此說JS是單線程的。能夠理解的是js當初設計成單線程語言的緣由是由於js須要操做dom,若是多線程執行的話會引入不少複雜的狀況,好比一個線程刪除dom,一個線程添加dom,瀏覽器就無法處理了。雖然如今js支持webworker多線線程了,可是新增的線程徹底在主線程的控制下,爲的是處理大量耗時計算用的,不能處理DOM,因此js本質上來講仍是單線程的。promise

2-3 同步異步

同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。瀏覽器

2-4 任務隊列

任務隊列就是用來存放一個個帶執行的異步操做的隊列,在ES6中又將任務隊列分爲宏觀任務隊列和微觀任務隊列。

宏任務隊列(macrotask queue)等同於咱們常說的任務隊列,macrotask是由宿主環境分發的異步任務,事件輪詢的時候老是一個一個任務隊列去查看執行的,"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。

微任務隊列(microtask queue)是由js引擎分發的任務,老是添加到當前任務隊列末尾執行。另外在處理microtask期間,若是有新添加的microtasks,也會被添加到隊列的末尾並執行

2-5 事件循環機制

異步時間添加到任務隊列中後,如何控制他們的具體執行時間呢?JS引擎一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

ES5的JS事件循環參考圖:

ES6的JS事件循環參考圖:

理解了JS程序執行的基本原理,下面就能夠步入正題,討論一下咱們在實際開發中,如何編寫異步程序才能讓本身的代碼易讀易懂bug少。

3 callback

在JavaScript中,回調函數具體的定義爲:函數A做爲參數(函數引用)傳遞到另外一個函數B中,而且這個函數B執行函數A。咱們就說函數A叫作回調函數。若是沒有名稱(函數表達式),就叫作匿名回調函數。

所以callback 不必定用於異步,通常同步(阻塞)的場景下也常常用到回調,好比要求執行某些操做後執行回調函數。

回調函數被普遍應用到JS的異步開發當中,下面分別列舉幾條開發中經常使用回調函數的狀況,如:

  1. 時間延遲操做
setTimeout(function(){
    //該方法爲回調方法
    //code
}, 1000)

setInterval(()=>{
    //該方法爲匿名回調方法
    //code
}, 1000)
複製代碼
  1. nodeapi
//node讀取文件
fs.readFile(xxx, 'utf-8', function(err, data) { 
    //該方法爲讀取文件成功後出發的回調方法
    //code
});
複製代碼
  1. ajax操做
$.ajax({
    type: "post",
    url: "xxx",
    success: function(data){
        //post請求成功回調方法
        //code
    },
    error: fucntion(e){
        //post請求錯誤回調方法
        //code
    }
})
複製代碼

用回調函數的方法來進行異步開發好處就是簡單明瞭,容易理解

回調函數的缺點, 用一個小的實例來講明一下:

method1(function(err, data) {
    //code1
    method2(function(err, data) { 
        //code2
        method3(function(err, data) { 
            //code3
            method4(D, 'utf-8', function(err, data) { 
                //code4 
            });
       });
   });
 });
複製代碼

若是說異步方法以前有明確的前後順序來執行,稍微複雜的操做很容易寫出上面示例的代碼結構,若是加上業務代碼,程序就顯得異常複雜,代碼難以理解和調試,這種就是咱們常說的回調地獄。

若是想要實現更加複雜的功能,回調函數的侷限性也會凸顯出來,好比同時執行兩個異步請求,當兩個操做都結束時在執行某個操做,或者同時進行兩個請求,取優先完成的結果來執行操做,這種都須要在各自的回調方法中監控狀態來完成。

隨着ES6/ES7新標準的普及,咱們應該尋求新的異步解決方案來替代這種傳統的回調方式。

4 Promise

ES6新增Promise對象的支持,Promise提供統一的接口來獲取異步操做的狀態信息,添加不能的處理方法。

Promise對象只有三種狀態:

  1. pendding: 初始狀態,既不是成功,也不是失敗狀態。
  2. fulfilled: 意味着操做成功完成。
  3. rejected: 意味着操做失敗。

Promise的狀態只能由內部改變,而且只能夠改變一次。

下面看看用Promise來實現多級回調能不能解決回調地獄的問題

function read(filename){
    return new Promise((resolve, reject) => {
        //異步操做code, Example:
        fs.readFile(filename, 'utf8', (err, data) => { 
            if(err) reject(err);
            resolve(data);
        });
    })
 }
 
 read(filename1).then(data=>{
    return read(filename2)
 }).then(data => {
    return read(filename3)
 }).then(data => {
    return read(filename4)
 }).catch(error=>{
    console.log(error);
 })
複製代碼

經過實踐代碼 咱們發現用Promise能夠像寫同步代碼同樣實現異步功能,避免了層層嵌套的問題。

如何用Promise來實現同時發起多個異步操做的需求

  • 多個請求都完成後在執行操做
function loadData(url){
    return new Promise((resolve, reject)=>{
        $.ajax({
            type: "post",
            url: url,
            success: function(data){
                //post請求成功回調方法
                resolve(data)
            },
            error: fucntion(e){
                //post請求錯誤回調方法
                reject(e)
            }
        })
    })
}

Promise.all([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {
    console.log(data)
}).catch(error => {
    console.log(error);
})
複製代碼
  • 多個請求有一個完成後(成功或拒絕)就執行操做
function loadData(url){
    return new Promise((resolve, reject)=>{
        $.ajax({
            type: "post",
            url: url,
            success: function(data){
                //post請求成功回調方法
                resolve(data)
            },
            error: fucntion(e){
                //post請求錯誤回調方法
                reject(e)
            }
        })
    })
}

Promise.race([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {
    console.log(data)
}).catch(error => {
    console.log(error);
})
複製代碼

用Promise來寫異步能夠避免回調地獄,也能夠輕鬆的來實現callback須要引入控制代碼才能實現的多個異步請求動做的需求。

固然Promise也有本身的缺點:

  1. promise一旦新建,就會當即執行,沒法取消
  2. 若是不設置回掉函數,promise內部拋出的錯誤就不會反應到外部
  3. 處於pending狀態時,是不能知道目前進展到哪一個階段的 ( 剛開始?,即將結束?)

帶着這些缺點,繼續往下學習別的異步編程方案。

**關於Promise的詳細文章能夠閱讀這篇你真的會用 Promise 嗎

5 Generator

ES6新增Generator異步解決方案,語法行爲與傳統方法徹底不同。

Generator函數是一個狀態機,封裝了多個內部狀態,也是一個遍歷器對象生成函數,生成的遍歷器對象能夠一次遍歷內部的每個狀態。

Generator用function*來聲明,除了正常的return返回數據以外,還能夠用yeild來返回屢次。

調用一個Generator對象生成一個generator對象,可是還並無去執行他,執行generator對象有兩種方法:

  • next()方法,next方法回去執行generator方法,遇到yeild會返回一個{value:xx, done: true/fasle}的對象,done爲true說明generator執行完畢
  • 第二個方法是用for....of循環迭代generator對象

Generator的用處不少,本文只討論利用它暫停函數執行,返回任意表達式的值的這個特性來使異步代碼同步化表達。從死路上來說咱們想達到這樣的效果:

function loadData(url, data){
    //異步請求獲取數據
    return new Promise((resolve, reject)=>{
        $.ajax({
            type: "post",
            url: url,
            success: function(data){
                //post請求成功回調方法
                resolve(data)
            },
            error: fucntion(e){
                //post請求錯誤回調方法
                reject(e)
            }
        })
    })
}
function*  gen() {
    yeild loadData(url1, data1);
    yeild loadData(url2, data2);
    yeild loadData(url3, data3);
}

for(let data of gen()){
    //分別輸出每次加載數據的返回值
    console.log(data)
}
複製代碼

但僅僅是這樣來實現是不行的,由於異步函數沒有返回值,必須經過從新包裝的方式來傳遞參數值。co.js就是一個這種generator的執行庫。使用它是咱們只須要將咱們的 gen 傳遞給它像這樣 co(gen) 是的就這樣。

function*  gen() {
    let data1 = yeild loadData(url1, data1);
    console.log(data1);
    let data2 = yeild loadData(url2, data2);
    console.log(data2);
    let data3 = yeild loadData(url3, data3);
    console.log(data3);
}
co(gen())
.then(data => {
    //gen執行完成
}).catch(err => {
    //code
})
複製代碼

由於ES7中新增了對async/await的支持,因此異步開發有了更好的選擇,基本上能夠放棄用原生generator來寫異步開發,因此咱們只是有個簡單的概念,下面咱們着重介紹一下異步編程的最終方案 async/await。

6 async/await

asycn/await方案能夠說是目前解決JS異步編程的最終方案了,async/await是generator/co的語法糖,同時也須要結合Promise來使用。該方案的主要特色以下:

  • 普通函數,即全部的原子型異步接口都返回Promise,Promise對象中能夠進行任意異步操做,必需要有resolve();
  • async函數,函數聲明前必需要有async關鍵字,函數中執行定義的普通函數,而且每一個執行前都加上await關鍵字,標識該操做須要等待結果。
  • 執行async函數。asynch函數的返回值是Promise對象,能夠用Promise對象的then方法來指定下一步操做。

還用用代碼來講明問題,用async/await方案來實現最初的需求

//普通函數
function loadData(url){
    //異步請求獲取數據
    return new Promise((resolve, reject)=>{
        $.ajax({
            type: "post",
            url: url,
            success: function(data){
                //post請求成功回調方法
                resolve(data)
            },
            error: fucntion(e){
                //post請求錯誤回調方法
                reject(e)
            }
        })
    })
}

//async函數
async function asyncFun(){
    //普通函數的調用
    let data1 = await loadData(url1);
    let data2 = await loadData(url2);
    let data3 = await loadData(url3)
}

asyncFun()
.then(data => {
    //async函數執行完成後操做
})
.catch(err => {
    //異常抓取
});
複製代碼

loadData()函數雖然返回的是Promise,可是await返回的是普通函數resole(data)時傳遞的data值。

經過和generator方式來的實現對比來看,更加理解了async/await是generator/co方法的語法糖,從函數結構上來講徹底同樣。可是省略了一些外庫的引入,一些通用方法的封裝,使異步開發的邏輯更加清晰,更加接近同步開發。

處理完有前後順序的請求處理,下面來個多個請求同時發起的例子

//普通函數
function loadData(url){
    //異步請求獲取數據
    return new Promise((resolve, reject)=>{
        $.ajax({
            type: "post",
            url: url,
            success: function(data){
                //post請求成功回調方法
                resolve(data)
            },
            error: fucntion(e){
                //post請求錯誤回調方法
                reject(e)
            }
        })
    })
}

//async函數
async function asyncFun(){
    await Promise.all([loadData('url1'), loadData('url2')]).then(data => {
    console.log(data); //['data1', 'data2']
    })
}

asyncFun();

//配合Promise的race方法一樣能夠實現任意請求完成或異常後執行操做的需求

//async函數
async function asyncFun(){
    await Promise.race([loadData('url1'), loadData('url2')]).then(data => {
    console.log(data);
    })
}
複製代碼

最佳實踐

經過上面四種不一樣的異步實現方式的對比能夠發現,async/await模式最接近於同步開發,即沒有連續回調,也沒有連續調用then函數的狀況,也沒有引入第三方庫函數,因此就目前來講async/await+promise的方案爲最佳實踐方案。

社區以及公衆號發佈的文章,100%保證是咱們的原創文章,若是有錯誤,歡迎你們指正。

文章首發在WebJ2EE公衆號上,歡迎你們關注一波,讓咱們你們一塊兒學前端~~~

再來一波號外,咱們成立WebJ2EE公衆號前端吹水羣,你們不論是看文章仍是在工做中前端方面有任何問題,咱們均可以在羣內互相探討,但願可以用咱們的經驗幫更多的小夥伴解決工做和學習上的困惑,歡迎加入。

相關文章
相關標籤/搜索