淺談幾個前端異步解決方案

      Javascript語言的執行環境是單線程。即一次只能完成一個任務。如有多個任務則需排隊逐個執行——前一個任務完成,再執行後一個任務。javascript

      這種執行模式實現簡單,執行環境相對單純。但隨着前端業務日漸複雜,事務和請求等日漸增多,這種單線程執行方式在複雜的業務下勢必效率低下,只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。css

       爲避免和解決這種問題,JS語言將任務執行模式分爲異步和同步。同步模式」就是上一段的模式,後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的;」異步模式」則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。前端

    「異步模式」很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,」異步模式」甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。java

      1.回調函數 

      異步編程最基本方法。git

      首先須要聲明,回調函數只是一種實現,並非異步模式特有的實現。回調函數一樣能夠運用到同步(阻塞)的場景下以及其餘一些場景。github

      回調函數的英文定義:A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。ajax

      字面上的理解,回調函數就是一個參數,將這個函數做爲參數傳到另外一個函數裏面,當那個函數執行完以後,再執行傳進去的這個函數。這個過程就叫作回調。編程

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

      用一個通俗的生活例子比喻一下就是:約會結束後你送你女友回家,離別時,你確定會說:「到家了給我發條信息,我很擔憂你。」 而後你女友回家之後還真給你發了條信息。其實這就是一個回調的過程。你留了個參數函數(要求女友給你發條信息)給你女友,而後你女友回家,回家的動做是主函數。她必須先回到家之後,主函數執行完了,再執行傳進去的函數,而後你就收到一條信息了。promise

     假定有兩個函數f1和f2,後者等待前者的執行結果。

     

f1();
f2(); 
複製代碼

     若f1是一個很耗時的任務,能夠考慮改寫f1,把f2寫成f1的回調函數。

