JS 異步編程解決方案:Promise、Generator、Async/Await

一、Prommise

1.1 簡介

回想一下我們使用傳統方式(回調函數)是如何來解決異步編程依賴問題的

login(param, function() {
  consloe.log('登錄成功,開始獲取用戶信息')
  getUser(param, function() {
    console.log('已經獲取到用戶信息,現在來獲取菜單')
    getMenu(param, function() {
      console.log('獲取到菜單了')
    })
  })
})

恐怖嗎?這還只是三層依賴,如果有更深的依賴關係,那真的會陷入 ajax 的回調地獄。爲了解決這個問題, 晴空一聲霹靂響,天上掉下個 Promise

Promise 是異步編程的一種解決方案,ES6 將其寫入了語言標準。譯爲 “承諾”,你將一件事情交給他管理,等到這個事情有了結果,或者成功或者失敗,它會承諾給你一個結果。

Promise 可以理解爲一個容器,它肚子裏裝着一個函數,通常在這個函數裏進行一些異步請求,會在將來某個時候拿到結果。Promsie 對象具有以下兩個特點:

  • 三個狀態:pending(進行中)、fullfiled(已成功)、rejected(已失敗),狀態只能在異步函數中改變,外界無法改變其狀態
  • 狀態不可逆:狀態只能從 pending -> fullfiled 或 pending -> rejected

1.2 基本用法

先介紹下 Promise 的基本用法,ES6 規定,Promise 是一個構造函數,使用生成 Promise 實例。

var login = function (param) {
  return new Promise(function (resolve, reject) {
    ajax('http://127.0.0.1/login', function(rest) {
      if (rest.success) {
        resolve(rest)
      } else {
        reject(new Error('登錄失敗'))
      }
    })
  })
}

構造函數 Promise 接收一個函數作爲入參,該函數接收 resolve、reject 兩個參數,這兩個參數會由 js 引擎提供,無需自己傳入

  1. 當異步函數判斷結果爲成功時調用 resolve,這時 Promise 的狀態就會從 pending 變成 fullfiled
  2. 當異步函數判斷結果爲失敗時調用reject,這時 Promise 的狀態就會從 pending 變成 rejected

現在我們用 Promise 把 login 做了改造,return 了一個 Promise 實例。那麼這個 login 該怎麼用呢?

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}, function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
})

login 函數執行會返回一個 Promise 對象,Promise 對象上有個 then 方法,它接受兩個函數

  • 前者爲異步成功時的回調,暫時可以理解爲註冊了 resolve 函數
  • 後者爲異步失敗時的回調,暫時可以理解爲註冊了 reject 函數

reject 函數除了可以註冊在 then 函數的第二個參數上以外,還可以註冊在 Promise.prototype.catch 函數中

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
})

不管成功或者失敗,我都想執行一步操作怎麼辦呢?ES9 引入了 Promise.prototype.finally 函數

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
}).finally(function () {
  // 不管成功或者失敗,都會走到這裏
})

1.3 鏈式調用

咋一看,用了 Promise 之後代碼量還增加了一點對吧!那它帶來的好處是什麼呢?別急,接着往下看

當我們把另外兩個接口 getUser、getMenu 兩個接口都用 Promise 改造之後......

login(param)
.then(function(loginData) {
    return getUser(loginData)
}).then(funtion(userData) {
    return getMenu(userData)
}).then(function(menuData) {
    // 可以一直鏈下去
})

 我們跳出了嵌套地獄,函數不用再寫在回調裏了,只需要使用 Promise 提供的 then 函數把所以的依賴按順序給鏈接起來

  • 前一個異步執行結束之後纔會執行下一個異步
  • 前一個 then 裏面的異步執行結果會傳入到下一個 then 註冊的函數裏,如上述代碼中 getUser 的結果會傳給 getMenu 這一行

1.4 Promise.all

上面是鏈式串行調用的方式,各個函數之間是有依賴關係的。但是現在我有這個需求

  • 異步函數之間沒有依賴關係
  • 我想要它們並行的去執行,這樣會比串行快很多
  • 等到所有的函數都執行完了把結果一起返回給我

這個時候 Promise.all 就排上用場了,現在假設上文中的 login、getUser、getMenu 三個函數之後沒有依賴關係

