[翻译] 使用JavaScript实现自己的Promises

1,160 阅读14分钟

写在正文前

本文翻译自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:)