手把手教你Node使用Promise替代回撥函式

前端攻城小牛啊發表於2018-11-26

async 的本質是一個流程控制。其實在非同步程式設計中,還有一個更為經典的模型,叫做 Promise/Deferred 模型(當然還有更多相關解決方法,比如 eventproxy,co 等,到時候遇到在挖坑)

手把手教你Node使用Promise替代回撥函式

首先,我們思考一個典型的非同步程式設計模型,考慮這樣一個題目:讀取一個檔案,在控制檯輸出這個檔案內容

var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
  console.log(data);
});
複製程式碼

看起來很簡單,再進一步: 讀取兩個檔案,在控制檯輸出這兩個檔案內容

var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
  console.log(data);
  fs.readFile('2.txt', 'utf8', function (err, data) {
    console.log(data);
  });
});//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼

要是讀取更多的檔案呢

var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
  fs.readFile('2.txt', 'utf8', function (err, data) {
    fs.readFile('3.txt', 'utf8', function (err, data) {
      fs.readFile('4.txt', 'utf8', function (err, data) {
        // ...
      });
    });//歡迎加入全棧開發交流圈一起學習交流:864305860
  });
});
複製程式碼

這就是傳說中的 callback hell,可以使用 async 來改善這段程式碼,但是在本例中我們要用 promise/defer 來改善它 promise 基本概念 首先它是一個物件,它和 javascript 普通的物件沒什麼區別,同時,它也是一種規範,跟非同步操作約定了統一的介面,表示一個非同步操作的最終結果,以同步的方式來寫程式碼,執行的操作是非同步的,但又保證程式執行的順序是同步的

    1. promise 只有三種狀態,未完成,完成 (fulfilled) 和失敗 (rejected)
    1. promise 的狀態可以由未完成轉換成完成,或者未完成轉換成失敗
    1. promise 的狀態轉換隻發生一次

promise 有一個 then 方法,then 方法可以接受 3 個函式作為引數。前兩個函式對應 promise 的兩種狀態 fulfilled, rejected 的回撥函式。第三個函式用於處理進度資訊 為了理解它,一些重要原理必須記牢:.then() 總是返回一個新的 promise,如下面程式碼:

var promise = readFile()
var promise2 = promise.then(readAnotherFile, console.error)
複製程式碼

這裡 then 的引數 readAnotherFile, console.error 是代表非同步操作成功後的動作 onFulfilled 或失敗後的動作 OnRejected,也就是說,讀取檔案成功後執行 readAnotherFile 函式,否則失敗列印記錄錯誤。這種實現是兩個中只有一種可能 也可以理解為:

promiseSomething().then(function (fulfilled) {
  // 當 promise 狀態變成 fulfilled 時,呼叫此函式
}, function (rejected) {
  // 當 promise 狀態變成 rejected 時,呼叫此函式
}, function (progress) {
  // 當返回進度資訊時,呼叫此函式
});//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼

Promise 法則有兩部分必須分離:

    1. then() 總是返回一個新的 promise,每次你呼叫它,它不管回撥做什麼,因為 .then() 在回撥被呼叫之前已經給了你一個承諾 promise,回撥的行為隻影響承諾 promise 的實施,如果回撥返回一個值,那麼 promise 將使用那個值,如果這個值是一個 promise,返回這個 promise 實施後的值給這個值,如果回撥丟擲錯誤,promise 將拒絕錯誤
    1. 被 .then() 返回的 promise 是一個新的 promise ,它不同於那些 .then() 被呼叫的 promise,promise 長長的鏈條有時會好些隱藏這個事實,不管如何,每次 .then() 呼叫都會產生一個新的 promise,這裡必須注意的是你真正需要考慮的是你最後呼叫 .then() 可能代表失敗,那麼如果你不捕獲這種失敗,那麼容易導致你的錯誤 exception 消失

來看一個利用 q 來處理這種問題的簡單例子:

var Q = require('q');
var defer = Q.defer();
/**
 * 獲取初始 promise
 * @private
 */
function getInitialPromise() {
  return defer.promise;
}//歡迎加入全棧開發交流圈一起學習交流:864305860
  
/**
 * 為 promise 設定三種狀態的回撥函式
 */