function f1(callback){setTimeout(function () {// f1的任務代碼callback();}, 1000);}複製代碼

     執行代碼就變成下面這樣:

f1(f2);
複製代碼

     採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。

    另外一個例子:

//定義主函數,回調函數做爲參數
function A(callback) {
    callback();  
    console.log('我是主函數');      
}

//定義回調函數
function B(){
    setTimeout("console.log('我是回調函數')", 3000);//模仿耗時操做  
}

//調用主函數,將函數B傳進去
A(B);

//輸出結果
我是主函數
我是回調函數

複製代碼

      上面的代碼中,咱們先定義了主函數和回調函數,而後再去調用主函數,將回調函數傳進去。

  定義主函數的時候,咱們讓代碼先去執行callback()回調函數,但輸出結果倒是後輸出回調函數的內容。這就說明了主函數不用等待回調函數執行完,能夠接着執行本身的代碼。因此通常回調函數都用在耗時操做上面。好比ajax請求,好比處理文件等。

    再來一個更俗的例子:

<strong>問:你有事去隔壁寢室找同窗,發現人不在,你怎麼辦呢?</strong><strong>方法1</strong>,每隔幾分鐘再去趟隔壁寢室,看人在不<strong>方法2</strong>,拜託與他同寢室的人,看到他回來時叫一下你 前者是輪詢,後者是回調。 那你說,我直接在隔壁寢室等到同窗回來能夠嗎? 能夠啊,只不過這樣本來你能夠省下時間作其餘事,如今必須浪費在等待上了。把原來的非阻塞的異步調用變成了阻塞的同步調用。 JavaScript的回調是在異步調用場景下使用的,使用回調性能好於輪詢。複製代碼

   對於回調函數,通常在同步情境下是最後執行的,而在異步情境下有可能不執行,由於事件沒有被觸發或者條件不知足,因此請忽略上上個例子中的小問題,並非必定回調函數就要執行。

   同時補充回調函數應用場合和優缺點:

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

       回調函數這種方式的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠」去耦合「,有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。

    2.Promise對象

      隨着ES6標準的發佈,處理異步數據流的解決方案又有了新的變化。promise就是這其中的一個。咱們都知道,在傳統的ajax請求中,當異步請求之間的數據存在依賴關係的時候,就可能產生很難看的多層回調,這樣會使代碼邏輯很容易形成混亂不便於閱讀和後期維護,俗稱」回調地獄」(callback hell)。另外一方面,每每錯誤處理的代碼和正常的業務代碼耦合在一塊兒,形成代碼會極其難看。爲了讓編程更美好,咱們就須要引入promise來下降異步編程的複雜性。

      因此某種程度上說,promise是對上面說到的回調函數處理異步編程的一個進階方案。首先Promise是CommandJS提出的一種規範,其目的是爲異步編程提供統一接口。

      簡單說,Promise的思想是,每個異步任務返回一個Promise對象,該對象有一個then方法,容許指定回調函數。形如這種形式:

f1().then(f2);
複製代碼

     對於函數f1,使用Jquery實現如下改寫:

function f1(){var dfd = $.Deferred();setTimeout(function () {// f1的任務代碼dfd.resolve();}, 500);return dfd.promise;}複製代碼

      這樣寫的優勢在於,回調函數變成了鏈式寫法,程序的流程能夠看得很清楚,並且有一整套的配套方法,能夠實現許多強大的功能。這也就是Promise處理異步編程的其中的一個方便之處。

      再舉一個制定多個回調函數的例子,其形式爲:

f1().then(f2).then(f3);
複製代碼

      當指定發生錯誤時的回調函數,其形式爲:

f1().then(f2).fail(f3);
複製代碼

      在此補充一點,promise中,若是一個任務已經完成,再添加回調函數,該回調函數會當即執行。因此,你不用擔憂是否錯過了某個事件或信號。這種方法的缺點就是編寫和理解,都相對比較難。

      展開談論一下Promise:Promise實際上就是一個特殊的Javascript對象,反映了」異步操做的最終值」。」Promise」直譯過來有預期的意思,所以,它也表明了某種承諾,即不管你異步操做成功與否,這個對象最終都會返回一個值給你。

      代碼示例

const promise = new Promise((resolve, reject) => {
  $.ajax('https://github.com/users', (value) =>  {
    resolve(value);
  }).fail((err) => {
    reject(err);
  });
});
promise.then((value) => {
  console.log(value);
},(err) => {
  console.log(err);
});
//也能夠採起下面這種寫法
promise.then(value => console.log(value)).catch(err => console.log(err));
複製代碼

上面的例子,會在Ajax請求成功後調用resolve回調函數來處理結果,若是請求失敗則調用reject回調函數來處理錯誤。Promise對象內部包含三種狀態,分別爲pending,fulfilled和rejected。這三種狀態能夠類比於咱們日常在ajax數據請求過程的pending,success,error。一開始請求發出後,狀態是Pending,表示正在等待處理完畢,這個狀態是中間狀態並且是單向不可逆的。成功得到值後狀態就變爲fulfilled,而後將成功獲取到的值存儲起來,後續能夠經過調用then方法傳入的回調函數來進一步處理。而若是失敗了的話,狀態變爲rejected,錯誤能夠選擇拋出(throw)或者調用reject方法來處理。

Promise基本語法以下

  • Promise實例必須實現then這個方法

  • then()必須能夠接收兩個函數做爲參數

  • then()返回的必須是一個Promise實例

    eg
    <script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//若是低版本瀏覽器不支持Promise,經過cdn這種方式
          <script type="text/javascript">
            function loadImg(src) {
                var promise = new Promise(function (resolve, reject) {
                    var img = document.createElement('img')
                    img.onload = function () {
                        resolve(img)
                    }
                    img.onerror = function () {
                        reject('圖片加載失敗')
                    }
                    img.src = src
                })
                return promise
            }
            var src = 'https://www.imooc.com/static/img/index/logo_new.png'
            var result = loadImg(src)
            result.then(function (img) {
                console.log(1, img.width)
                return img
            }, function () {
                console.log('error 1')
            }).then(function (img) {
                console.log(2, img.height)
            })
         </script>
    做者:浪裏行舟
    連接:https://juejin.im/post/5b1962616fb9a01e7c2783a8
    來源:掘金
    著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
    複製代碼

       Promise還能夠作更多的事情,好比,有若干個異步任務,須要先作任務1,若是成功後再作任務2,任何任務失敗則再也不繼續並執行錯誤處理函數。要串行執行這樣的異步任務,不用Promise須要寫一層一層的嵌套代碼。

       有了Promise,咱們只須要簡單地寫job1.then(job2).then(job3).catch(handleError); 其中job一、job2和job3都是Promise對象。

       好比咱們想實現第一個圖片加載完成後,再加載第二個圖片,若是其中有一個執行失敗,就執行錯誤函數:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
        var result1 = loadImg(src1) //result1是Promise對象
        var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
        var result2 = loadImg(src2) //result2是Promise對象
        result1.then(function (img1) {
            console.log('第一個圖片加載完成', img1.width)
            return result2  // 鏈式操做
        }).then(function (img2) {
            console.log('第二個圖片加載完成', img2.width)
        }).catch(function (ex) {
            console.log(ex)
        })複製代碼

      這裏需注意的是: then 方法能夠被同一個 promise 調用屢次,then 方法必須返回一個 promise 對象。上例中result1.then若是沒有明文返回Promise實例,就默認爲自己Promise實例即result1,result1.then返回了result2實例,後面再執行.then實際上執行的result2.then。

 Promise的經常使用方法

       除了串行執行若干異步任務外,Promise還能夠並行執行異步任務

      試想一個頁面聊天系統,咱們須要從兩個不一樣的URL分別得到用戶的我的信息和好友列表,這兩個任務是能夠並行執行的,用Promise.all()實現以下:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 得到一個Array: ['P1', 'P2']
});複製代碼

       有些時候,多個異步任務是爲了容錯。好比,同時向兩個URL讀取用戶的我的信息,只須要得到先返回的結果便可。這種狀況下,用Promise.race()實現:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});複製代碼

因爲p1執行較快,Promise的then()將得到結果'P1'。p2仍在繼續執行,但執行結果將被丟棄。

總結:Promise.all接受一個promise對象的數組,待所有完成以後,統一執行success;

Promise.race接受一個包含多個promise對象的數組,只要有一個完成,就執行success。

對上面的例子作下修改,加深對這二者的理解:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
     var result1 = loadImg(src1)
     var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
     var result2 = loadImg(src2)
     Promise.all([result1, result2]).then(function (datas) {
         console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
         console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })
     Promise.race([result1, result2]).then(function (data) {
         console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })複製代碼

若是咱們組合使用Promise,就能夠把不少異步任務以並行和串行的方式組合起來執行。

      Promise.reject(reason): 返回一個新的promise對象,用reason值直接將狀態變爲rejected

const promise2 = new Promise((resolve, reject) => {
  reject('Failed');
});

const promise2 = Promise.reject('Failed');
複製代碼

上面兩種寫法是等價的。

        Promise.resolve(value): 返回一個新的promise對象,這個promise對象是被resolved的。與reject相似,下面這兩種寫法也是等價的。

const promise2 = new Promise((resolve, reject) => {
  resolve('Success');
});

const promise2 = Promise.resolve('Success');
複製代碼

then 利用這個方法訪問值或者錯誤緣由。其回調函數就是用來處理異步處理返回值的。

catch 利用這個方法捕獲錯誤,並處理。

3.Async/Await簡介與用法

簡介

  • async/await是寫異步代碼的新方式,之前的方法有回調函數Promise
  • async/await是基於Promise實現的,它不能用於普通的回調函數。
  • async/await與Promise同樣,是非阻塞的。
  • async/await使得異步代碼看起來像同步代碼,這正是它的魔力所在。

語法

用promise示例和asyn/await示例兩段代碼演示:

promise

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
複製代碼

async/await

const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}

makeRequest()
複製代碼

它們有一些細微不一樣:

  • 函數前面多了一個aync關鍵字。await關鍵字只能用在aync定義的函數內。async函數會隱式地返回一個promise,該promise的reosolve值就是函數return的值。(示例中reosolve值就是字符串」done」)

  • 第1點暗示咱們不能在最外層代碼中使用await,由於不在async函數內。

// 不能在最外層代碼中使用await
await makeRequest()

// 這是會出事情的 
makeRequest().then((result) => {
  // 代碼
})
複製代碼

await getJSON()表示console.log會等到getJSON的promise成功reosolve以後再執行。

相對於promise,async/await的優點有哪些

1.簡潔

       由示例可知,使用Async/Await明顯節約了很多代碼。咱們不須要寫.then,不須要寫匿名函數處理Promise的resolve值,也不須要定義多餘的data變量,還避免了嵌套代碼。這些小的優勢會迅速累計起來,這在以後的代碼示例中會更加明顯。

2.錯誤處理

        Async/Await讓try/catch能夠同時處理同步和異步錯誤。在下面的promise示例中,try/catch不能處理JSON.parse的錯誤,由於它在Promise中。咱們須要使用.catch,這樣錯誤處理代碼很是冗餘。而且,在咱們的實際生產代碼會更加複雜。

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse可能會出錯
        const data = JSON.parse(result)
        console.log(data)
      })
      // 取消註釋,處理異步代碼的錯誤
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}
複製代碼