Promise.all([login(), getUser(), getMenu()]).then(function(rest) {
    // 三個異步函數都執行完了纔會走到這裏
    console.log(rest); // [loginData, userData, menuData]
}).catch(function(e) {
    // 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})

Promise.all 函數的入參是一個數組,數組中的每個元素都是一個 promise 實例,經過 Promise.all 封裝成了一個新的 promise 實例。等到數組中所有 promise 實例的狀態都變成 resolved 的時候就會,這個新的 promise 實例的狀態頁面變成 resolved。如果數組中有一個 promise 的狀態變成了 rejected,那麼這個新的 promise 的狀態也就變成了 rejected。

1.5 Promise.race

現在我的需求又變了,這三個異步函數誰先執行完我就先處理誰,其他兩個我就不管了。這個時候就得用 Promise.race 了,我們稱之爲“競賽模式”。

Promise.race([login(), getUser(), getMenu()]).then(function(rest) {
    // 只要有一個先執行結束,它的結果就會返回到 rest 上
    console.log(rest); // loginData 或 userData 或 menuData
}).catch(function(e) {
    // 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})

1.6 自己擼一個

現在都講究造輪子,既然 Promise 的用法我們都已經知道了,那就自己來擼一個吧!

從用法上我們可以看出一下幾點

  • Promise 是一個構造函數
  • 有一個狀態
  • 有兩個數組,一個用來保存成功時的回調函數,一個用來保存失敗時的回調函數
  • 原型上有 then、catch、finally 這麼幾個方法
  • 有 resolve、reject、all、reace 這麼幾個靜態方法

構造函數

function Promise (executor) {
  var self = this;
  this.status = 'pending';  // 狀態
  this.data = undefined;
  this.onResolvedCallback = []; // 通過 then 註冊的成功回調
  this.onRejectedCallback = []; // 通過 then 或者 catch 註冊的失敗回調
  
  function resolve (value) {
    if (this.status === 'pending') {
      self.status = 'resolved'; // 成功時將狀態改爲 resolved
      self.data = value;
      for (var i=0; i<this.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value) // 執行註冊的成功函數
      }
    }
  }

  function reject (reason) {
    if (this.status === 'pending') {
      self.status = 'rejected'; // 失敗時將狀態改爲 rejected
      for (var i=0; i<this.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason) // 執行註冊的失敗函數
      }
    }
  }

  try {
    executor(resolve, reject);  // new Promise 的時候立即執行 executor
  } catch (e) {
    reject(e)
  }
}

then 

then 函數是用來註冊成功或者失敗時的回調函數的。需要注意的是,當調用 then 函數來註冊回調時,promise 實例的狀態有可能是 pending、resolved、rejected。

Promise.prototype.then = function (onResolved, onRejected) {
  // 爲了可以使用鏈式寫法,then 方法返回的是一個 promise 對象
  if (self.status === 'pending') {
    // 通常情況下,如果 promise 包裹的是一個異步操作,那麼走到 then 是,promise 應該還是 pending狀態
    return promise1 = new Promise(function (resolve, reject) {
      // 註冊成功回調
      // self.onResolveCallback.push(onResolved)
      // 本來我們直接把 onResolved push 到 onResolveCallback 這個數組裏就可以了,異步執行完成時會調用到這個 onResolved,並把結果傳進去
      // 但是 onResolved 裏面可能還有一個異步操作(假設p),我們得等到這個異步p的狀態改變了之後,才能繼續下一個 then,這樣才能完成正確的依賴關係
      // 所以我們還得判斷 onResolved 函數的返回結果
      self.onResolveCallback.push(function (value) {
        var x = onResolved(value);
        if (x instanceof Promise) {
          // 如果 onResolved 返回的仍是一個 promise,那就等等到這個 promise 的狀態改變了之後,才能改變 promise1 的狀態,然後繼續下一個 then
          x.then(resolve, reject)
        } else {
          // 否則直接改變 promise1 的狀態,把 promise1 的執行結果 x 傳入到下一個 then 中的回調
          resolve(x);
        }

      })

      // 註冊失敗回調
      self.onRejectCallback.push(function(reason) {
        var x = onRejected(reason);
        if (x instanceof Promise) {
          x.then(resolve, reject)
        } else {
          reject(reason)
        }
      })
    })
    
    
  }
  if (self.status === 'resolved') {
    // 如果 new Promise(executor) 的時候,傳入的 executor 內並沒有執行異步操作,而是直接調用了 resolve,那麼走到 then 的時候,Promise 已經是 resolved 狀態了
    // 爲了能夠鏈式調用我們還是要返回一個 promise
    return new Promise(function(resolve, reject) {
      // 由於狀態已經是 resolved 了,就不需要把 onResolved、onRejected push 到隊列中了
      // 直接把 executor 的執行直接塞進 onResolved 就行了
      var x = onResolved(self.value)
      if (x instanceof Promise) {
        x.then(resolve, reject)
      } else {
        resolve(x)
      }
    })
  }
  if (self.status === 'rejected') {
    // 邏輯和上面差不多,只是最後一步應該執行 reject(x)
  }
}

all

做一個計數,等所有 promise 都完成了,執行 resolve 改變狀態就行了

Promise.all = function(arr) {
  return new Promise(function (resolve, reject) {
    var count = 0;  // 記錄已完成了 promise
    var list = []
    // 數組 arr 中每一項都是一個 promis 對象
    arr.forEach((p,index) => {
      p.then(res => {
        count ++;
        list[index] = res;
        if (count === arr.length) {
          // 所有的 promise 都成功了
          resolve(list)
        }
      }).catch(e => {
        reject(e)
      })
    })
  })
}

race

做一個標識,第一個完成的 promise 執行 resolve 改變下狀態,其他的就不管了

Promise.race = function(arr) {
  return new Promise(function (resolve, reject) {
    // 做一個標識,如果有一個完成了執行 resolve,等其他的 promise 完成了就不管了
    var done = false;
    // 數組 arr 中每一項都是一個 promis 對象
    arr.forEach((p,index) => {
      p.then(res => {
        if (!done) {
          resolve(res)
        }
      }).catch(e => {
        if (!done) {
          reject(e)
        }
      })
    })
  })
}

 重要的就這麼幾個函數,好了,Promise 的使用和實現到此結束。

然而技術總是在不斷的更新,程序員在追求代碼優雅的路上也在不斷的探索。

二、Generator

2.1 簡介

Generator 是 ES6 提出的一種異步編程解決方案,語法上可以將它理解爲一個狀態機,封裝了多個內部狀態。同時 Generator 也是一個遍歷器生成函數,執行它可以生成一個遍歷器,用於遍歷 Generator 函數內部的每個狀態。

2.2 基本用法

function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

執行 test() 函數時,其內部的代碼並沒有開始執行,而是生成了一個遍歷器 b,執行 b.next() 時纔開始執行 test 內部的代碼。每次遇到 yield 標誌就會暫停,如同一個指針一步一步往下執行,直到遇到 return 或者 沒有代碼爲止。

2.3 原理

function test () {
  var a;
  return generator (function (_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          a = 1 + 2;
          _context.next = 4;  // 標記下一個需要執行的代碼塊
          return 2; // 返回 yield 後的內容
        case 4:
          _context.next = 6;
          return 3;
        case 6:
        case 'end':
          return _context.stop();
      }
    }
  })
}