getInitialPromise().then(function (success) {
  console.log(success);
}, function (error) {
  console.log(error);
}, function (progress) {
  console.log(progress);
});
defer.notify('in progress'); // 控制檯列印 in progress
defer.resolve('resolve');   // 控制檯列印 resolve
defer.reject('reject');    // 沒有輸出。promise 的狀態只能改變一次
複製程式碼

promise 的傳遞 then 方法會返回一個 promise,在下面這個例子中,我們用 outputPromise 指向 then 返回的 promise。

var outputPromise = getInputPromise().then(function (fulfilled) {
  
  }, function (rejected) {
  
  });
複製程式碼

現在 outputPromise 就變成了受 function(fulfilled) 或者 function(rejected) 控制狀態的 promise 了。直白的意思就是:當 function(fulfilled) 或者 function(rejected) 返回一個值,比如一個字串,陣列,物件等等,那麼 outputPromise 的狀態就會變成 fulfilled。 在下面這個例子中,我們可以看到,當我們把 inputPromise 的狀態通過 defer.resovle() 變成 fulfilled 時,控制檯輸出 fulfilled. 當我們把 inputPromise 的狀態通過 defer.reject() 變成 rejected,控制檯輸出 rejected

var Q = require('q');
var defer = Q.defer();
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
 * 通過 defer 獲得 promise
 * @private
 */
function getInputPromise() {
  return defer.promise;
}  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
 * 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 function(rejected)
 * 將 then 返回的 promise 賦給 outputPromise
 * function(fulfilled) 和 function(rejected) 通過返回字串將 outputPromise 的狀態由
 * 未完成改變為 fulfilled
 * @private
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
var outputPromise = getInputPromise().then(function (fulfilled) {
  return 'fulfilled';
}, function (rejected) {
  return 'rejected';
});  
/**
 * 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled),控制檯列印 'fulfilled: fulfilled'。
 * 當 outputPromise 狀態由未完成變成 rejected, 呼叫 function(rejected), 控制檯列印 'rejected: rejected'。
 */
outputPromise.then(function (fulfilled) {
  console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
  console.log('rejected: ' + rejected);
});
  /**//歡迎加入全棧開發交流圈一起學習交流:864305860
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
defer.reject(); // 輸出 fulfilled: rejected  
/**
 * 將 inputPromise 的狀態由未完成變成 fulfilled
 */
//defer.resolve(); // 輸出 fulfilled: fulfilled
複製程式碼

當 function(fulfilled) 或者 function(rejected) 丟擲異常時,那麼 outputPromise 的狀態就會變成 rejected

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
  
/**
 * 通過 defer 獲得 promise
 * @private
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
  return defer.promise;
}
  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
 * 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 function(rejected)
 * 將 then 返回的 promise 賦給 outputPromise
 * function(fulfilled) 和 function(rejected) 通過丟擲異常將 outputPromise 的狀態由
 * 未完成改變為 reject
 * @private
 */
var outputPromise = getInputPromise().then(function (fulfilled) {
  throw new Error('fulfilled');
}, function (rejected) {
  throw new Error('rejected');
});  
/**
 * 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)。
 * 當 outputPromise 狀態由未完成變成 rejected, 呼叫 function(rejected)。
 */
outputPromise.then(function (fulfilled) {
  console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
  console.log('rejected: ' + rejected);
});  
/**
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
defer.reject();   // 控制檯列印 rejected [Error:rejected]
  //歡迎加入全棧開發交流圈一起學習交流:864305860
/**
 * 將 inputPromise 的狀態由未完成變成 fulfilled
 */
//defer.resolve(); // 控制檯列印 rejected [Error:fulfilled]
複製程式碼

當 function(fulfilled) 或者 function(rejected) 返回一個 promise 時,outputPromise 就會成為這個新的 promise. 這樣做的意義在於聚合結果 (Q.all),管理延時,異常恢復等等 比如說我們想要讀取一個檔案的內容,然後把這些內容列印出來。可能會寫出這樣的程式碼:

// 錯誤的寫法
var outputPromise = getInputPromise().then(function (fulfilled) {
  fs.readFile('test.txt', 'utf8', function (err, data) {
    return data;
  });//歡迎加入全棧開發交流圈一起學習交流:864305860
});
複製程式碼

然而這樣寫是錯誤的,因為 function(fulfilled) 並沒有返回任何值。需要下面的方式:

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
  
