[翻譯] 使用JavaScript實現本身的Promises

寫在正文前

本文翻譯自Maciej Cieślar的文章:Implementing Promises In JavaScript。這篇文章描述的是在做者瞭解了promises是如何使用以後,是如未嘗試用TypeScript實現promises。文章若有翻譯很差的地方還望多多包涵,有什麼意見建議歡迎在下面的評論區留言。javascript

implementing promises in JavaScript

在編程過程當中我最愛的時刻就是當徹底理解一個概念的時候心裏產生 啊,原來是這樣 的那一刻。 即便這個過程可能很費時,很費力,可是當幸福真的到來的時候,會發現一切都是值得的。html

我認爲評估(也是幫助提升)咱們對於某一個主題的理解程度的最有效的方式就是嘗試並把這些知識應用在實戰中。這麼作不只可讓咱們認識和最終解決咱們的薄弱之處,也可讓咱們對於事物運行的方式有所瞭解。即便一個簡單的試錯方式也會暴露出一些之前常常忽略的細節。java

抱着這個想法,我認爲學習如何實現promises是我編程生涯中最重要的時刻之一,他給了我不同的方式瞭解異步代碼的工做原理,也讓我成爲了一個更好的程序員。git

我但願這個文章能夠幫助你,讓你也能夠逐漸用JavaScript實現本身的promise。程序員

咱們將會注重於根據Promises/A+ 規範以及BluebirdApi的方法來實現Promise。同時會使用Jest實踐測試驅動github

TypeScript也會派上用場。鑑於咱們將會在下面瘋狂的操做,我就假設你對Promise有基本的理解,以及對他們怎麼工做的有模糊的認識。若是你沒有的話,固然這裏也是一個開始的好地方。typescript

既然咱們已經有了方向,那咱們就進入正題,先來克隆分支,而後一塊兒開始吧。npm

Promise 的核心內容

衆所周知,promise是一個擁有下面這些屬性的對象:編程

Then

一個將處理器添加到咱們的promise的方法。它將會返回一個新的promise,其中包含從上一個處理器中的方法傳遞下來的值。api

Handlers處理器

處理器的數組將會附加到then裏面。處理器是擁有onSuccessonFail兩個方法的對象,這兩個方法將會做爲參數傳入到then中then(onSuccess,onFail).

處理器的接口實現代碼以下:

type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;

interface Handler<T, U> {
    onSuccess: HandlerOnSuccess<T, U>;
    onFail: HandlerOnFail<U>;
}
複製代碼

State 狀態

一個promise會有三種狀態中的一種:resolved,rejected,pending.

Resolved 意味着要麼一路順風的運行完了,咱們也接收到值了,要麼咱們捕獲而且處理了咱們的錯誤。

Rejected 意味着要麼咱們的請求被駁回了,要麼咱們的錯誤被拋出可是並無被捕獲。

Pending 意味着當前既沒有resolve也沒有rejected被調用,而且咱們仍然在等待那個值。

有個術語叫作promise已解決意味着promise要麼處於resolved要麼處於rejected。

Value值

值要麼是rejected 要麼是resolved。 一旦這個值定下來了,就毫不能被更改。

測試

根據TDD方法(測試驅動),咱們須要在寫真實的代碼以前編寫測試代碼。下面是咱們的核心代碼的測試用例:

describe('PQ <constructor>',() =>{
    //是promise
    test('resolves like a promise',() => {
        return new PQ<number>((resolve) => { setTimeout(() => { resolve(1); },30); }).then((val) => { expect(val).toBe(1); }); }); //老是異步 test('is always asynchronous', () => { const p = new PQ((resolve) => resolve(5)); expect((p as any).value).not.toBe(5); }) //resolve的時候能獲得指望的值 test('resolves with the expected value',() => { return new PQ<number>((resolve) => resolve(30)).then((val) => { expect(val).toBe(30); }); }); //在調用then以前,resolve了一個thenabled對象 // 「thenable」 是定義了 then 方法的對象. test('resolves a thenable before calling then', () => { return new PQ<number>((resolve)=> resolve(new PQ((resolve) => resolve(30))), ).then((val) => expect(val).toBe(30)); }) //可以捕獲reject狀況下的錯誤 test('catches errors(reject)',()=>{ const error = new Error('Hello there'); return new PQ((resolve,reject) => { return reject(error); }).catch((err: Error) => { expect(err).toBe(error) }) }) //可以捕獲拋出異常狀況下的錯誤 test('catches errors (throw)', () => { const error = new Error('General Kenobi!'); return new PQ(() => { throw error; }).catch((err) => { expect(err).toBe(error); }); }); //promise是不可變的,而且可以返回一個新的promise test('is not mutable - then returns a new promise', () => { const start = new PQ<number>((resolve) => resolve(20)); return PQ.all([ start.then((val)=>{ expect(val).toBe(20); return 30; }).then((val) => expect(val).toBe(30)), start.then(val => expect(val).toBe(20)), ]) }) }) 複製代碼

運行咱們的測試

我強烈建議使用 Visual Studio Code中的Jest插件。它能在後臺運行咱們的測試,而且可以直接在代碼行中展現出結果,綠色表示測試經過,紅色表示測試不經過等。

咱們也能夠經過Output 控制檯看到運行的結果,而後選擇JEST標籤。

jest Tab

還有另外一種方式運行測試。

npm run test
複製代碼

不管咱們怎麼運行測試,均可以看到全部的測試都是不經過的。

那麼如今就讓咱們把它們變爲經過。

實現核心Promise

構造函數

class PQ<T> {
  private state: States = States.PENDING;
  private handlers: Handler<T, any>[] = [];
  private value: T | any;
  public static errors = errors;

  public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
    try {
      callback(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }
}
複製代碼

咱們的構造函數使用回調函數做爲參數。

當咱們調用這個回調函數的時候,使用this.resolvethis.reject做爲參數。

注意正常狀況下,咱們本應該把this.resolvethis.reject綁定在this上,可是這裏咱們使用類的箭頭函數來代替。

設置結果

如今咱們須要設置結果。請記住咱們必須正確的處理結果,那就意味着若是咱們想返回一個promise,咱們必需要先resolve它。

Class PQ<T> {
   // ....
   
   private setResult = ( value : T | any, state: States) => {
        const set = ()=>{
            if( this.state !== States.Pending){
                return null
            }
            
            if( isThenable(value)) {
                return ( value as Thenable <T>).then(this.resolve , this.reject);
            }
            
            this.value = value;
            this.state = state;
            
            return this.executeHandlers();
       };
   setTimeout( set , 0);
};
複製代碼

首先,咱們會檢查狀態是否是沒有處於pending(進行中)狀態 — 若是的確沒有處於pending的話,那就證實,promise已經處理完了,咱們不能給他賦任何新的值。

其次,咱們會檢查 值是不是thenable對象(有then的對象)。簡單的說,thenable(有then的對象)就是有then方法的對象。

按照慣例,一個有then的對象應該表現的像一個promise。因此爲了獲得正確的結果,咱們將調用then,並將this.resolvethis.reject傳遞進去做爲參數。

一旦這個"有then的對象"設置完成,他將會調用咱們的方法之一,而後給出咱們期待中的非promise值。

因此如今咱們須要檢查對象是不是一個有then的對象。

是不是有then的對象的測試:

describe('isThenable',() => {
    test('detects objects with a then method', () => {
        expect(isThenable({ then: () => null })).toBe(true);
        expect(isThenable(null)).toBe(false);
        expect(isThenable({})).toBe(false);
     });
});
複製代碼

isThenable方法的代碼:

const isFunction = (func: any) => 
    typeof func === 'function';

const isObject =  (supposedObject: any) =>
    typeof supposedObject === 'object' &&
    supposedObject !== null &&
    !Array.isArray(supposedObject);
    
const isThenable = (obj: any) => 
    isObject(obj) && isFunction(obj.then);
複製代碼

有一點值得注意的是,即便回調函數的內部代碼是同步,咱們的promise永遠也都不會是同步的。

咱們會使用setTimeout用來延遲執行,直到事件循環的下一輪開始。

如今須要作的就只剩下設置咱們的value值和status值,而後再執行已經寫好的處理程序了。

執行處理器

Class PQ<T> {
        // ...
        
        private executeHandlers = () => {
            if(this.state === State.pending){
                return null
            }
            
            this.handlers.forEach((handler) => {
                if (this.state === States.REJECTED) {
                    return handler.onFail(this.value);
                }
                return handler.onSuccess(this.value);
           })
           
           this.handler = [];
        };
    }
複製代碼

再說一遍,狀態不能是pending。

promise的狀態決定了咱們將要調用的函數。若是是 resolved,咱們將會調用onSuccess,不然,咱們會調用onFail.

爲了安全起見,如今讓咱們清空咱們的處理器數組防止在未來執行任何意料外的操做。由於處理器將會在被添加以後被執行。

這也是咱們接下來必須討論的事情:添加咱們處理器的方式。

attachHandler添加處理器

private attachHandler = (handler: Handler<T , any>) => {
    this.handlers = [ ... this.handlers,handler];
    this.execureHandlers();
};
複製代碼

就像你所看到的這麼簡單,咱們只是往咱們的處理器數組中添加了一個新的處理器,而後執行處理器。僅此而已。

如今,讓咱們把他們放在一塊兒來實現咱們的then方法。

then

Class PQ<T> {
    public then<U>( onSuccess?:HandlerOnSuccess<T , U>,onFail?: HandlerOnFail ) {
        return new PQ< U | T >((resolve, reject) => {
            return this.attachHandler({
            onSuccess: (result) => {
                if(!onSuccess) {
                    return resolve(result);
                }
                
                try{
                    return resolve(onSuccess(result));
                }
                catch (e){
                    return reject(e);
                }
            },
            onFail: (reason) =>{
                if(!onFail){
                    return reject(reason);
                }
                
                try{
                    return resolve(onFail(reason));
                }
                catch(e){
                    return reject(e);
                }
            }
            })
        })
    }
}
複製代碼

在then中,咱們須要返回一個promise,而且在回調函數中咱們須要添加一個將會用於等待當前的promise被處理的處理器。

當發生這種狀況的時候,不管是onSuccess的處理器仍是onFail的處理器被執行,咱們都將按照相應的處理繼續。

有一件須要記得的事情是,並不必定非要把處理器傳遞給then。這很重要的,可是一樣重要的是,咱們不要嘗試執行任何未定義的內容。

還有,當處理器被傳遞給onFail的時候,咱們其實是resolve了返回的promise,由於拋出的錯誤已經被捕獲了。

catch

catch實際上就是then方法的一個抽象。

public catch<U>(onFail: HandlerOnFail<U>) {
    return this.then<U>(identity, onFail);
 }
複製代碼

僅此而已。

Finally

Finally其實也是then(finallyCb, finallyCb)的抽象,由於咱們其實並非真的關心promise的結果。

實際上,他也是還保留了上一個promise的結果,而後把它返回而已。因此finallyCb返回的結果並不重要。

finally的測試用例:

describe('PQ.prototype.finally', () => {
    test('it is called regardless of the promise state', () => {
        let counter = 0
        ;return PQ.resolve(15)
        .finally(() => {
            counter += 1;
        }).then(() => {
            return PQ.reject(15);
        }).then(() => {
            // wont be called
            counter = 1000;
        }).finally(() => {
            counter += 1;
        }).catch((reason) => {
            expect(reason).toBe(15);
            expect(counter).toBe(2);
        });
    });
});
複製代碼
Class PQ<T>{
    public finally<U>(cb: Finally<U>) {
        return new PQ<U>((resolve, reject) => { let val: U | any; let isRejected: boolean; return this.then( (value) => { isRejected = false; val = value; return cb(); },(reason) => { isRejected = true; val = reason; return cb(); }, ).then( () => { if (isRejected) { return reject(val); } return resolve(val); }); }); } } 複製代碼

toString

測試用例:

describe('PQ.prototype.toString', () => {
    test('return [object PQ]',() => {
        expect(new PQ<undefined>((resolve) => resolve()).toString()).toBe(
            '[object PQ]',
        );
    });
});
複製代碼

toString實現代碼

Class PQ<T>{
    public toString() {
        return `[object PQ]`;
    }
}
複製代碼

toString 函數只會返回一個字符串[object PQ]

目前爲止咱們已經實現了咱們的promise的核心方法,如今咱們能夠實現一些以前提到的Bluebird 的方法,這些方法會讓咱們操做promise更簡單。

附加的方法

Promise.resolve

官方文檔的運行方式

測試用例:

describe('PQ.prototype.resolve', () => {
  test('resolves a value', () => {
    return PQ.resolve(15).then((val) => expect(val).toBe(15));
  });
});
複製代碼

實現代碼:

public static resolve<U = any>(value?: U | Thenable<U>) {
    return new PQ<U>((resolve) => {
      return resolve(value);
    });
  }
複製代碼

Promise.reject

官方文檔的運行方式

測試用例

describe('PQ.prototype.reject', () => {
  test('rejects a value', () => {
    const error = new Error('Hello there');

    return PQ.reject(error).catch((err) => expect(err).toBe(error));
  });
});
複製代碼

實現代碼

public static reject<U>(reason?: any) {
    return new PQ<U>((resolve, reject) => {
      return reject(reason);
    });
  }
複製代碼

Promise.all

官方文檔的運行方式

(譯者注:這個api和promise原生的all是有區別的。)

測試用例:

describe('PQ.all', () => {
  test('resolves a collection of promises', () => {
    return PQ.all([PQ.resolve(1), PQ.resolve(2), 3]).then((collection) => {
      expect(collection).toEqual([1, 2, 3]);
    });
  });

  test('rejects if one item rejects', () => {
    return PQ.all([PQ.resolve(1), PQ.reject(2)]).catch((reason) => {
      expect(reason).toBe(2);
    });
  });
});
複製代碼

實現代碼:

public static all<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U[]>((resolve, reject) => { if (!Array.isArray(collection)) { return reject(new TypeError('An array must be provided.')); } let counter = collection.length; const resolvedCollection: U[] = []; const tryResolve = (value: U, index: number) => { counter -= 1; resolvedCollection[index] = value; if (counter !== 0) { return null; } return resolve(resolvedCollection); }; return collection.forEach((item, index) => { return PQ.resolve(item) .then((value) => { return tryResolve(value, index); }) .catch(reject); }); }); } 複製代碼

我認爲這個實現是很是簡單的。

collection.length爲開始,當咱們每次運行tryResolve的時候,會逐一減小這個值,直到這個值爲零,也就是說此時集合中的每一個任務都已經被解決了(resolve)。最後咱們會resolve這個新建立的(每一個任務都處於resovle的)集合。

Promise.any

[工做原理](bluebirdjs.com/docs/api/pr…

測試用例:

describe('PQ.any', () => {
  test('resolves the first value', () => {
    return PQ.any<number>([
      PQ.resolve(1),
      new PQ((resolve) => setTimeout(resolve, 15)),
    ]).then((val) => expect(val).toBe(1));
  });

  test('rejects if the first value rejects', () => {
    return PQ.any([
      new PQ((resolve) => setTimeout(resolve, 15)),
      PQ.reject(1),
    ]).catch((reason) => {
      expect(reason).toBe(1);
    });
  });
});
複製代碼

實現代碼:

public static any<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U>((resolve, reject) => {
      return collection.forEach((item) => {
        return PQ.resolve(item)
          .then(resolve)
          .catch(reject);
      });
    });
  }
複製代碼

咱們只是等待resolve第一個值來並在promise中返回它。

Promise.props

官方文檔的運行方式

測試用例:

describe('PQ.props', () => {
  test('resolves object correctly', () => {
    return PQ.props<{ test: number; test2: number }>({
      test: PQ.resolve(1),
      test2: PQ.resolve(2),
    }).then((obj) => {
      return expect(obj).toEqual({ test: 1, test2: 2 });
    });
  });

  test('rejects non objects', () => {
    return PQ.props([]).catch((reason) => {
      expect(reason).toBeInstanceOf(TypeError);
    });
  });
});
複製代碼

實現代碼:

public static props<U = any>(obj: object) {
    return new PQ<U>((resolve, reject) => { if (!isObject(obj)) { return reject(new TypeError('An object must be provided.')); } const resolvedObject = {}; const keys = Object.keys(obj); const resolvedValues = PQ.all<string>(keys.map((key) => obj[key])); return resolvedValues .then((collection) => { return collection.map((value, index) => { resolvedObject[keys[index]] = value; }); }) .then(() => resolve(resolvedObject as U)) .catch(reject); }); } 複製代碼

咱們迭代傳進對象的鍵,resolve每一個值。而後咱們將值分配給一個新的對象,而後它將用來使promise變爲resolved。

Promise.prototype.spread

官方文檔的運行方式

測試用例

describe('PQ.protoype.spread', () => {
  test('spreads arguments', () => {
    return PQ.all<number>([1, 2, 3]).spread((...args) => {
      expect(args).toEqual([1, 2, 3]);
      return 5;
    });
  });

  test('accepts normal value (non collection)', () => {
    return PQ.resolve(1).spread((one) => {
      expect(one).toBe(1);
    });
  });
});
describe('PQ.spread', () => {
  test('resolves and spreads collection', () => {
    return PQ.spread([PQ.resolve(1), 2, 3], (...args) => {
      expect(args).toEqual([1, 2, 3]);
    });
  });
});
複製代碼

實現代碼:

public static spread<U extends any[]>(
    collection: U,
    handler: HandlerOnSuccess<any[]>,
  ) {
    return PQ.all(collection).spread(handler);
  }
複製代碼

Promise.delay

官方文檔的運行方式

測試代碼:

describe('PQ.delay', () => {
    //在resolve以前等待給定的毫秒數
  test('waits for the given amount of miliseconds before resolving', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(40).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('delay');
    });
  });

  test('waits for the given amount of miliseconds before resolving 2', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(60).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('timeout');
    });
  });
});
複製代碼