使用aync/await的話,catch能處理JSON.parse錯誤

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}複製代碼

3.條件語句

下面示例中,須要獲取數據,而後根據返回數據決定是直接返回,仍是繼續獲取更多的數據。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}
複製代碼

這些代碼看着就頭痛。嵌套(6層),括號,return語句很容易讓人感到迷茫,而它們只是須要將最終結果傳遞到最外層的Promise。

上面的代碼使用async/await編寫能夠大大地提升可讀性:

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}
複製代碼

4. 中間值

你極可能遇到過這樣的場景,調用promise1,使用promise1返回的結果去調用promise2,而後使用二者的結果去調用promise3。你的代碼極可能是這樣的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}
複製代碼

若是promise3不須要value1,能夠很簡單地將promise嵌套鋪平。若是你忍受不了嵌套,你能夠將value 1 & 2 放進Promise.all來避免深層嵌套:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {      
      return promise3(value1, value2)
    })
}
複製代碼

這種方法爲了可讀性犧牲了語義。除了避免嵌套,並無其餘理由將value1和value2放在一個數組中。

使用async/await的話,代碼會變得異常簡單和直觀。

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}
複製代碼

5. 錯誤棧

下面示例中調用了多個Promise,假設Promise鏈中某個地方拋出了一個錯誤:

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })
複製代碼

