When I read the book Exploring ES6, I suddenly wanted to have A deeper understanding of the design concept and design principle of Promise, so I read some articles and introduction about how to implement A Promise instance that conforms to the Promise/A+ specification, searched some relevant knowledge. I also read other people’s Promise implementation source code.
I was heartened by the results, and my understanding of promises improved considerably by implementing A Promise class that conforms to the Promise/A+ specification 😂.
It all comes down to learning ES6 Promise first, then reading the Promise/A+ specification and Promise implementation and knowledge sharing by third-party developers, and then building our own implementation logic step by step based on the Promise/A+ specification.
Then cut the crap and get started.
From specification to implementation
Before we start writing the code, let’s read the official documentation for Promises/A+.
Several terms
In short, there are five terms mentioned in the official document, as follows:
- Promise
- thenable
- value
- exception
- reason
A Promise is an object or function that has then methods and follows the Promise/A+ specification.
Thenable means that an object or function has a THEN method
Value is a valid Javascript value.
Exception is a value thrown using a throw statement.
Reason is the reason why the Promise status changes to Rejected.
Specification briefly
Reading the specification helps us write code, organize our thinking, and finally write A Promise implementation that passes all of the Promise/A+ test cases.
Promise State
-
2.1.1 There are only three states of a Promise:
-
Pending Indicates the initialization status
- 2.1.1.1 Can explicitly convert states to
fulfilled
orrejected
- 2.1.1.1 Can explicitly convert states to
-
Fulfilled successfully
- 2.1.2.1 State Cannot be reconverted
- 2.1.2.2 Having an immutable
value
-
The rejected failure
- 2.1.3.1 State Cannot be reconverted
- 2.1.3.2 Have an immutable
reason
Immutable means can be compared using === and always true, not completely immutable deep properties.
In addition, when instantiating with new, we need to provide an executor function argument to the constructor.
Thinking 🤔
Now let’s start with the simplest state requirement, assuming that we are in a confined space with only the keyboard at our fingertips.
Consider implementing the Promise State above, organizing the content of the code to be written with a few words, such as:
- My Promise implementation is called
Yo
Yo
Set the initial value and the initial state during initialization. The state can be changed tofulfilled
orrejected
.Yi
There are two static methods to explicitly convert its state:fulfill
andReject
, when the state ispending
“, so that the method can be executed later once the state changes.
Note the corresponding specification information entry
Soon, our implementation looks like this:
// Save some commonly used variables,
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const nop = () = > {}
const $undefined = undefined
const $function = "function"
// Use Symbol to protect Promise properties
const promiseState = Symbol("promiseState")
const promiseValue = Symbol("promiseValue")
class Yo {
constructor(executor) {
// Executor checks ahead of time and throws exceptions without creating additional internal variable and attribute methods
if(executor === $undefined) {
throw new TypeError("You have to give a executor param.")}if(typeofexecutor ! == $function) {throw new TypeError("Executor must be a function.")}this[promiseState] = PENDING / / 2.1.1
this[promiseValue] = $undefined
try {
executor(this.$resolve.bind(this), this.$reject.bind(this))}catch (e) {
this.$reject.bind(this)(e)
}
}
$resolve(value) {
if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
this[promiseState] = FULFILLED / / 2.1.1.1
this[promiseValue] = value / / 2.1.2.2
}
$reject(reason) {
if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
this[promiseState] = REJECTED / / 2.1.1.1
this[promiseValue] = reason / / 2.1.3.2}}Copy the code
then
methods
The THEN method is A core part of the Promise/A+ specification.
A Promise must provide a then method to access its value or Reason, which takes two optional arguments:
promise.then(onFulfilled, onRejected)
Copy the code
“Reading specifications always requires a little patience”
Its specifications are as follows:
- 2.2.1
onFulfilled
andonRejected
It’s all optional- 2.2.1.1 if
onFulfilled
If it is not a function, this parameter is ignored - 2.2.1.2 if
onRejected
If it is not a function, this parameter is ignored
- 2.2.1.1 if
- 2.2.2 if
onFulfilled
It’s a function- 2.2.2.1 This function is in
promise
The status offulfilled
Is called asynchronously, and uses itvalue
Value as the first argument - 2.2.2.2 This function is not available
promise
The status offullfilled
Was called before - 2.2.2.3 in one
promise
Can only be called once on an instance
- 2.2.2.1 This function is in
- 2.2.3 if
onRejected
It’s a function- 2.2.3.1 This function is in
promise
The status ofrejected
Is called asynchronously, and uses itvalue
Value as the first argument - 2.2.3.2 This function is not available
promise
The status ofrejected
Was called before - 2.2.3.3 in one
promise
Can only be called once on an instance
- 2.2.3.1 This function is in
- 2.2.4
onFulfilled
andonRejected
Will be called asynchronously (cannot be called until the current stack is empty) - 2.2.5
onFulfilled
andonRejected
Must be called as a function (should not be used internallythis
Value, the reason lies in strict mode and non-strict modethis
Inconsistent values) - 2.2.6
then
Can be in the samepromise
Instance, so we can use one in different placespromise.then
Methods f- 2.2.6.1 when
promise
The status offulfilled
When, all of themthen
On the incomingonFulfilled
Functions are executed in the order in which they are called - 2.2.6.2 when
promise
The status ofrejected
When, all of themthen
On the incomingonRejected
Functions are executed in the order in which they are called
- 2.2.6.1 when
- 2.2.7
then
Method will eventually return a new onepromise
Example:promise2 = promise1.then(onFulfilled, onRejected)
- 2.2.7.1 if
onFulfilled
oronRejected
Return a valuex
, the implementation ofPromise
Parsing steps of:[[Resolve]](promise2, x)
- 2.2.7.2 if
onFulfilled
oronRejected
Throw an exceptione
,promise2
directlyreject(e)
- 2.2.7.3 if
onFulfilled
It’s not a function, andpromise1
The status offulfilled
,promise2
Continue to usepromise1
The state and value of. - 2.2.7.4 if
onFulfilled
It’s not a function, andpromise1
The status ofrejected
,promise2
Continue to usepromise1
The state andreason
- 2.2.7.1 if
Perfect ✍ ️
According to the definition of the specification, based on the above code, we will improve the THEN method.
class Yo {
constructor(executor){...this[promiseConsumers] = []
try {
executor(this.$_resolve.bind(this), this.$reject.bind(this))}catch (e) {
this.$reject.bind(this)(e)
}
}
$resolve(value) {
if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
this[promiseState] = FULFILLED / / 2.1.1.1
this[promiseValue] = value / / 2.1.2.2
this.broadcast()
}
$reject(reason) {
if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
this[promiseState] = REJECTED / / 2.1.1.1
this[promiseValue] = reason / / 2.1.3.2
this.broadcast()
}
static then(onFulfilled, onRejected) {
const promise = new Yo(nop) // The new instance returned by the then method
/ / 2.2.1.1
promise.onFulfilled = typeof onFulfilled === $function ? onFulfilled : $undefined;
/ / 2.2.1.2
promise.onRejected = typeof onRejected === $function ? onRejected : $undefined;
// 2.2.6.1, 2.2.6.2
this[promiseConsumers].push(promise)
this.broadcast()
/ / 2.2.7
return promise
}
static broadcast() {
const promise = this;
2.2.2.1,.2.2.2.2, 2.2.3.1, 2.2.3.2
if(this[promiseState] === PENDING) return
2.2.6.1, 2.2.6.2, 2.2.2.3, 2.2.3.3
const callbackName = promise[promiseState] === FULFILLED ? "onFulfilled" : "onRejected"
const resolver = promise[promiseState] === FULFILLED ? "$resolve" : "$reject"
soon(
function() {
2.2.6.1, 2.2.6.2, 2.2.2.3, 2.2.3.3
const consumers = promise[promiseConsumers].splice(0)
for (let index = 0; index < consumers.length; index++) {
const consumer = consumers[index];
try {
const callback = consumer[callbackName] // Gets the function passed in when the then method executes
const value = promise[promiseValue]
// 2.2.1.1, 2.2.1.2, 2.2.5 without context
if(callback) {
consumer['$resolve'](callback(value))
} else {
// onpity/onRejected is not a function
// 2.2.7.3, 2.2.7.4
consumer[resolver](value)
}
} catch (e) {
// Exception is set to Rejected
consumer['$reject'](e)
}
}
}
)
}
}
// soon function come from Zousan.js
const soon = (() = > {
const fq = [], // function queue
// avoid using shift() by maintaining a start pointer
// and remove items in chunks of 1024 (bufferSize)
bufferSize = 1024
let fqStart = 0
function callQueue() {
while(fq.length - fqStart) {
try {
fq[fqStart]()
} catch (err) {
console.log(err)
}
fq[fqStart++] = undefined // increase start pointer and dereference function just called
if(fqStart === bufferSize) {
fq.splice(0, bufferSize)
fqStart = 0}}}// run the callQueue function asyncrhonously as fast as possible
// Execute this function, and the returned function is assigned to cqYield
const cqYield = (() = > {
// Return a function and execute
// This is the fastest way browsers have to yield processing
if(typeofMutationObserver ! = ='undefined')
{
// first, create a div not attached to DOM to "observe"
const dd = document.createElement("div")
const mo = new MutationObserver(callQueue)
mo.observe(dd, { attributes: true })
return function() { dd.setAttribute("a".0)}// trigger callback to
}
// if No MutationObserver - this is the next best thing for Node
if(typeofprocess ! = ='undefined' && typeof process.nextTick === "function")
return function() { process.nextTick(callQueue) }
// if No MutationObserver - this is the next best thing for MSIE
if(typeofsetImmediate ! == _undefinedString)return function() { setImmediate(callQueue) }
// final fallback - shouldn't be used for much except very old browsers
return function() { setTimeout(callQueue,0)}}) ()// this is the function that will be assigned to soon
// it take the function to call and examines all arguments
return fn= > {
fq.push(fn) // push the function and any remaining arguments along with context
if((fq.length - fqStart) === 1) { // upon addubg our first entry, keck off the callback
cqYield()
}
}
})()
Copy the code
There are different opinions on the Internet about the logical implementation of onFulfilled or onRejected after the state transition. My favorite implementation comes from @Trincot’s answer on Stack Overflow. If you are interested, you can check the reference link at the end of this article.
The solution to a previously registered callback function that is called asynchronously after a state change is as follows:
- use
consumers
An array to storethen
Method returnpromise
- in
then
Method for each to be returnedpromise
Adds the parameter with the same name that it passesonFulfilled
andonRejected
As aPromise
Property of. - For some that have already switched states
Promise
Instance, needs to be inthen
Methodbroadcast
Methods.
The broadcast method is critical and is called once in the resolve, Reject, and then methods.
We use the broadcast method to make a “broadcast” function. When the Promise state is transformed, we will create a micro-task according to its state, and asynchronously call the onFulfilled or onRejected attribute methods on all promises in the consumers array.
In addition, how to create microtasks to asynchronously execute related functions is also the key to realize the Promise class. Here, I learned the Promise implementation scheme of the predecessors of @blueJava: Zousan.js, with its Github warehouse address at the end of the article.
In Zousan.js, the author specifically creates a soon function that creates a microtask to execute as quickly as possible from the function parameters passed in.
The core of this is to create document nodes that use the API to create microtasks and eventually perform the target function if the browser environment supports MutationObserver. If not, check process.nextTick and setImmediate. Finally, setTimeout is used to create a macro task to call the target function asynchronously.
At this point, our Yo class is almost complete, and finally The third specification: The Promise Resolution Procedure.
The Promise Resolution Procedure
The Promise Resolution Procedure is expressed as [[Resolve]](Promise, x), why do we need to implement this specification?
When we use the resolve or reject methods in executor functions, the arguments passed in can be any valid Javascript value. In some cases, this value can be a primitive type of data, a Thenables object, or a Promise instance created by another Promise implementation.
We need to deal with this problem so that the different parameters have a consistent and exact solution.
So, let’s move on to how the specification is defined.
The steps to execute [[Resolve]](promise, x) are as follows:
- 2.3.1 if
promise
andx
Reference the same object, thenreject
aTypeError
As abnormalreason
- 2.3.2 if
x
Is aPromise
, then its state is adopted- 2.3.2.1 if
x
ispending
The,promise
keeppending
untilx
State changes - 2.3.2.2 、2.3.2.3
x
When the condition is stable, use it directlyvalue
orreason
- 2.3.2.1 if
- 2.3.3 If it is not
Promise
But an ordinary personthenable
object- 2.3.3.1 set
then
Is equal to thex.then
- 2.3.3.2 If it is obtained
x.then
Value throws an exception, thenreject
thispromise
And treat the exception asreason
- 2.3.3.3 if
then
Is a function, then willx
Bound to this functionthis
Object that can be passed in order to change the currentpromise
Method of stateresolve
andreject
- 2.3.3.3.1 if
resolve
Execute and pass in oney
Value, the command is executed[[Resolve]](promise, y)
- 2.3.3.3.2 if
reject
Execute and pass in onereason
, this is adoptedreason
As arejected
The state of thereason
- 2.3.3.3.3 if
resolve
andreject
The first call takes precedence, and only the first call is executed. Subsequent calls are ignored - 2.3.3.3.4 If called
then
An exception was thrown when- 2.3.3.3.4.1 if
resolve
orreject
If yes, this exception is ignored - 2.3.3.3.4.2 otherwise,
reject
This exception serves as itsreason
- 2.3.3.3.4.1 if
- 2.3.3.3.1 if
- 2.3.3.4 if
then
Is not a function, then thispromise
In order tox
forvalue
, the state changes tofulfilled
- 2.3.3.1 set
- 2.3.4 if
x
Is not an object or functionpromise
In order tox
forvalue
, the state changes tofulfilled
For Promise, it is the function that transforms the state that needs to consider how the above specification is implemented.
Let’s continue to work on the unfinished code.
Because I need to handle a complex resolve function, rather than just changing the state and setting value or reason after settling, I chose to name this method $_resolve to distinguish it from the simple $resolve method.
class Yo {... $_resolve(x) {let hasCalled,then;
/ / 2.3.1
if(this === x) {
console.log('circular');
throw new TypeError("Circular reference error, value is promise itself.")}/ / 2.3.2
if(x instanceof Yo) {
console.log('instance');
/ / 2.3.2.1, 2.3.2.2, 2.3.2.3
x.then(this.$_resolve.bind(this), this.$reject.bind(this))}else if(x === Object(x)) {
/ / 2.3.3
try {
/ / 2.3.3.1
then = x.then;
if(typeof then === $function) {
/ / 2.3.3.3
then.call(
x,
// first argument resolvePromise
function(y) {
if(hasCalled) return
hasCalled = true
/ / 2.3.3.3.1
this.$_resolve(y)
}.bind(this),
// second argument is rejectPromise
function (reasonY) {
if(hasCalled) return
hasCalled = true
/ / 2.3.3.3.2
this.$reject(reasonY)
}.bind(this))}else {
// 2.3.3.4 Original value
this.$resolve(x)
}
} catch (e) {
// 2.3.3.2, 2.3.3.3.4 Abnormal
if(hasCalled) return / / 2.3.3.3.4.1
this.$reject(e) / / 2.3.3.3.4.2}}else {
// 2.3.4 Original value
this.$resolve(x)
}
}
...
}
Copy the code
At this point
For a Promise implementation, we also need to add a catch method, which can be seen as syntactic sugar for the then method.
Of course, static methods resolve and reject can simply be added.
class Yo{...catch(onRejected) {
return this.then($undefined, onRejected)
}
static reject(reason) {
return new Yo((_, reject) = > {
reject(reason)
})
}
static resolve(value) {
return new Yo(resolve= > {
resolve(value)
})
}
...
}
Copy the code
Finally, we tested our implementation using Promises – aplus-Tests.
After installing the dependencies, add the deferred static method to Yo as follows:
class Yo {...static deferred() {
const result = {}
result.promise = new Yo((resolve, reject) = > {
result.resolve = resolve
result.reject = reject
})
return result
}
...
}
Copy the code
Then add the test command to the scripts field of package.json, and use YARN Run test to test as follows:
Since then, we’ve implemented promises that comply with the Promise/A+ specification. While Yo may not be robust enough, even some of the usual methods aren’t provided, as A simple implementation for learning about Promise, Yo does its job well. All the code is shown below (you can also access the GitHub repository source youyiqin/ Yo source code by referring to the last item below) :
Finish scattering flowers.
Write in the last
Learning about ES6 Promises, and reading some third-party Promise implementation examples from developers online, is very useful for understanding and using promises for asynchronous programming. Personally implementing A Promise implementation that can be tested through Promise/A+ test cases has enhanced the author’s ability to use Promise to some extent.
Welcome to discuss and learn ~
reference
- Basic Javascript promise implementation attempt – Stack Overflow
- bluejava/zousan: A Lightning Fast, Yet Very Small Promise A+ Compliant Implementation
- Youyiqin/yukio okamoto, the source code