/**
 * 通過 defer 獲得promise
 * @private
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
  return defer.promise;
}
  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil時,呼叫 function(fulfilled)
 * 當 inputPromise 狀態由未完成變成 rejected時,呼叫 function(rejected)
 * 將 then 返回的 promise 賦給 outputPromise
 * function(fulfilled) 將新的 promise 賦給 outputPromise
 * 未完成改變為 reject
 * @private
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
var outputPromise = getInputPromise().then(function (fulfilled) {
  var myDefer = Q.defer();
  fs.readFile('test.txt', 'utf8', function (err, data) {
    if (!err && data) {
      myDefer.resolve(data);
    }
  });//歡迎加入全棧開發交流圈一起學習交流:864305860
  return myDefer.promise;
}, function (rejected) {
  throw new Error('rejected');
});
  
/**
 * 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled),控制檯列印 test.txt 檔案內容。
 *
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
  console.log(fulfilled);
}, function (rejected) {
  console.log(rejected);
});  
/**
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
//defer.reject();  
/**
 * 將 inputPromise 的狀態由未完成變成 fulfilled
 */
defer.resolve(); // 控制檯列印出 test.txt 的內容
複製程式碼

方法傳遞 方法傳遞有些類似於 Java 中的 try 和 catch。當一個異常沒有響應的捕獲時,這個異常會接著往下傳遞 方法傳遞的含義是當一個狀態沒有響應的回撥函式,就會沿著 then 往下找 沒有提供 function(rejected)

var outputPromise = getInputPromise().then(function (fulfilled) { })
複製程式碼

如果 inputPromise 的狀態由未完成變成 rejected, 此時對 rejected 的處理會由 outputPromise 來完成

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
 * 通過defer獲得promise
 * @private
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
  return defer.promise;
}
  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
 * 當 inputPromise 狀態由未完成變成 rejected 時,這個 rejected 會傳向 outputPromise
 */
var outputPromise = getInputPromise().then(function (fulfilled) {
  return 'fulfilled'
});//歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
  console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
  console.log('rejected: ' + rejected);
});
  /**
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
defer.reject('inputpromise rejected'); // 控制檯列印 rejected: inputpromise rejected
/**
 * 將 inputPromise的狀態由未完成變成fulfilled
 *///歡迎加入全棧開發交流圈一起學習交流:864305860
//defer.resolve();
複製程式碼

沒有提供 function(fulfilled)

var outputPromise = getInputPromise().then(null, function (rejected) { })
複製程式碼

如果 inputPromise 的狀態由未完成變成 fulfilled, 此時對 fulfil 的處理會由 outputPromise 來完成

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
  
/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
  return defer.promise;
}//歡迎加入全棧開發交流圈一起學習交流:864305860
  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil時,傳遞給 outputPromise
 * 當 inputPromise 狀態由未完成變成 rejected時,呼叫 function(rejected)
 * function(fulfilled) 將新的 promise 賦給 outputPromise
 * 未完成改變為 reject
 * @private
 */
var outputPromise = getInputPromise().then(null, function (rejected) {
  return 'rejected';
});  //歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
  console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
  console.log('rejected: ' + rejected);
});  
/**
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
// defer.reject('inputpromise rejected');  
/**
 * 將 inputPromise 的狀態由未完成變成fulfilled
 */
defer.resolve('inputpromise fulfilled'); // 控制檯列印fulfilled: inputpromise fulfilled
複製程式碼

可以使用 fail(function(error)) 來專門針對錯誤處理,而不是使用 then(null,function(error))

var outputPromise = getInputPromise().fail(function (error) { })
複製程式碼

看這個例子:

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
  return defer.promise;
}
  
/**
 * 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 then(function(fulfilled))
 * 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 fail(function(error))
 * function(fulfilled) 將新的 promise 賦給 outputPromise
 * 未完成改變為reject
 * @private
 */
var outputPromise = getInputPromise().then(function (fulfilled) {
  return fulfilled;
}).fail(function (error) {
  console.log('fail: ' + error);
});//歡迎加入全棧開發交流圈一起學習交流:864305860
/**
 * 將 inputPromise 的狀態由未完成變成 rejected
 */
defer.reject('inputpromise rejected');// 控制檯列印 fail: inputpromise rejected
  
/**
 * 將 inputPromise 的狀態由未完成變成 fulfilled
 */