Promise鏈中返回的錯誤棧沒有給出錯誤發生位置的線索。更糟糕的是,它會誤導咱們;錯誤棧中惟一的函數名爲callAPromise,然而它和錯誤沒有關係。(文件名和行號仍是有用的)。

然而,async/await中的錯誤棧會指向錯誤所在的函數:

const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
  })
複製代碼

在開發環境中,這一點優點並不大。可是,當你分析生產環境的錯誤日誌時,它將很是有用。這時,知道錯誤發生在makeRequest比知道錯誤發生在then鏈中要好。

6. 調試

最後一點,也是很是重要的一點在於,async/await可以使得代碼調試更簡單。2個理由使得調試Promise變得很是痛苦:

  • 不能在返回表達式的箭頭函數中設置斷點

const markRequest = () => {
    return callAPromise ()
        .then (() => callAPromise())
        .then (() => callAPromise())
        .then (() => callAPromise())
        .then (() => callAPromise())

}
複製代碼

  • 若是你在.then代碼塊中設置斷點,使用Step Over快捷鍵,調試器不會跳到下一個.then,由於它只會跳過異步代碼。使用await/async時,你再也不須要那麼多箭頭函數,這樣你就能夠像調試同步代碼同樣跳過await語句。

    const markRequest = async () => {
        await callAPromise()
        await callAPromise()
        await callAPromise()
        await callAPromise()
        await callAPromise()
    }複製代碼

總結

      對於經常使用的不一樣異步編程處理方案,我的觀點是針對不一樣的業務場景可根據狀況選擇合適高效的方案,各有優點劣勢,不必頂一個踩一個,雖然技術不斷髮展優化,但有些技術不至於淘汰如此之快,存在即合理。

相關文章
相關標籤/搜索