Há um tempo atrás publiquei um artigo sobre como podemos entender Promises de uma vez por todas, se você ainda não leu, recomendo a leitura para que possamos continuar, mas vou dar um pequeno resumo do que conversamos nele.
Promises são estruturas que lidam com valores que podem ser obtidos futuramente em uma execução, por exemplo, uma requisição para um servidor externo, ou então uma leitura de um arquivo. O que poucos sabem é que, de fato, a Promise é um padrão de projeto que pode ser implementado utilizando orientação a objetos e a especificação descrita no PromisesA+.
Como a melhor forma de aprender é fazendo, vamos mergulhar na especificação das Promises e vamos implementar nossa própria Promise do zero!
Especificação
A especificação de Promises como conhecemos no JavaScript está no Promises/A+ (uma evolução do Promises/A), pois, antes da implementação nativa, algumas bibliotecas como o Q e o Bluebird já implementavam este padrão. Então resolveu-se criar uma especificação aberta onde as pessoas que implementavam este modelo pudessem escrever e debater estes assuntos com outros programadores. Esta especificação define, basicamente, como o método then
deve funcionar, de forma que todas as Promises que conformem com ela devem funcionar da mesma maneira em qualquer lugar.
Terminologia
Vamos dar nome a algumas coisas, primeiramente vamos definir todos os termos que vamos utilizar na nossa promise, isto vem de uma tradução direta da especificação:
- A
Promise
é um objeto com um métodothen
cujo comportamento conforma com esta especificação - Um
thenable
é um objeto ou função que define um métodothen
- Um valor é qualquer valor válido no JavaScript (incluindo
undefined
, umthenable
ou até outrapromise
) - Uma
exception
é uma exceção padrão de desenvolvimento que é levantada a partir de umthrow
- A razão é o motivo pelo qual uma
promise
foi rejeitada (quando sofre umaexception
)
Estado
A Promise é essencialmente uma máquina de estados. Ela pode estar em um de três possíveis estados:
-
Pendente: Neste estado ela pode ir para
fulfilled
ourejected
- Fulfilled (completa): Neste estado, a promise não pode transicionar para nenhum outro estado; Também deve possuir um valor que não deve ser alterável
- Rejected (rejeitada): Neste estado, a promise não pode transicionar para nenhum outro estado; Também deve possuir uma razão que não deve ser alterável
Then
Todas as Promises precisam especificar um método then
que vai ser o responsável por, de fato, avaliar a função e retornar o valor atual. Todo método then
deve ter a seguinte assinatura:
promise.then(onFulfilled, onRejected)
Onde, onFulfilled
é uma função com a seguinte assinatura:
(value: T) => void
E onRejected
tem a mesma assinatura, porém com uma razão ao invés de um valor.
Além disso, o then
precisa seguir uma série de regras para poder ser considerado conforme com a especificação. Não vou colocar todas elas aqui, mas vou incluir as mais importantes:
- Tanto
onFulfilled
quantoonRejected
são parâmetros opcionais para othen
e devem ser ignorados se não forem funções -
onFulfilled
, quando aceito, deve ser chamado sempre após a promise ter sido resolvida, com o valor da promise como primeiro argumento. Além disso ele só pode ser chamado uma vez. -
onRejected
, quando aceito, deve ser chamado sempre após a promise ter sido rejeitada, com a razão da promise como primeiro argumento. Além disso ele só pode ser chamado uma vez. -
then
pode ser encadeado múltiplas vezes na mesma promise. Quando a promise é completada ou rejeitada, todos os handlersthen
devem ser executados em ordem. -
then
deve retornar outra promise
Implementação
Para começarmos a implementar nossa promise, vamos primeiro criar um arquivo chamado PromiseTypes.ts
, vamos usar Typescript para poder fazer com que ela tenha alguns tipos que vão deixar muito mais simples o entendimento. Neste arquivo vamos colocar os tipos globais, que já sabemos que existem, como a função onFulfilled
e onRejected
, executores e tudo mais.
Vamos começar criando um enumerador com os estados possíveis de uma promise:
export enum PromiseStates {
PENDING,
FULFILLED,
REJECTED
}
Agora vamos criar os tipos de apoio e os tipos base da promise que vamos usar:
export type ResolveFunction = (value: any) => any
export type RejectFunction = (reason: any) => any
export type Thennable = { then: (value: any) => TypePromise }
export type ExecutorFunction = (resolve: ResolveFunction, reject: RejectFunction) => void
Estamos chamando de executor
a função que a Promise irá receber em seu construtor, ela que deverá conter o resolve
e o reject
. Da mesma forma, criamos um tipo para o thennable
. Vamos também criar um outro tipo auxiliar só para que possamos deixar nosso código mais bonito, chamado nullable<T>
, ele vai servir para podermos implementar elementos que podem ser nulos:
export type Nullable<T> = T | null
Máquina de estados
Vamos começar criando um arquivo chamado TypePromise.ts
, vamos chamar nossa classe de TypePromise
para não conflitar com a implementação nativa de Promises, por enquanto ela é uma simples máquina de estados, considerando todos os estados que temos que ter:
import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable } from './PromiseTypes'
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
}
Veja que temos um tipo novo, as HandlerFunctions
são objetos que vamos passar quando tivermos diversos then
ou catch
em nossa promise. Nestas situações temos que executar os handlers um por um. Cada um deles é um objeto com as duas funções do executor, vamos adicionar no nosso arquivo PromiseTypes.ts
e importar no arquivo principal, nosso arquivo PromiseTypes.ts
fica assim:
import { TypePromise } from './TypePromise'
export enum PromiseStates {
PENDING,
FULFILLED,
REJECTED
}
export type ResolveFunction = (value: any) => any
export type RejectFunction = (reason: any) => any
export type ExecutorFunction = (resolve: ResolveFunction, reject: RejectFunction) => void
export type Thennable = { then: (value: any) => TypePromise }
export type Nullable<T> = T | null
export type HandlerFunction = {
onFulfilled?: ResolveFunction;
onRejected?: Nullable<RejectFunction>
}
Agora vamos transicionar nossa promise para os dois valores conhecidos, FULFILLED
e REJECTED
:
import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
}
}
Veja que as transições nada mais são do que alteradores de estado. São eles que vão finalizar uma promise e setar seu valor final.
Vamos agora criar uma outra transição chamada de resolve
, esta transição será responsável por executar a promise em si e definir se ela foi resolvida ou rejeitada, bem como tratar se nossa promise receber outra promise.
import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
}
Nossa função resolve
é basicamente a responsável por saber se a função que recebemos é um objeto thennable
, se sim, então irá chamar uma nova função chamada doResolve
que leva um executor e faz um proxy da execução para os métodos internos da nossa própria promise, que serão chamados uma vez que a promise for resolvida, em poucas palavras, esta função é responsável por esperar que uma possível promise interna seja resolvida, uma vez que uma promise não pode ser resolvida com outra promise. Vamos implementar primeiramente o getThen
, que é o responsável por extrair ou ignorar uma função thennable
:
import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
}
É uma função bem simples, ela recebe um objeto thennable
e verifica se o tipo do valor passado é um objeto ou uma função, caso seja – e possua uma propriedade then
– então verificamos se esta propriedade é uma função para podermos retornar somente este handler.
Vamos para o método doResolve
, ele é o método principal da promise, porque é ele que vai iniciar toda a cadeia. Sendo responsável por garantir a única execução e também por criar wrappers ao redor das funções passadas pelo usuário para que elas possam ser controladas internamente.
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
}
Agora fizemos algumas coisas a mais. Primeiramente nossa função doResolve
basicamente checa se o nosso executor é confiável, ela faz isso criando wrappers ao redor das funções internas que temos para que elas somente sejam executadas uma única vez, isto é feito na nossa função getHandlerType
que, basicamente, retorna uma nova função com assinatura (value: any)
com uma checagem se ela já foi ou não executada alguma vez. Se sim, apenas retornamos, se não, vamos pegar a respectiva função de acordo com o tipo que queremos – ela pode ser uma função de resolução ou de rejeição, para fazer isso criamos no topo do arquivo o enum interno ReturnType
– e vamos executá-la retornando seu valor.
Por fim, vamos fazer uma pequena alteração em nossa resolução de promises, de forma que possamos criar uma cadeia de promises a serem resolvidas. Para isso, vamos criar um método chamado executeHandler
, que será responsável por receber e executar um objeto HandlerFunction
, e a função auxiliar executeAllHandlers
que irá iterar pelo nosso array de handlers e executar todos até que não haja mais nenhum:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
}
Com isso terminamos nossa máquina de estados.
Expondo métodos
Já que acabamos de construir nossa máquina de estados e suas transições, vamos agora criar os métodos que vão poder ser executados pelo usuário, como o then
, catch
e etc. Primeiramente vamos adicionar uma forma de resolver uma promise, criando seu construtor:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
constructor (executor: ExecutorFunction) {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
this.executeHandler = this.executeHandler.bind(this)
this.doResolve(executor, this.resolve, this.reject)
}
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
}
Veja que nosso construtor simplesmente seta os valores iniciais e chama a função doResolve
, dando inicio à resolução da promise.
Observando os valores
Para observarmos uma promise utilizamos then
, para casos de sucesso, ou catch
para casos de erro. Vamos criar nosso primeiro método público then
:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
constructor (executor: ExecutorFunction) {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
this.executeHandler = this.executeHandler.bind(this)
this.doResolve(executor, this.resolve, this.reject)
}
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
const handleResult = (type: ReturnType) => {
return (result: any) => {
try {
const executorFunction = type === ReturnType.ERROR ? reject : resolve
const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
} catch (error) {
reject(error)
}
}
}
return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
})
}
}
Nosso método then
implementa o que falamos anteriormente, ele recebe dois parâmetros opcionais e retorna uma instancia de uma nova promise. Novamente usamos a técnica do hashmap para podermos selecionar as funções que serão executadas. Caso as funções passadas pelo usuário sejam, de fato, funções, vamos executar estas funções primeiro e depois passar o resultado para o executor final que vai resolver a promise, se não, vamos apenas executar a função de resolução que pode ser reject
ou resolve
. Veja que temos um novo método, o done
.
A função done(onFulfilled, onRejected)
tem uma semântica mais simples de entender do que a do then
, embora uma use a outra para que seja possível finalizar a promise. A função done
segue as seguintes regras:
- Somente um dos dois parâmetros é chamado
- Só pode ser chamada uma vez
Esta função também só pode ser executada no final do tick
do event loop então temos que ter certeza que ela sempre será agendada para esta execução. Para isso vamos usar a API process.nextTick
que irá agendar a execução da função na fila de microtasks do Node (veja este guia para entender melhor) e será executado sempre no final do Event Loop:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
constructor (executor: ExecutorFunction) {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
this.executeHandler = this.executeHandler.bind(this)
this.doResolve(executor, this.resolve, this.reject)
}
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
const handleResult = (type: ReturnType) => {
return (result: any) => {
try {
const executorFunction = type === ReturnType.ERROR ? reject : resolve
const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
} catch (error) {
reject(error)
}
}
}
return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
})
}
private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
process.nextTick(() => {
this.executeHandler({
onFulfilled,
onRejected
})
})
}
}
Este método nada mais faz do que executar o nosso handler then
no final do tick.
Catch
Para finalizar, vamos implementar o método catch
para que o usuário possa capturar os erros da promise. Ele é bastante simples e também tira proveito da função done
, diferentemente do then
, o catch
sempre terá um argumento cujo tipo é uma função de rejeição:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private value: any = null
private thenHandlers: HandlerFunction[] = []
constructor (executor: ExecutorFunction) {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
this.executeHandler = this.executeHandler.bind(this)
this.doResolve(executor, this.resolve, this.reject)
}
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
const handleResult = (type: ReturnType) => {
return (result: any) => {
try {
const executorFunction = type === ReturnType.ERROR ? reject : resolve
const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
} catch (error) {
reject(error)
}
}
}
return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
})
}
private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
process.nextTick(() => {
this.executeHandler({
onFulfilled,
onRejected
})
})
}
catch (onRejected: RejectFunction) {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
return this.done(resolve, (error: any) => {
if(typeof onRejected === 'function') {
try {
return resolve(onRejected(error))
} catch (error) {
reject(error)
}
}
return reject(error)
})
})
}
}
Também retornamos uma nova promise e verificamos se a função passada não é uma outra promise que precisamos resolver antes.
Bonus: Finally
Há algum tempo a especificação dopromise.prototype.finally
entrou em vigor e já está funcionando no ES6. A função finally
tem um objetivo simples: Assim como o finally
que temos em um bloco try/catch, o finally
de uma promise é sempre executado no final de sua resolução, independente se a promise foi resolvida ou rejeitada, porém, diferentemente dos observadores como then
e catch
, o finally não retorna outra promise então não é possível encadear chamadas após o finally ser executado.
A implementação é relativamente simples, vamos criar uma propriedade em nossa promise chamada finalFunction
, que começará como uma função vazia. O objetivo é termos um método finally
que irá trocar o valor desta propriedade para a função que o usuário passar e então vai executá-la no final dos métodos fulfill
ou reject
:
import {
PromiseStates,
ResolveFunction,
RejectFunction,
ExecutorFunction,
Nullable,
Thennable,
HandlerFunction
} from './PromiseTypes'
enum ReturnType {
SUCCESS = 'success',
ERROR = 'error'
}
export class TypePromise {
private state: PromiseStates = PromiseStates.PENDING
private finalFunction: Function = () => { }
private value: any = null
private thenHandlers: HandlerFunction[] = []
constructor (executor: ExecutorFunction) {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
this.executeHandler = this.executeHandler.bind(this)
this.doResolve(executor, this.resolve, this.reject)
}
private fulfill (value: any) {
this.state = PromiseStates.FULFILLED
this.value = value
this.executeAllHandlers()
this.finalFunction() // Executamos o finally
}
private reject (reason: any) {
this.state = PromiseStates.REJECTED
this.value = reason
this.executeAllHandlers()
this.finalFunction() // Executamos o finally
}
private executeAllHandlers () {
this.thenHandlers.forEach(this.executeHandler)
this.thenHandlers = []
}
private executeHandler (handler: HandlerFunction) {
if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
}
private resolve (result: any) {
try {
const then = this.getThen(result)
if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
this.fulfill(result)
} catch (error) {
this.reject(error)
}
}
private getThen (value: Thennable) {
const valueType = typeof value
if (value && (valueType === 'object' || valueType === 'function')) {
const then = value.then
if (typeof then === 'function') return then
}
return null
}
private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
return (value: any) => {
if (done) return
done = true
return { error: onRejected, success: onFulfilled }[type](value)
}
}
private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
let done = false
try {
resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
} catch (error) {
if (done) return
done = true
onRejected(error)
}
}
then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
const handleResult = (type: ReturnType) => {
return (result: any) => {
try {
const executorFunction = type === ReturnType.ERROR ? reject : resolve
const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
} catch (error) {
reject(error)
}
}
}
return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
})
}
private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
process.nextTick(() => {
this.executeHandler({
onFulfilled,
onRejected
})
})
}
catch (onRejected: RejectFunction) {
return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
return this.done(resolve, (error: any) => {
if(typeof onRejected === 'function') {
try {
return resolve(onRejected(error))
} catch (error) {
reject(error)
}
}
return reject(error)
})
})
}
finally (finalFunction: Function) {
if (typeof finalFunction === 'function') this.finalFunction = finalFunction
}
}
Conclusão
Criamos nossa própria implementação de uma função em Typescript. Com isso temos a vantagem de ter tipagens em tempo de desenvolvimento, porém ainda precisamos checar os tipos durante a execução da função porque, afinal, todo o código é transpilado para JavaScript quando fizermos o build do mesmo. Podemos usar nossa promise desta forma:
import { TypePromise } from './TypePromise'
function foo (param: any) {
return new TypePromise((resolve, reject) => {
if (Math.random() > 0.5) return setTimeout(resolve, 1000, param)
return setTimeout(reject, 1000, 'error')
})
}
(() => {
foo(5)
.then((value) => console.log(value))
.catch((error) => console.error(error))
.finally(() => console.log('aways return'))
})()
Note também que estamos utilizando any
como tipo em vários casos, isto não é uma boa prática ao se utilizar Typescript porque deixamos a tipagem de lado. Então, como uma lição de casa, um desafio bacana seria implementar os tipos genéricos para que class TypePromise
passe a ser class TypePromise<T>
. Vamos abordar essa correção na sequencia deste artigo!
Se você estiver interessado em aprender mais, dê uma olhada nestas referências:
- https://www.promisejs.org/implementing/
- https://levelup.gitconnected.com/understand-javascript-promises-by-building-a-promise-from-scratch-84c0fd855720
- https://github.com/khaosdoctor/PromiseFromScratch
Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!