//defer.resolve('inputpromise fulfilled');
複製程式碼

可以使用 progress(function (progress)) 來專門針對進度資訊進行處理,而不是使用 then(function (success) { }, function (error) { }, function (progress) { })

var Q = require('q');
var defer = Q.defer();
/**
 * 獲取初始 promise
 * @private
 */
function getInitialPromise() {
  return defer.promise;
}
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
 * 為 promise 設定 progress 資訊處理函式
 */
var outputPromise = getInitialPromise().then(function (success) {
  
}).progress(function (progress) {
  console.log(progress);
});
  
defer.notify(1);
defer.notify(2); // 控制檯列印 1,2
複製程式碼

promise 鏈 promise 鏈提供了一種讓函式順序執行的方法 函式順序執行是很重要的一個功能。比如知道使用者名稱,需要根據使用者名稱從資料庫中找到相應的使用者,然後將使用者資訊傳給下一個函式進行處理

var Q = require('q');
var defer = Q.defer();
// 一個模擬資料庫
var users = [{ 'name': 'andrew', 'passwd': 'password' }];
function getUsername() {
  return defer.promise;
}
  function getUser(username) {
  var user;
  users.forEach(function (element) {
    if (element.name === username) {
      user = element;
    }
  });//歡迎加入全棧開發交流圈一起學習交流:864305860
  return user;
}  
// promise 鏈
getUsername().then(function (username) {
  return getUser(username);
}).then(function (user) {
  console.log(user);
});  
defer.resolve('andrew');
複製程式碼

我們通過兩個 then 達到讓函式順序執行的目的。 then 的數量其實是沒有限制的。當然,then 的數量過多,要手動把他們連結起來是很麻煩的。比如

foo(initialVal).then(bar).then(baz).then(qux)
複製程式碼

這時我們需要用程式碼來動態製造 promise 鏈

var funcs = [foo, bar, baz, qux]
var result = Q(initialVal) 
funcs.forEach(function (func) {
  result = result.then(func)
})
return result
複製程式碼

當然,我們可以再簡潔一點

var funcs = [foo, bar, baz, qux]
funcs.reduce(function (pre, current),Q(initialVal){
  return pre.then(current)
})//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼

看一個具體的例子

function foo(result) {
  console.log(result);
  return result + result;
}
  
// 手動連結
Q('hello').then(foo).then(foo).then(foo);
  
// 控制檯輸出: hello
// hellohello
// hellohellohello
  
// 動態連結
var funcs = [foo, foo, foo];
var result = Q('hello');
  
funcs.forEach(function (func) {
  result = result.then(func);
});
  //歡迎加入全棧開發交流圈一起學習交流:864305860
// 精簡後的動態連結
funcs.reduce(function (prev, current) {
  return prev.then(current);
}, Q('hello'));
複製程式碼

對於 promise 鏈,最重要的是需要理解為什麼這個鏈能夠順序執行。如果能夠理解這點,那麼以後自己寫 promise 鏈可以說是輕車熟路啊 promise 組合 回到我們一開始讀取檔案內容的例子。如果現在讓我們把它改寫成 promise 鏈,是不是很簡單呢?

var Q = require('q'),
  fs = require('fs');  
function printFileContent(fileName) {
  return function () {
    var defer = Q.defer();
    fs.readFile(fileName, 'utf8', function (err, data) {
      if (!err && data) {
        console.log(data);
        defer.resolve();
      }
    })
    return defer.promise;
  }
} //歡迎加入全棧開發交流圈一起學習交流:864305860
// 手動連結
printFileContent('sample01.txt')()
  .then(printFileContent('sample02.txt'))
  .then(printFileContent('sample03.txt'))
  .then(printFileContent('sample04.txt'));  // 控制檯順序列印 sample01 到 sample04 的內容
複製程式碼

我們會發現為什麼要他們順序執行呢,如果他們能夠並行執行不是更好嗎? 我們只需要在他們都執行完成之後,得到他們的執行結果就可以了 我們可以通過 Q.all([promise1,promise2...]) 將多個 promise 組合成一個 promise 返回。 注意:

    1. 當 all 裡面所有的 promise 都 fulfil 時,Q.all 返回的 promise 狀態變成 fulfil
    1. 當任意一個 promise 被 reject 時,Q.all 返回的 promise 狀態立即變成 reject