function generator (cb) {
  return (function () {
    var obj = {
      next: 0,
      stop: function () {}
    }
    return {
      next: function () {
        var ret = cb(obj);
        if (ret === undefined)
          return { value: undefined, done: true }
        return { value: ret, done: false }

      }
    }
  })()
}

 這是基本用法中的代碼被 babel 轉譯後的樣子,babel 會根據 yield 將代碼分割三塊,然後給每一塊代碼加一個編號,通過 switch、case 傳入的參數值來決定執行哪一塊代碼。

  • 第一次 b.next(),傳入到 test 中的代碼塊編號是 0,在這個代碼塊中,會標記出下一個要執行的代碼塊編號,並返回 yield 後的內容。
  • 第二次 b.next(),obj 裏已經知道下一個要執行的代碼塊的編號是 4
  • 以此類推

三、Async/Await

3.1 簡介

async 函數在 ES2017 納入了標準,它是 Generator 函數的語法糖,使得異步操作變得更加簡單。

Generator 函數需要手動去執行 next() 函數,而 async 函數內置了執行器,且返回值是一個 pending 狀態的 Promise,方便是用 then 函數進行鏈式調用。

3.2 基本用法

async 函數的返回值是一個 promise,可以使用 then 註冊回調函數。它和 await 搭配使用,當 async 函數執行到 await 的時候,就會等待 await 後的函數執行完之後再執行後面的內容。比如文章開頭的三個函數就可以寫成這樣

async function start () {
  var loginData = await login().catch(...);
  var userData = await getUser();
  var menuData = await getMenu();
  return menuData;
}
start.then(...)

看起來是不是很簡單、很優雅,我們可以像寫同步代碼一樣去操作異步函數。而且比起 Promise 我們可以更加容易的去中斷鏈路,在任意一行 return 就行了。

這裏需要注意幾點

  • async 必須緊跟着 function,像這種寫法是錯誤的 var async start = function () {}
  • await 必須在 async 申明的函數內部使用,不然會報錯
  • await 後面需要是一個 promise 對象,才能達到“等待”的效果。其他對象是達不到這個效果的,如 await setTimeout()

3.3 原理

async 的實現原理,就是將 Generator 函數和自動執行器包裝在一個函數裏

async function (args) {
  // ...
}
// 等同於
function fn (args) {
  return spawn (function* () {
    // ...
  })
}

所有的 async 函數都可以寫成上面第二種形式,其中 spawn 函數就是自動執行器。那麼現在基本用法中的例子就可以寫成下面這個樣子

function start () {
  function* genF () {
    var loginData = yield login().catch(...);
    var userData = yield getUser();
    var menuData = yield getMenu();
    return menuData;
  }

  return spawn(genF)
}

下面我們來看一下 spawn 的實現方式

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

首先 async 函數返回的是一個 pending 狀態 Promise,那麼 spawn 首先要返回一個 Promise

然後在 Promise 內我們執行 genF() 生成了一個遍歷器 gen

第一次執行遍歷器得到的 next.value,是例子中 login() 返回的 promise。我們將這這個 promise 對象封裝在了 Promise.resolve(next.value)

等到 login 異步執行成功了,也就是next.value(promise)的狀態也變了,這時候就可以執行 Promise.resolve(next.value).then 內的內容,也就是執行下一個 next()(也就是getUser())

以此類推

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章