實現代碼:

public static delay(timeInMs: number) {
    return new PQ((resolve) => {
      return setTimeout(resolve, timeInMs);
    });
  }
複製代碼

經過使用setTimeout,咱們很容易的就能將執行resolve函數的這個操做推遲給定的毫秒數。

Promise.prototype.timeout

官方文檔的運行方式

測試代碼

describe('PQ.prototype.timeout', () => {
  test('rejects after given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(resolve, 50);
    })
      .timeout(40)
      .catch((reason) => {
        expect(reason).toBeInstanceOf(PQ.errors.TimeoutError);
      });
  });

  test('resolves before given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(() => resolve(500), 500);
    })
      .timeout(600)
      .then((value) => {
        expect(value).toBe(500);
      });
  });
});
複製代碼

實現代碼:

class PQ<T> {

  // ...
  
  public timeout(timeInMs: number) {
    return new PQ<T>((resolve, reject) => { const timeoutCb = () => { return reject(new PQ.errors.TimeoutError()); }; setTimeout(timeoutCb, timeInMs); return this.then(resolve); }); } } 複製代碼

這個其實有一點問題。

若是setTimeout的執行速度比咱們的promise快的化,他將調用咱們特殊的error來拒絕(reject)這個promise。

Promise.promisfy

官方文檔的運行方式