我們來把上面讀取檔案內容的例子改成並行執行吧

var Q = require('q');
var fs = require('fs');
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
 *讀取檔案內容
 *@private
 */
function printFileContent(fileName) {
  // Todo: 這段程式碼不夠簡潔。可以使用 Q.denodeify 來簡化
  var defer = Q.defer();  
  fs.readFile(fileName, 'utf8', function (err, data) {
    if (!err && data) {
      console.log(data);
      defer.resolve(fileName + ' success ');
    } else {
      defer.reject(fileName + ' fail ');
    }
  })  //歡迎加入全棧開發交流圈一起學習交流:864305860
  return defer.promise;  
}  
Q.all([printFileContent('sample01.txt'), printFileContent('sample02.txt'), printFileContent('sample03.txt'), printFileContent('sample04.txt')])
  .then(function (success) {
    console.log(success);
  }); // 控制檯列印各個檔案內容 順序不一定
複製程式碼

現在知道 Q.all 會在任意一個 promise 進入 reject 狀態後立即進入 reject 狀態。如果我們需要等到所有的 promise 都發生狀態後(有的 fulfil, 有的 reject),再轉換 Q.all 的狀態, 這時我們可以使用 Q.allSettled

var Q = require('q'),
  fs = require('fs');
/**
 *讀取檔案內容
 *@private
 */  
function printFileContent(fileName) {  
  // Todo: 這段程式碼不夠簡潔。可以使用Q.denodeify來簡化
  var defer = Q.defer();  
  fs.readFile(fileName, 'utf8', function (err, data) {
    if (!err && data) {
      console.log(data);
      defer.resolve(fileName + ' success ');
    } else {
      defer.reject(fileName + ' fail ');
    }//歡迎加入全棧開發交流圈一起學習交流:864305860
  })
    return defer.promise;    
}  
Q.allSettled([printFileContent('nosuchfile.txt'), 
printFileContent('sample02.txt'), 
printFileContent('sample03.txt'), 
printFileContent('sample04.txt')])
  .then(function (results) {
    results.forEach(
      function (result) {
        console.log(result.state);
      }
    );
  });//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼

結束 promise 鏈 通常,對於一個 promise 鏈,有兩種結束的方式。第一種方式是返回最後一個 promise 如 return foo().then(bar); 第二種方式就是通過 done 來結束 promise 鏈 如 foo().then(bar).done() 為什麼需要通過 done 來結束一個 promise 鏈呢? 如果在我們的鏈中有錯誤沒有被處理,那麼在一個正確結束的 promise 鏈中,這個沒被處理的錯誤會通過異常丟擲

var Q = require('q'); 
function getPromise(msg, timeout, opt) { 
  var defer = Q.defer(); 
  setTimeout(function () {
    console.log(msg);
    if (opt)
      defer.reject(msg);
    else
      defer.resolve(msg);
  }, timeout); 
  return defer.promise; 
}  //歡迎加入全棧開發交流圈一起學習交流:864305860
/**
 * 沒有用 done() 結束的 promise 鏈
 * 由於 getPromse('2',2000,'opt') 返回 rejected, getPromise('3',1000) 就沒有執行
 * 然後這個異常並沒有任何提醒,是一個潛在的 bug
 */
getPromise('1', 3000)
  .then(function () { return getPromise('2', 2000, 'opt') })
  .then(function () { return getPromise('3', 1000) });
  /**
 * 用 done() 結束的 promise 鏈
 * 有異常丟擲
 */
getPromise('1', 3000)
  .then(function () { return getPromise('2', 2000, 'opt') })
  .then(function () { return getPromise('3', 1000) })
  .done();
複製程式碼

promise 代表一個非同步操作的最終結果。主要通過 promise 的 then 方法訂閱其最終結果的處理回撥函式,和訂閱因某原因無法成功獲取最終結果的處理回撥函式。

A 與 A+ 的不同點

  • A+ 規範通過術語 thenable 來區分 promise 物件
  • A+ 定義 onFulfilled/onRejectd 必須是作為函式來呼叫,而且呼叫過程必須是非同步的
  • A+ 嚴格定義了 then 方法鏈式呼叫時,onFulfilled/onRejectd 的呼叫順序

結語

感謝您的觀看,如有不足之處,歡迎批評指正。

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。

相關文章