標籤:

深入 Promise(三)——命名 Promise

目錄:

  1. 深入 Promise(一)——Promise 實現詳解

  2. 深入 Promise(二)——進擊的 Promise

  3. 深入 Promise(三)——命名 Promise

我們經常會遇到這種情況:比如通過用戶名查找並返回該用戶信息和他的關注者。通常有兩種方法:

  1. 定義一個外部變數:

    var userngetUserByName(nswbmw)n .then((_user) => {n user = _usern return getFollowersByUserId(user._id)n })n .then((followers) => {n return {n user,n followersn }n })n

  2. 使用閉包:

    getUserByName(nswbmw)n .then((user) => {n return getFollowersByUserId(user._id).then((followers) => {n return {n user,n followersn }n })n })n

兩種實現都可以,但都不太美觀。於是我之前產生了一個想法:同一層的 then 的參數是之前所有 then 結果的逆序。體現在代碼上就是:

Promise.resolve()n .then(function () {n return getUserByName(nswbmw)n })n .then(function (user) {n return getFollowersByUserId(user._id)n })n .then((followers, user) => {n return {n user,n followersn }n })n

第 3 個 then 的參數是前兩個 then 結果的逆序,即 followers 和 user。更複雜比如嵌套 promise 的我就不列了,這種實現的要點在於:如何區分 then 的層級。從 appoint 的實現我們知道,每個 then 返回一個新的 promise,這導致了無法知道當前 then 來自之前嵌套多深的 promise。所以這個想法無法實現。

命名 Promise

後來,我又想出了一種比上面更好的一種解決方法,即命名 Promise:當前 then 的第一個參數仍然是上個 promise 的返回值(即兼容 Promise/A+ 規範),後面的參數使用依賴注入。體現在代碼上就是:

Promise.resolve()n .then(function user() {n return getUserByName(nswbmw)n })n .then(function followers(_, user) {n return getFollowersByUserId(user._id)n })n .then((_, user, followers) => {n return {n user,n followersn }n })n

上面通過給 then 的回調函數命名(如:user),該回調函數的返回值掛載到 promise 內部變數上(如:values: { user: xxx} ),並把父 promise 的 values 往子 promise 傳遞。then 的第二個之後的參數通過依賴注入實現注入,這就是命名 Promise 實現的基本思路。我們可以給 Promise 構造函數的參數、then 回調函數和 catch 回調函數命名。

於是,我在 appoint 包基礎上修改並發布了 named-appoint 包。

named-appoint 原理:給 promise 添加了 name 和 values 屬性,name 是該 promise 的標識(取 Promise 構造函數的參數、then 回調函數或 catch 回調函數的名字),values 是個對象存儲了所有祖先 promise 的 name 和 value。當父 promise 狀態改變時,設置父 promise 的 value 和 values( this.values[this.name] = value),然後將 values 拷貝到子 promise 的 values,依次往下傳遞。再看個例子:

const assert = require(assert)nconst Promise = require(named-appoint)nnew Promise(function username(resolve, reject) {n setTimeout(() => {n resolve(nswbmw)n })n})n.then(function user(_, username) {n assert(_ === nswbmw)n assert(username === nswbmw)n return {n name: nswbmw,n age: 17n }n})n.then(function followers(_, username, user) {n assert.deepEqual(_, { name: nswbmw, age: 17 })n assert(username === nswbmw)n assert.deepEqual(user, { name: nswbmw, age: 17 })n return [n {n name: zhangsan,n age: 17n },n {n name: lisi,n age: 18n }n ]n})n.then((_, user, followers, username) => {n assert.deepEqual(_, [ { name: zhangsan, age: 17 }, { name: lisi, age: 18 } ])n assert(username === nswbmw)n assert.deepEqual(user, { name: nswbmw, age: 17 })n assert.deepEqual(followers, [ { name: zhangsan, age: 17 }, { name: lisi, age: 18 } ])n})n.catch(console.error)n

很明顯,命名 Promise 有個前提條件是:在同一條 promise 鏈上。如下代碼:

const assert = require(assert)nconst Promise = require(named-appoint)nnew Promise(function username(resolve, reject) {n setTimeout(() => {n resolve(nswbmw)n })n})n.then(() => {n return Promise.resolve()n .then(function user(_, username) {n assert(username === undefined)n return {n name: nswbmw,n age: 17n }n })n})n.then(function (_, username, user) {n assert.deepEqual(_, { name: nswbmw, age: 17 })n assert(username === nswbmw)n assert(user === undefined)n})n.catch(console.error)n

最後一個 then 列印 undefined,因為內部產生了一條新的 promise 鏈分支。

結合 co 使用

與 co 結合使用是沒有什麼變化的,如:

const Promise = require(named-appoint)nconst co = require(co)nnconst promise = Promise.resolve()n .then(function user() {n return nswbmwn })n .then(function followers() {n return [{ name: zhangsan }, { name: lisi }]n })n .then((_, user, followers) => {n return {n user,n followersn }n })nco(function *() {n console.log(yield promise)n /*n { user: nswbmw,n followers: [ { name: zhangsan }, { name: lisi } ] }n */n}).catch(console.error)n

順便擅自製定了一個 Promise/A++ 規範。

『挑剔的』錯誤處理

我們繼續腦洞一下。Swift 中錯誤處理是這樣的:

do {n try getFollowers("nswbmw")n} catch AccountError.No_User {n print("No user")n} catch AccountError.No_followers {n print("No followers")n} catch {n print("Other error")n}n

可以設定 catch 只捕獲特定異常的錯誤,如果之前的 catch 沒有捕獲錯誤,那麼錯誤將會被最後那個 catch 捕獲。通過命名 catch 回調函數 JavaScript 也可以實現類似的功能,我在 appoint 的基礎上修改並發布了 condition-appoint 包。看個例子:

var Promise = require(condition-appoint)nPromise.reject(new TypeError(type error))n .catch(function SyntaxError(e) {n console.error(SyntaxError: , e)n })n .catch(function TypeError(e) {n console.error(TypeError: , e)n })n .catch(function (e) {n console.error(default: , e)n })n

將會被第二個 catch 捕獲,即列印:

TypeError: [TypeError: type error]n

修改一下:

var Promise = require(condition-appoint)nPromise.reject(new TypeError(type error))n .catch(function SyntaxError(e) {n console.error(SyntaxError: , e)n })n .catch(function ReferenceError(e) {n console.error(ReferenceError: , e)n })n .catch(function (e) {n console.error(default: , e)n }) n

將會被第三個 catch 捕獲,即列印:

default: [TypeError: type error]n

因為沒有對應的錯誤 catch 函數,所以最終被一個匿名的 catch 捕獲。再修改一下:

var Promise = require(condition-appoint)nPromise.reject(new TypeError(type error))n .catch(function SyntaxError(e) {n console.error(SyntaxError: , e)n })n .catch(function (e) {n console.error(default: , e)n })n .catch(function TypeError(e) {n console.error(TypeError: , e)n }) n

將會被第二個 catch 捕獲,即列印:

default: [TypeError: type error]n

因為提前被匿名的 catch 方法捕獲。

condition-appoint 實現原理很簡單,就在 appoint 的 then 里加了 3 行代碼:

Promise.prototype.then = function (onFulfilled, onRejected) {n ...n if (isFunction(onRejected) && this.state === REJECTED) {n if (onRejected.name && ((this.value && this.value.name) !== onRejected.name)) {n return this;n }n }n ...n};n

判斷傳入的回調函數名和錯誤名是否相等,不是匿名函數且不相等則通過 return this 跳過這個 catch 語句,即實現值穿透。

當然,condition-appoint 對自定義錯誤也有效,只要自定義錯誤設置了 name 屬性。


推薦閱讀:

libuv漫談之線程
Egg.js 中 GraphQL 小試牛刀
Node.js 實現 Hot Reload
Node應用內存泄漏分析方法論與實戰
狼叔的2017年總結

TAG:Promise | Nodejs |