測試用例

describe('PQ.promisify', () => {
  test('works', () => {
    const getName = (firstName, lastName, callback) => {
      return callback(null, `${firstName} ${lastName}`);
    };

    const fn = PQ.promisify<string>(getName);
    const firstName = 'Maciej';
    const lastName = 'Cieslar';

    return fn(firstName, lastName).then((value) => {
      return expect(value).toBe(`${firstName} ${lastName}`);
    });
  });
});
複製代碼

實現代碼:

public static promisify<U = any>(
    fn: (...args: any[]) => void,
    context = null,
  ) {
    return (...args: any[]) => {
      return new PQ<U>((resolve, reject) => {
        return fn.apply(context, [
          ...args,
          (err: any, result: U) => {
            if (err) {
              return reject(err);
            }

            return resolve(result);
          },
        ]);
      });
    };
  }
複製代碼

咱們將全部傳遞的參數都綁定到函數上,而且-最後一個-是咱們給出的錯誤優先的回調函數。

Promise.promisifyAll

官方文檔的運行方式

測試代碼:

describe('PQ.promisifyAll', () => {
  test('promisifies a object', () => {
    const person = {
      name: 'Maciej Cieslar',
      getName(callback) {
        return callback(null, this.name);
      },
    };

    const promisifiedPerson = PQ.promisifyAll<{
      getNameAsync: () => PQ<string>;
    }>(person);

    return promisifiedPerson.getNameAsync().then((name) => {
      expect(name).toBe('Maciej Cieslar');
    });
  });
});
複製代碼

