Promise implementation is not as simple or as difficult as it might seem, and it takes less than 200 lines of code to implement an alternative to the original Promise.
Promises have been an integral API on the front end and are now ubiquitous. Are you sure you know Promise well enough? If not, you should know how Promise works. If you think you know it, have you ever fulfilled your Promise?
In any case, understanding how promises are implemented can help improve our front-end skills.
The following code is implemented using ES6 syntax, is not COMPATIBLE with ES5, and runs fine on the latest Google Browser.
If you want to see the results first, check out the full version at the end of this article, or check out Github, which includes unit tests.
Part of the term
-
executor
new Promise( function(resolve, reject) {... } /* executor */ );Copy the code
Executor is a function that takes resolve and reject.
-
onFulfilled
p.then(onFulfilled, onRejected);Copy the code
This parameter is called as a then callback function when a Promise becomes fulfillment.
-
OnRejected This parameter is called as a callback when a Promise turns into a rejection.
p.then(onFulfilled, onRejected); p.catch(onRejected);Copy the code
Promise implementation Principle
Better to look at Promises/A+ specifications first, here is A personal summary of the fundamentals of code implementation.
Three states of Promise
- pending
- fulfilled
- rejected
The Promise object’s pending may become a pity and Rejected, but it cannot be reversed.
The then and catch callback methods can only be executed in a non-pending state.
Promise lifecycle
In order to better understand, I summarized the life cycle of Promise. The life cycle is divided into two situations and the life cycle is irreversible.
pending -> fulfilld
pending -> rejected
Executor, then, Catch, and finally executions each have a new life cycle.
Principle of the chain
How do you keep chain calls to.then,.catch, and.finally?
Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ Each THEN, catch, and finally has its own Promise life cycle.
This will be a pity. Prototype. Then = function(ondepressing,onReject) {return Promise(() => {// this will be a pity.Copy the code
However, we need to consider the case that the link is broken midway. If the link continues after the break, the state of the Promise may be non-pending.
This point is not so easy to understand when first exposed to.
The default is to start with new Promise(…) .then(…) In chain calls, then, catch, and other callback functions are all in pending state, and the callback functions are added to the asynchronous queue waiting for execution. The pending state may become a pity or Rejected state, which needs to be executed immediately instead of joining the asynchronous queue waiting for execution.
Examples of continuous chains are as follows:
new Promise(...) .then(...)Copy the code
Examples of broken links are as follows:
const a = new Promise(...) // This may be a pity or the rejected state (...). // The pending state may become a pity or the rejected state.Copy the code
So you have to think about both cases.
Asynchronous lined up
This requires an understanding of macro tasks and microtasks, but not all browser JavaScript apis provide such methods.
So I’m going to use setTimeout instead.
While non-asynchronous resolve or Reject can be implemented without asynchronous queuing, the native Promise all then callbacks are executed in asynchronous queuing.
Note here that asynchronous queuing is not used in the first part of the step-by-step examples, but is added to resolve or reject asynchronously.
The first step is to define the structure
To make it different from the original Promise, we prefix it to NPromise.
Define state
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejectedCopy the code
Define the Promise instance method
class NPromise {
constructor(executor) {}
then(onFulfilled, onRejected) {}
catch(onRejected) {}
finally(onFinally) {}
}Copy the code
Define extension methods
NPromise.resolve = function(value){}
NPromise.reject = function(resaon){}
NPromise.all = function(values){}
NPromise.race = function(values){}Copy the code
A simple Promise
The first simple Promsie does not take into account cases such as asynchronous resolve, which is only used like, and the callback to then is not asynchronous.
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' /** * @param {Function} executor * Executor is a function that takes resolve and reject * the Promise constructor calls the executor function immediately upon execution */ class NPromise {constructor(executor) {// There are some attribute variables that you don't want to define, but just to do that, This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This. _onFulfilled)} /** * @param {Any} value = (value) => {if (this._status === == this _error = undefined}} /** * The operation fails * @param {Any} reason Value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { if (this._status === FULFILLED) { if (onFulfilled) { const _value = onFulfilled ? onFulfilled(this._nextValue) : This._nextvalue // If ondepressing is defined, run ondepressing and return result // otherwise skip, Resolve (_value)}} if (this._status === REJECTED) {if (onRejected) {this. Resolve (onRejected(this._error))} else {// This is a pity. The Promise will return the rejected state, reject(this._error)}})} catch(onRejected) {// This is a pity This.then (null, onRejected)} finally(onRejected) {this.then(null, onRejected)} finally(onRejected) {Copy the code
Test example (setTimeout only to provide a separate execution environment)
setTimeout(() => { new NPromise((resolve) => { console.log('resolved:') resolve(1) }) .then((value) => { console.log(value) return 2 }) .then((value) => { console.log(value) }) }) setTimeout(() => { new NPromise((_, Reject) => {console.log('reject :') reject('err')}). Then ((value) => {console.log(value) return 2}) . The catch ((err) = > {the console. The log (err)})}) / / / / output resolved: / / 1 / / 2 / / rejected: / / errCopy the code
Consider catching errors
Resolve (resolve); then (resolve);
Change points relative to previous step:
- Executor execution requires a try catch
Then, catch, and finally are all executed by executor, so you only need to try catch executor.
- No promise. Catch is defined and an error message is printed
It’s not a throw, it’s a console.error, which is also native, so you can’t catch a Promise error by trying it.
- If the callback function for then is not a function type, skip it
This is outside the scope of error capture processing, which is mentioned here by the way.
Code implementation
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return Typeof fn === 'function'} /** * @param {function} executor * executor is a function with resolve and reject arguments * Promise constructor is called immediately upon execution Executor function */ class NPromise {constructor(executor) {constructor(executor) { This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} /** * this._error)}} @param {Any} value this = (value) => {this PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined this._throwErrorIfNotCatch() } } /** * Failed * @param {Any} Reason Failed value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined this._throwErrorIfNotCatch() } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = (reason) => { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { return handleResolve(this._nextValue) } if (this._status === REJECTED) {return handleReject(reason)}} handle(this._error) // Error has been passed to the next NPromise and needs to be reset. // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined})} Catch (onRejected) {// This. Then (null, someday) Onrejecte)} finally(onRejecte) {}Copy the code
Test example (setTimeout only to provide a separate execution environment)
SetTimeout (() => {new NPromise((resolve) => {console.log('executor error :') const a = 2 a = 3 resolve(1)}). Catch ((value) => { console.log(value) return 2 }) }) setTimeout(() => { new NPromise((resolve) => { resolve() }) .then(() => { const B = 3 b = 4 return 2}). Catch ((err) => {console.log(' console.log ') ') console.log(err)})}) setTimeout(() => {new NPromise((resolve) => {console.log(' print error message directly, red: ') resolve()}). Then (() => {throw Error('test') return 2})}) Assignment to constant variable. // at <anonymous>:97:7 // at new NPromise (<anonymous>:21:7) // at <anonymous>:94:3 // TypeError: Assignment to constant variable. // at <anonymous>:111:9 // at <anonymous>:59:17 // at new Promise (<anonymous>) // at Npromise. then (<anonymous>:54:12) // at <anonymous>:109:6 test // at <anonymous>:148:11 // at handleResolve (<anonymous>:76:52) // at handle (<anonymous>:89:18) // at <anonymous>:96:7 // at new NPromise (<anonymous>:25:7) // at NPromise.then (<anonymous>:73:12) // at <anonymous>:147:6Copy the code
The value considered resolved is of type Promise
Resolve (resolve); then (resolve);
Change points relative to previous step:
- New isPromise method
- The then method handles the case where this._nextValue is a Promise
Code implementation
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject // Class NPromise {constructor(executor) {// Some attribute variables may not be defined. This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} /** * this._error)}} @param {Any} value this = (value) => {this PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined this._throwErrorIfNotCatch() } } /** * Failed * @param {Any} Reason Failed value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined this._throwErrorIfNotCatch() } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = (reason) => { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} handle(this._error) // Error was passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined})} Catch (onRejected) {// This. Then (null, someday) Onrejecte)} finally(onRejecte) {}Copy the code
The test code
setTimeout(() => { new NPromise((resolve) => { resolve( new NPromise((_resolve) => { _resolve(1) }) ) }).then((value) => { console.log(value) }) }) setTimeout(() => { new NPromise((resolve) => { resolve( new NPromise((_, _reject) => { _reject('err') }) ) }).catch((err) => { console.log(err) }) })Copy the code
Consider the asynchronous case
Async Resolve or reject, which are more complex, need to be placed into a queue and wait for the pending state to change.
Native JavaScript comes with asynchronous queuing, and we can take advantage of that by using setTimeout instead, So this Promise has the same level of priority as setTimeout (natively, the Promise takes precedence over setTimeout).
Change points relative to previous step:
- This. _ondepressing is fulfilled with asynchronous processing
- This. _onRejected adds asynchronous processing
- Add this._callbackQueue with an empty array as the initial value
- Added the this.__runCallbackQueue method to run an asynchronous queue
During synchronization, the Promise state immediately switches to a non-pending state and the asynchronous queue becomes null. Before asynchronously changing the Promise state, a value is pending, and then, catch, and finally callbacks are queued for execution.
Whether asynchronous or synchronous, this.__runCallbackQueue is executed only once in the current Promise life cycle.
- The then method needs to discriminate according to the Promise state
If the state is not pending, execute the callback function immediately (skip if there is no callback function).
If the state is pending, join an asynchronous queue and wait for the promises to be non-pending before they are executed.
- The callback to the then method adds try catch error handling
- Finally methods are implemented
Code implementation
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject } / class NPromise {constructor(executor) {constructor(executor) {if (! IsFunction (executor)) {throw new TypeError('Expected the executor to be a function.')} try {// Initialize status to PENDING This. _status = PENDING // fullfilled This._error = undefined // then, catch, finally asynchronous callback queue, This._callbacQueue = [] executor(this. _ondepressing, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} _runCallbackQueue = () => {if (this._callbacqueue.length > 0) {// resolve or reject ForEach (fn => {fn()}) this._callbacQueue = []} This._throwErrorIfnotCatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} if (this._status === PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined This._runcallbackqueue ()}} @param {Any} reason = / _onRejected = reason => {setTimeout(() => {setTimeout() => { if (this._status === PENDING) { this._status = REJECTED this._error = reason this._nextValue = undefined this._runCallbackQueue() } }) } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = reason => { try { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} Catch (err) {reject(err)}} if (this._status === PENDING) { // New NPromise(...); .then(...) Var a = NPromise(...) ; a.then(...) This._callbacqueue.push (() => {// Use setTimeout instead of // to ensure that the state is not pending before execution of subsequent THEN, catch, or Finally callback handle(this._error) // Error has been passed to the next NPromise and needs to be reset otherwise it will throw multiple identical errors // With this._throwErrorIfnotCatch, If there is no catch this._error = undefined})} else {// if there is no catch this._error = undefined})} else {// Var a = NPromise(...); ; // after a few seconds a.chen (...) // Handle (this._error) // Error has been passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined}})} Catch (onRejected) {return this. Then (null, onRejected) } finally(onFinally) { return this.then( () => { onFinally() return this._nextValue }, () => {onFinally() // The error needs to be thrown, the next Promise will catch throw this._error})}}Copy the code
The test code
new NPromise((resolve) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
.then((value) => {
console.log(value)
return new NPromise((resolve) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
})
.then((value) => {
console.log(value)
})Copy the code
Expanding method
The relatively difficult extension method should be Promise. All, the rest are quite simple.
NPromise.resolve = function(value) {
return new NPromise(resolve => {
resolve(value)
})
}
NPromise.reject = function(reason) {
return new NPromise((_, reject) => {
reject(reason)
})
}
NPromise.all = function(values) {
return new NPromise((resolve, reject) => {
let ret = {}
let isError = false
values.forEach((p, index) => {
if (isError) {
return
}
NPromise.resolve(p)
.then(value => {
ret[index] = value
const result = Object.values(ret)
if (values.length === result.length) {
resolve(result)
}
})
.catch(err => {
isError = true
reject(err)
})
})
})
}
NPromise.race = function(values) {
return new NPromise(function(resolve, reject) {
values.forEach(function(value) {
NPromise.resolve(value).then(resolve, reject)
})
})
}Copy the code
The final version
You can also look at Github, which has unit tests.
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject } / class NPromise {constructor(executor) {constructor(executor) {if (! IsFunction (executor)) {throw new TypeError('Expected the executor to be a function.')} try {// Initialize status to PENDING This. _status = PENDING // fullfilled This._error = undefined // then, catch, finally asynchronous callback queue, This._callbacQueue = [] executor(this. _ondepressing, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} _runCallbackQueue = () => {if (this._callbacqueue.length > 0) {// resolve or reject ForEach (fn => {fn()}) this._callbacQueue = []} This._throwErrorIfnotCatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} if (this._status === PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined This._runcallbackqueue ()}} @param {Any} reason = / _onRejected = reason => {setTimeout(() => {setTimeout() => { if (this._status === PENDING) { this._status = REJECTED this._error = reason this._nextValue = undefined this._runCallbackQueue() } }) } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = reason => { try { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} Catch (err) {reject(err)}} if (this._status === PENDING) { // New NPromise(...); .then(...) Var a = NPromise(...) ; a.then(...) This._callbacqueue.push (() => {// Use setTimeout instead of // to ensure that the state is not pending before execution of subsequent THEN, catch, or Finally callback handle(this._error) // Error has been passed to the next NPromise and needs to be reset otherwise it will throw multiple identical errors // With this._throwErrorIfnotCatch, If there is no catch this._error = undefined})} else {// if there is no catch this._error = undefined})} else {// Var a = NPromise(...); ; // after a few seconds a.chen (...) // Handle (this._error) // Error has been passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined}})} Catch (onRejected) {return this. Then (null, onRejected) } finally(onFinally) { return this.then( () => { onFinally() return this._nextValue }, () => {onFinally() // Error needs to be thrown, Throw this._error})}} npromise. resolve = function(value) {return new NPromise(resolve => { resolve(value) }) } NPromise.reject = function(reason) { return new NPromise((_, reject) => { reject(reason) }) } NPromise.all = function(values) { return new NPromise((resolve, reject) => { let ret = {} let isError = false values.forEach((p, index) => { if (isError) { return } NPromise.resolve(p) .then(value => { ret[index] = value const result = Object.values(ret) if (values.length === result.length) { resolve(result) } }) .catch(err => { isError = true reject(err) }) }) }) } NPromise.race = function(values) { return new NPromise(function(resolve, reject) { values.forEach(function(value) { NPromise.resolve(value).then(resolve, reject) }) }) }Copy the code
conclusion
After some testing, except for the following two points:
- Use setTimeout macro task queuing instead of microtasks
- The extension methods promise. all and promise. race only consider arrays, not iterators.
Promise alone is almost 100% the same in use and effect as native.
If you don’t believe me, check out the unit tests on Github while you try the following code:
Notice NPromise and Promise.
new NPromise((resolve) => {
resolve(Promise.resolve(2))
}).then((value) => {
console.log(value)
})Copy the code
or
new Promise((resolve) => {
resolve(NPromise.resolve(2))
}).then((value) => {
console.log(value)
})Copy the code
All of the above results are returned to normal.