[翻譯] 使用JavaScript實現自己的Promises

Yukiiiiiiiiiii發表於2018-12-26

寫在正文前

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

implementing promises in JavaScript

在程式設計過程中我最愛的時刻就是當完全理解一個概念的時候內心產生 啊,原來是這樣 的那一刻。 即使這個過程可能很費時,很費力,但是當幸福真的到來的時候,會發現一切都是值得的。

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

抱著這個想法,我認為學習如何實現promises是我程式設計生涯中最重要的時刻之一,他給了我不一樣的方式瞭解非同步程式碼的工作原理,也讓我成為了一個更好的程式設計師。

我希望這個文章可以幫助你,讓你也可以逐漸用JavaScript實現自己的promise。

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

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

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

Promise 的核心內容

眾所周知,promise是一個擁有下面這些屬性的物件:

Then

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

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:)

相關文章