實現代碼:

public static promisifyAll<U>(obj: any): U {
    return Object.keys(obj).reduce((result, key) => {
      let prop = obj[key];

      if (isFunction(prop)) {
        prop = PQ.promisify(prop, obj);
      }

      result[`${key}Async`] = prop;

      return result;
    }, {}) as U;
  }
}
複製代碼

咱們將會迭代傳入的對象的鍵,而後將其方法promise化,而且在每一個函數名字前添加關鍵字async.

打包

咱們到此爲止只是實現了全部BlueBird Api方法中的一小部分,因此我強烈的建議您去探索,去嘗試調用,而後嘗試實現全部剩餘的部分。

雖然可能萬事開頭難,可是彆氣餒,畢竟容易的化就沒什麼意義了。

很是感謝您的閱讀。我但願你會以爲這篇文章頗有價值,但願它可以幫你完全搞懂promise的概念。從如今起你會以爲使用promise或者用他編寫異步代碼是這樣的舒爽這樣的真香。

若是你有任何的問題,盡請在下面的留言板留言或者私戳我。

喜歡個人話,就關注個人blog

或者訂閱我

譯者結語

若是你對個人翻譯或者內容有什麼意見或者建議歡迎在下面留言告訴我,喜歡文章就給個贊吧,很是感謝您的閱讀,Hava a nice day:)

相關文章
相關標籤/搜索