preface
Many beginners of JavaScript have experienced the fear of being dominated by callback hell, until they mastered promise syntax. Although promise has been built into many languages for a long time, it is jQuery 1.5 that really promotes it in JavaScript$.ajax
The refactoring supports promise, and its usage coincides with the chain call advocated by jQuery. After the birth of ES6, everyone began to enter the era of promise. Then es8 introduced async syntax to make the asynchronous writing of JavaScript more elegant.
Today, we’ll implement a promise step by step. If you haven’t used promise, it’s recommended that you be familiar with the promise syntax before reading this article.
Constructors
In the existingPromise/A+
standardIt doesn’t specify where the promise object comes from in jQuery$.Deferred()
Get the promise object. In ES6, get the promise object by instantiating the promise class. Here, we use es syntax to construct a class and return the promise object by instantiation. Because promise already exists, we temporarily name this class asDeferred
γ
class Deferred {
constructor(callback) {
const resolve = () => {
// TODO
}
const reject = () => {
// TODO
}
try {
callback(resolve, reject)
} catch (error) {
reject(error)
}
}
}
The constructor accepts a callback. When calling a callback, it needs to pass in two methods: resolve and reject.
Status of promise
Promise is divided into three states
- β³
pending
: waiting, this is the initial state of promise; - πββοΈ
fulfilled
: finished. Call resolve normally; - π
ββοΈ
rejected
: rejected, internal error, or state after calling reject;
We can see that promise has a state stored in the[[PromiseState]]
In the middle. Let’s add a state for deferred.
//Definition of basic variable
const STATUS = {
PENDING: 'PENDING',
FULFILLED: 'FULFILLED',
REJECTED: 'REJECTED'
}
class Deferred {
constructor(callback) {
this.status = STATUS.PENDING
const resolve = () => {
// TODO
}
const reject = () => {
// TODO
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
Another interesting thing here is that in the early implementation of browsers, the fully state was resolved, which was obviously inconsistent with promise specification. Now, of course, it’s fixed.
Internal results
Apart from the status, promise has an internal result[[PromiseResult]]
, which is used to hold the value accepted by resolve / reject.
Continue to add an internal result to the constructor.
class Deferred {
constructor(callback) {
this.value = undefined
this.status = STATUS.PENDING
const resolve = value => {
this.value = value
// TODO
}
const reject = reason => {
this.value = reason
// TODO
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
Save callback
When using promise, we usually call the promise object.then
Method, in the promise state tofulfilled
orrejected
At the same time, get the internal results, and then do the follow-up processing. So in the constructor, we need to construct two arrays to store the.then
Method.
class Deferred {
constructor(callback) {
this.value = undefined
this.status = STATUS.PENDING
this.rejectQueue = []
this.resolveQueue = []
const resolve = value => {
this.value = value
// TODO
}
const reject = reason => {
this.value = reason
// TODO
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
resolve
Andreject
modify state
Next, we need to implement the resolve and reject methods, which will change the state of the project object when they are called. After any method is called, other methods cannot be called.
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('πββοΈ')
}, 500)
setTimeout(() => {
reject('π
ββοΈ')
}, 800)
}).then(
() => {
console.log('fulfilled')
},
() => {
console.log('rejected')
}
)
At this point, the console will only print out thefulfilled
And it’s not going to happenrejected
γ
class Deferred {
constructor(callback) {
this.value = undefined
this.status = STATUS.PENDING
this.rejectQueue = []
this.resolveQueue = []
Let called // used to determine whether the state has been modified
const resolve = value => {
if (called) return
called = true
this.value = value
//Modification status
this.status = STATUS.FULFILLED
}
const reject = reason => {
if (called) return
called = true
this.value = reason
//Modification status
this.status = STATUS.REJECTED
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
Call callback
After modifying the status, the promise that gets the result will call the callback passed in by the then method.
class Deferred {
constructor(callback) {
this.value = undefined
this.status = STATUS.PENDING
this.rejectQueue = []
this.resolveQueue = []
Let called // used to determine whether the state has been modified
const resolve = value => {
if (called) return
called = true
this.value = value
//Modification status
this.status = STATUS.FULFILLED
//Call callback
for (const fn of this.resolveQueue) {
fn(this.value)
}
}
const reject = reason => {
if (called) return
called = true
this.value = reason
//Modification status
this.status = STATUS.REJECTED
//Call callback
for (const fn of this.rejectQueue) {
fn(this.value)
}
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
Students familiar with JavaScript event system should know that,promise.then
Method is placed in the micro task queue and called asynchronously.
Therefore, we need to put the callback call into the asynchronous queue. Here, we can put it into the setTimeout to delay the call. Although it doesn’t conform to the specification, it’s not easy.
class Deferred {
constructor(callback) {
this.value = undefined
this.status = STATUS.PENDING
this.rejectQueue = []
this.resolveQueue = []
Let called // used to determine whether the state has been modified
const resolve = value => {
if (called) return
called = true
//Asynchronous call
setTimeout(() => {
this.value = value
//Modification status
this.status = STATUS.FULFILLED
//Call callback
for (const fn of this.resolveQueue) {
fn(this.value)
}
})
}
const reject = reason => {
if (called) return
called = true
//Asynchronous call
setTimeout(() =>{
this.value = reason
//Modification status
this.status = STATUS.REJECTED
//Call callback
for (const fn of this.rejectQueue) {
fn(this.value)
}
})
}
try {
callback(resolve, reject)
} catch (error) {
//In case of exception, reject it directly
reject(error)
}
}
}
Then method
Next, we need to implement the then method. Those who have used promise must know that the then method can continue to be chained, so then must return a promise object. But inPromise/A+
In the specification, it is clearly stipulated that the then method returns a new promise object instead of directly returning this. We can verify this by the following code.
You can see thatp1
Objects andp2
Is two different objects, and the then method returnsp2
Object is also an instance of promise.
In addition, the then method also needs to determine the current state, if the current state is notpending
State, you can call the incoming callback directly without putting it into the queue to wait.
class Deferred {
then(onResolve, onReject) {
if (this.status === STATUS.PENDING) {
//Put the callback in the queue
const rejectQueue = this.rejectQueue
const resolveQueue = this.resolveQueue
return new Deferred((resolve, reject) => {
//Staging to successful callback pending call
resolveQueue.push(function (innerValue) {
try {
const value = onResolve(innerValue)
//Change the current promise state
resolve(value)
} catch (error) {
reject(error)
}
})
//Staging to failed callbacks waiting to be called
rejectQueue.push(function (innerValue) {
try {
const value = onReject(innerValue)
//Change the current promise state
resolve(value)
} catch (error) {
reject(error)
}
})
})
} else {
const innerValue = this.value
const isFulfilled = this.status === STATUS.FULFILLED
return new Deferred((resolve, reject) => {
try {
const value = isFulfilled
? onresolve (innervalue) // call onresolve successfully
: onreject (innervalue) // call onreject in failure state
Resolve (value) // returns the result to then
} catch (error) {
reject(error)
}
})
}
}
}
Now that our logic can basically run through, let’s try to run a piece of code:
new Deferred(resolve => {
setTimeout(() => {
resolve(1)
}, 3000)
}).then(val1 => {
console.log('val1', val1)
return val1 * 2
}).then(val2 => {
console.log('val2', val2)
return val2
})
After 3 seconds, the console displays the following results:
It can be seen that this is basically in line with our expectations.
Value penetration
If we call then without passing in any parameters, according to the specification, the current promise value can be passed through to the next then method. For example, the following code:
new Deferred(resolve => {
resolve(1)
})
.then()
.then()
.then(val => {
console.log(val)
})
You don’t see any output in the console, but you can see the correct result when you switch to promise.
To solve this problem, we only need to determine whether the parameter is a function when then is called. If not, we need to give a default value.
const isFunction = fn => typeof fn === 'function'
class Deferred {
then(onResolve, onReject) {
//Solution value penetration
onReject = isFunction(onReject) ? onReject : reason => { throw reason }
onResolve = isFunction(onResolve) ? onResolve : value => { return value }
if (this.status === STATUS.PENDING) {
// ...
} else {
// ...
}
}
}
Now we can get the right results.
One step away
Now we are only one step away from the perfect implementation of the then method, which is what we pass in by calling the then methodonResolve/onReject
Callback, you also need to determine their return value. If the callback returns a promise object, what should we do? Or if there is a circular reference, what should we do?
We’re getting it in frontonResolve/onReject
After the return value of theresolve
perhapsresolve
Now we need to process their return values.
then(onResolve, onReject) {
//The solution value penetration code has been omitted
if (this.status === STATUS.PENDING) {
//Put the callback in the queue
const rejectQueue = this.rejectQueue
const resolveQueue = this.resolveQueue
const promise = new Deferred((resolve, reject) => {
//Staging to successful callback pending call
resolveQueue.push(function (innerValue) {
try {
const value = onResolve(innerValue)
- resolve(value)
+ doThenFunc(promise, value, resolve, reject)
} catch (error) {
reject(error)
}
})
//Staging to failed callbacks waiting to be called
rejectQueue.push(function (innerValue) {
try {
const value = onReject(innerValue)
- resolve(value)
+ doThenFunc(promise, value, resolve, reject)
} catch (error) {
reject(error)
}
})
})
return promise
} else {
const innerValue = this.value
const isFulfilled = this.status === STATUS.FULFILLED
const promise = new Deferred((resolve, reject) => {
try {
const value = isFulfilled
? onresolve (innervalue) // call onresolve successfully
: onreject (innervalue) // call onreject in failure state
- resolve(value)
+ doThenFunc(promise, value, resolve, reject)
} catch (error) {
reject(error)
}
})
return promise
}
}
Return value judgment
When we use promise, we often return a new promise in the then method, and then pass the internal result of the new promise to the later then method.
fetch('server/login')
.then(user => {
//Returns a new promise object
return fetch(`server/order/${user.id}`)
})
.then(order => {
console.log(order)
})
function doThenFunc(promise, value, resolve, reject) {
//If value is a promise object
if (value instanceof Deferred) {
//Call the then method and wait for the result
value.then(
function (val) {
doThenFunc(promise, value, resolve, reject)
},
function (reason) {
reject(reason)
}
)
return
}
//If it is not a promise object, it returns directly
resolve(value)
}
Judge circular reference
If the return value of the callback function of the current then method is a new promise object generated by the current then method, it is considered to be a circular reference
The new promise object returned by the then methodp1
, as the return value in the callback, an exception will be thrown. Because according to the previous logic, the code will always be trapped in this logic.
Therefore, we need to prevent in advance and throw out errors in time.
function doThenFunc(promise, value, resolve, reject) {
//Circular reference
if (promise === value) {
reject(
new TypeError('Chaining cycle detected for promise')
)
return
}
//If value is a promise object
if (value instanceof Deferred) {
//Call the then method and wait for the result
value.then(
function (val) {
doThenFunc(promise, value, resolve, reject)
},
function (reason) {
reject(reason)
}
)
return
}
//If it is not a promise object, it returns directly
resolve(value)
}
Now let’s try to return a new promise object in then.
const delayDouble = (num, time) => new Deferred((resolve) => {
console.log(new Date())
setTimeout(() => {
resolve(2 * num)
}, time)
})
new Deferred(resolve => {
setTimeout(() => {
resolve(1)
}, 2000)
})
.then(val => {
console.log(new Date(), val)
return delayDouble(val, 2000)
})
.then(val => {
console.log(new Date(), val)
})
The above results are also perfectly in line with our expectations.
Catch method
In fact, the catch method is very simple, which is equivalent to the abbreviation of then method.
class Deferred {
constructor(callback) {}
then(onResolve, onReject) {}
catch(onReject) {
return this.then(null, onReject)
}
}
Static method
resolve/reject
The promise class also provides two static methods to directly return the promise object whose state has been fixed.
class Deferred {
constructor(callback) {}
then(onResolve, onReject) {}
catch(onReject) {}
static resolve(value) {
return new Deferred((resolve, reject) => {
resolve(value)
})
}
static reject(reason) {
return new Deferred((resolve, reject) => {
reject(reason)
})
}
}
all
The all method accepts an array of promise objects and changes the state of all promise objects in the array tofulfilled
The result is also an array, and each value of the array corresponds to the internal result of the promise object.
First of all, we need to determine whether the incoming parameter is an array, and then construct a result array and a new promise object.
class Deferred {
static all(promises) {
//Non array parameter, throw exception
if (!Array.isArray(promises)) {
return Deferred.reject(new TypeError('args must be an array'))
}
//Used to store the results of each promise object
const result = []
const length = promises.length
//If remaining is set to zero, all promise objects have been fulfilled
let remaining = length
const promise = new Deferred(function (resolve, reject) {
// TODO
})
return promise
}
}
Next, we need to make a judgment to intercept the resolve of each promise object. Each resolve needs toremaining
Minus one untilremaining
Zero.
class Deferred {
static all(promises) {
//Non array parameter, throw exception
if (!Array.isArray(promises)) {
return Deferred.reject(new TypeError('args must be an array'))
}
Const result = [] // used to store the result of each promise object
const length = promises.length
let remaining = length
const promise = new Deferred(function (resolve, reject) {
//If the array is empty, an empty result is returned
if (promises.length === 0) return resolve(result)
function done(index, value) {
doThenFunc(
promise,
value,
(val) => {
//The result of resolve is put into result
result[index] = val
if (--remaining === 0) {
//If all the promises have returned results
//Then run the following logic
resolve(result)
}
},
reject
)
}
//Put in asynchronous queue
setTimeout(() => {
for (let i = 0; i < length; i++) {
done(i, promises[i])
}
})
})
return promise
}
}
Now we use the following code to judge whether the logic is correct. As expected, after the code runs, after 3 seconds, the console will print an array[2, 4, 6]
γ
const delayDouble = (num, time) => new Deferred((resolve) => {
setTimeout(() => {
resolve(2 * num)
}, time)
})
console.log(new Date())
Deferred.all([
delayDouble(1, 1000),
delayDouble(2, 2000),
delayDouble(3, 3000)
]).then((results) => {
console.log(new Date(), results)
})
The above running results are basically in line with our expectations.
race
The race method also accepts an array of promise objects, but it only needs one promise to be changed tofulfilled
The status will return the result.
class Deferred {
static race(promises) {
if (!Array.isArray(promises)) {
return Deferred.reject(new TypeError('args must be an array'))
}
const length = promises.length
const promise = new Deferred(function (resolve, reject) {
if (promises.length === 0) return resolve([])
function done(value) {
doThenFunc(promise, value, resolve, reject)
}
//Put in asynchronous queue
setTimeout(() => {
for (let i = 0; i < length; i++) {
done(promises[i])
}
})
})
return promise
}
}
Next, let’s change the previous case to race. As expected, after the code runs, after one second, the console will print a 2.
const delayDouble = (num, time) => new Deferred((resolve) => {
setTimeout(() => {
resolve(2 * num)
}, time)
})
console.log(new Date())
Deferred.race([
delayDouble(1, 1000),
delayDouble(2, 2000),
delayDouble(3, 3000)
]).then((results) => {
console.log(new Date(), results)
})
The above running results are basically in line with our expectations.
summary
A simple version of the promise class has been implemented. Some details are omitted here. The complete code can be accessedgithub. The emergence of promise has laid a solid foundation for the later async syntax. In the next blog, you can have a good chat about the asynchronous programming history of JavaScript, and accidentally dig a hole for yourself…