看promise教你如何優雅的寫js非同步程式碼

xiaoer_03發表於2018-05-20

眾所周知,函式是js的一等公民。何為一等公民,也就是說函式是js中最重要的組成部分。其中一個很重要的體現點就是函式可以是一個函式的引數或者返回值。當一個函式的引數或者返回值是函式時,我們稱這個函式為高階函式。高階函式在js的發展歷程中起了很重要的作用。例如js的模組化,js的非同步程式設計等等。那麼今天我們就來聊聊js的非同步程式設計

js 非同步發展史

1.簡單粗暴的回撥函式
2.相對優雅的Promise
3. 遠看像同步的Generator
4. 同步程式設計的乾兒子之async

回撥函式

回撥函式是javascript中最原始的非同步程式設計寫法,該寫法正是利用了js語法中函式可以作為引數傳遞這一特性,下面我們來看下通過回撥函式來實現js非同步的一個小例子。

// node js 中的檔案讀取
fs.readFile('./a.txt','utf8',(err,data)=>{
  if(err) return err
  console.log(data)
});
複製程式碼

通過上面傳入回撥函式的方式我們可以在檔案讀取完畢後列印出data,這種情況下完全沒問題,而且也很好理解。但如果我們想要實現一個在檔案讀取完畢後再寫入到另外個檔案,寫完之後再進行其他操作呢,依然通過回撥函式來實現。

fs.readFile('./a.txt','utf8',(err,data)=>{
  if(err) return err
  fs.writeFile('./b.txt', data, function(err, data) {
      if (err) return err
      
      ///操作data
  })
});
複製程式碼

可以看到,要想進行一系列非同步操作的話可以簡單粗暴的在一個非同步操作中巢狀另外的操作,一直到你的非同步操作完畢。但這樣下去就會出現傳說中的回撥金字塔問題,不利於我們們程式碼的維護。於是在千呼萬喚中Promise終於誕生了。

Promise

promise翻譯成中文就是承諾的意思。何為承諾,即答應過了就不會再改變。在promise中的體現就是promise的狀態從pending轉換成resolve或者reject就不會再改變。promise的簡單用法是

let promise = function (path) {
    
    return new Promise(function(resolve,reject){
        
        fs.readFile(path, 'uft8', function(err, data){
            if (err) reject(err)
            resolve(data)
        })
    })
}

promise('./a.txt')
.then(data => {
    // 成功後的回撥
    console.log(data)
}, err => {
    // 失敗後的回撥
    console.log(err)
})
複製程式碼

promise有個重要的特性就是promise例項會有個then方法,此方法就類似原始的回撥函式,可以將非同步操作成功或者失敗後的操作寫在then方法中,當然非同步操作成功後再進行的其他非同步操作也是寫在then方法中

promise('./a.txt')
.then(data => {
    處理data
    return new Promise((resolve, reject) => {
        fs.writeFile('./b.txt', data, function(err, data){
            if (err) reject(err)
            resolve(data)
        })
        
    })
}, err => {
    // 失敗後的回撥
    console.log(err)
})
.then(data => {
    // 接收到寫操作成功後的data
    console.log(data)
}, err => {
    console.log(err)
})
複製程式碼

promise的另一個重要特性,也是解決回撥地獄的一個關鍵點。那就是鏈式呼叫。那麼如何才能實現鏈式呼叫,那就是讓then方法的返回值是另外一個promise,但我們平常在編碼中很多時候手動返回的不是一個promise,這個時候promise內部通過呼叫Promise.resolve()方法將該值轉化成了一個promise,這個時候下一個then方法中的第一個函式接受到data就是resolve時傳遞的值。所以我們們可以通過在then方法中一直返回一個新的promise例項這樣一直呼叫then方法去避免噁心的回撥地獄問題。

另外promise例項上還有個catch方法,該方法用於捕獲promise呼叫過程中的錯誤。內部實現為

catch(onRejected) {
    // catch就是then的沒有成功的簡寫
    return this.then(null, onRejected);
}
複製程式碼

另外Promise的幾個靜態方法也特別有用。

1.Promise.resolve()

有時需要將現有物件轉為Promise物件,Promise.resolve方法就起到這個作用。該例項狀態為resolve。

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))

// 內部實現
Promise.resolve = function (val) {
    return new Promise((resolve, reject) => resolve(val))
}
複製程式碼
2.Promise.reject()

Promise.reject(reason)方法也會返回一個新的Promise例項,該例項的狀態為rejected。

const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s)
});
// 出錯了

// 內部實現
Promise.reject = function (val) {
    return new Promise((resolve, reject) => reject(val));
}
複製程式碼
3.Promise.race()

Promise.race方法是將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.race([p1, p2, p3]);
複製程式碼

上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。

// 內部實現
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(resolve, reject);
        }
    });
}
複製程式碼
4.Promise.all()
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.all([p1, p2, p3]);
複製程式碼

上面程式碼中,Promise.all方法接受一個陣列作為引數,p1、p2、p3都是 Promise 例項,如果不是,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。(Promise.all方法的引數可以不是陣列,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 例項。)

// 內部實現
Promise.all = function (promises) {
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0; // i的目的是為了保證獲取全部成功,來設定的索引
        function processData(index,data) {
            arr[index] = data;
            i++;
            if (i === promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
             promises[i].then(data=>{
                processData(i,data);
            }, reject);
        }
     })
}
複製程式碼

Generator

Generator 函式有多種理解角度。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

形式上,Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。

// * 和 yield一起使用,yield產出
function * gen() { // 可以暫停,呼叫next才會繼續走
  let a = yield '買菜'; // a的結果是買回來的菜
  let b = yield a; // b的結果是做好的菜
  return b; // 返回做好的菜
}
let a = gen('菜'); // 執行後返回的是迭代器
console.log(a.next());
console.log(a.next('買好的菜'));
複製程式碼

generator通常是和co一起搭配使用,co可以自動去呼叫generator的next方法,不在手動跳用。下面大概列下自己實現的一個co方法

function co(it) {
  // 非同步遞迴怎麼實現
  return new Promise((resolve,reject)=>{
    function next(data){ // next是為了實現非同步迭代
      let { value, done } = it.next(data);
      if(!done){
        value.then((data=>{
          // 當第一個promise執行完再繼續執行下一個next
          next(data);
        }), reject); // 有一個失敗了就失敗了
      }else{ // 迭代成功後將成功的結果返回
        resolve(value);
      }
    }
    next();
  });
}

co(gen()).then(data=>{
  console.log(data);
});
複製程式碼

async

async 函式是什麼?一句話,它就是 Generator 函式的語法糖。

generator寫法

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
複製程式碼

再來看我們們的async的寫法

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
複製程式碼

一比較就會發現,async函式就是將Generator函式的星號(*)替換成async,將yield替換成await,僅此而已。

async函式對 Generator 函式的改進,體現在以下四點。

(1)內建執行器。 Generator 函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

(2)更好的語義。 async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

(3)更廣的適用性。 co模組約定,yield命令後面只能是 Thunk 函式或 Promise 物件,而async函式的await命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。

(4)返回值是 Promise。 async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。

綜上

Promise是javascript非同步程式設計的發展的一個里程碑。在後面的不管是generator抑或是async都是對promise的進一步封裝。他們的終極目的都是為了讓js的非同步程式設計看著更加同步。方便我們們在平常的開發中維護程式碼。以上是對js非同步程式設計的一點點總結,歡迎吐槽。

相關文章