Interview Promise
The context in which Promise appears
- Asynchronous callbacks are discontinuous, with multiple callbacks registered at the same time and execution time not linear enough
const callback1 = function() {}
const callback2 = function() {}
const callback3 = function() {}
const res1 = ajax(callback1)
const res2 = ajax(callback2)
const res3 = ajax(callback3)
Copy the code
All three callbacks are registered at the same time, but the timing of their calls is really variable
- The callback hell
function triggerCb(cb1) {
return function(cb2) {
cb1()
cb2()
}
}
Copy the code
Callback hell causes the following problems:
1. It is easy to cause extremely poor readability of the code, and the internal operation will be very chaotic
2. Each callback function has success and failure states, requiring each callback function to maintain a set of error mechanisms
Those familiar with design patterns should be aware of the chain of responsibility pattern. Promises are a typical example of asynchronous invocation in a chain of responsibility
I Promise.
The state of the Promise
Promise states include: pending, depressing, and Rejected
According to the official document:
A promise must be in one of three states: pending, fulfilled, or rejected.When pending, a promise:may transition to either the fulfilled or rejected state.When fulfilled, a promise:must not transition to any other state.must have a value, which must not change.When rejected, a promise:must not transition to any other state.must have a reason, which must not change. Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.
When the state changes from pending to fulfiiled or rejected, it is locked and cannot be backtracked or changed
Promise of thenable
The usage of Promise. Then must be familiar to everyone, but specific it contains what hidden points, today can be combed
1. The arguments to then are not necessarily callback functions and pass through values
The two arguments to then are themselves defined as resolvedFn and rejectedFn, but it can pass non-functional data
var p = Promise.resolve('martin')
p.then(1).then((res) = > {})
p.then(new Error('error')).then((res) = > {})
Copy the code
As shown in the above example, the final result is 1, which indicates that when the THEN processes non-functional data, the PromiseResult of the original is directly passed to the next THEN (including rejectedFn).
2. Then returns a new Promise. Then returns a variable that is not the same as the Promise that called the THEN method
var p = Promise.reject('martin')
varp1 = p.then() p1 ! == p p1.PromiseStatus ==='fulfilled'
p.PromiseStatus === 'rejected'
Copy the code
In the above example, p1 is the variable returned after P calls then. == p, the result is true, proving that then does not return a promise
then must return a promise;
The Promise. Then callback must be a Promise
3. Then can be called multiple times by a Promise
As mentioned above, then returns a new Promise. When we write code, we usually make chained calls that inherit from the previous Promise to generate a new Promise, but we can also use variable caching of promises and register several THEN callbacks on that variable
var p = Promise.resolve('martin')
p.then(() = > {})
p.then(() = > {})
Copy the code
These two THEN methods are registered on P, so the state of P will act on both then methods
4. Then Returns the state
We know that the then method returns a new Promise, but does its state change? In most cases, the Promise returned by THEN is a pity state. However, as long as the THEN callback function throws an error, the new Promise state will be set to Rejected
var p = Promise.reject('martin')
var p1 = p.then(() = > {}) // This is very depressing
var p2 = p.then(() = > { throw new Error('martin')})// Then it becomes Rejected
var p3 = p.then(() = > { return new Error('martin')})// This is a pity. // Only throw can become rejected
Copy the code
Promise.catch
1. I catch the Promise in the rejected state
We said that multiple “then” can work on a promise, so can “catch”
var p = Promise.reject('martin')
p.then(() = > {}, function reject(){})
p.catch(e= > e)
p.catch(e= > e)
Copy the code
The second callback, reject, and the following two catches are executed. Reject is registered, and a catch is not executed. A new Promise is returned by then. When an error occurs, if the upstream Promise does not intercept, the downstream Promise will take over
var p = Promise.reject('martin')
var p2 = p.then(() = >{}).catch(function error(){})
var p3 = p.then(() = >{}, () = >{}).catch(function error(){})
Copy the code
P2’s catch callback executes, but P3’s catch callback does not, because P3’s catch is blocked by the then-registered reject function
2. Promise.catch hijacks the then callback in case of an error
Then is hijacked by a catch
3. A catch returns a Promise state. A catch returns a Promise state as then
Promise.finally
1. Promise.finally state follows the previous Promise
The promise.finally state follows the previous Promise
var p = Promise.reject('martin')
var p1 = p.finally(() = > {})
Copy the code
The state of P1 will follow the state of P, so it is rejected
Write a Promise
- We’ll start by defining a class for a Promise that has two private values, state and value
class AWeSomePromise {
constructor() {
this.PromiseState = 'pending'
this.PromiseResult = void 0}}Copy the code
- Promise is instantiated with a callback that takes resolve and Reject, which assign a value to the passed value and change the state
class AWeSomePromise {
constructor(callback) {
this.PromiseState = 'pending'
this.PromiseResult = void 0
if (typeof callback === 'function') {
callback(this.resolve.bind(this), this.reject.bind(this))}}resolve(resvalue) {
this.excuteStatus(resvalue, 'fulfilled')}reject(resvalue) {
this.excuteStatus(resvalue, 'rejected')}excuteStatus(promiseResult, promiseState) {
this.PromiseResult = promiseResult / / assignment
this.PromiseState = promiseState // Change the state
triggerFn() // Trigger thenable}}Copy the code
-
Define a THEN method on the prototype, where you need to combine several of the features mentioned above in promise.THEN
- Thenable returns a new Promise
then(resolveFn, rejectFn) { this.newPromise = new AWeSomePromise() // then returns a new Promise return this.newPromise } Copy the code
- The second and later thenable calls resolveFn or rejectFn directly based on the previous state
then() { if(status ! = ='pending') { this.excuteThen() } } excuteThen() { setTimeout(() = > { if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled') else if (this.PromiseState === 'rejected') this.triggerFn('rejected')},10)}Copy the code
- The parameters thenable receives can be non-functional data, and several Thenable can work simultaneously on a Promise instance
get resolveFns() { return[]}/ / save resolveFn get rejectFns() { return[]}/ / in the same way then(resolveFn, rejectFn) { const isResolveFn = typeof resolveFn === 'function' // determine whether reslveFn is a function const isRejectFn = typeof rejectFn === 'function' function excuteFn(thenStatus) { return function(fn, newPromise) { const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction try { newPromise.PromiseState = status = 'fulfilled' if (this.PromiseState === thenStatus && type) { if (fn) { // Check whether the passed resolveFn is a function value = fn(this.PromiseResult) // value is a global value, which we'll talk about in a second if (typeof value === 'undefined') value = this.PromiseResult // If thenable does not return a value, the value will be passed through newPromise.PromiseResult = value // Update the value of the next Promise (when a Promise is asynchronous) } else { value = this.PromiseResult } } if (thenStatus === 'rejected' && !type) { const errorCallbacks = newPromise.errorCallbacks this.triggerErrorCallback(errorCallbacks) // Trigger the catch callback}}catch(err) { status = thenStatus value = err newPromise.PromiseState = status = 'fulfilled' setTimeout(() = > { this.triggerErrorCallback(newPromise.errorCallbacks) }, 0)}this.triggerFn(null, newPromise.finallyCallbacks) // Finally callback is triggered}}this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise)) this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise)) } triggerFn(thenStatus, fn) { // Loop trigger resolveFns and rejectFns if(! fn) { fn = thenStatus ==='fulfilled' ? this.resolveFns : this.rejectFns } let currentFn = null while(currentFn = fn.shift()) { // We need to fetch the first callback from resolveFns and kick it out of the queue if (currentFn) { currentFn() } } } The resolve, reject, and excuteStatus operations need to be changed resolve(resvalue) { this.excuteStatus(resvalue, 'fulfilled'.this.triggerFn.bind(this.'fulfilled'))}reject(rejectvalue) { this.excuteStatus(rejectvalue, 'rejected'.() = > { this.triggerFn.bind(this.'rejected')})}excuteStatus(promiseResult, promiseStatus, cb){...setTimeout(() = > { // Since promise. thenable is a microtask, setTimeout is used to simulate it if (cb) { cb() } this.triggerFn(this.finallyCallbacks) / / triggers finallyCallbacks }, 0)}Copy the code
- Thenable Transparent transmission of status and value
We know that the status of the second and subsequent thenable registrations is not changed by the resolve and Reject functions, so we need to define several values in the global context to cache the status of the previous one
let status = 'pending' let value = void 0 class AWeSomePromise { constructor(callback) { this.PromiseState = status // Assign the previous state directly this.PromiseResult = value // Change the last value directly if (typeof callback === 'function') { callback(this.resolve.bind(this), this.reject.bind(this))}}then() { function excuteFn(thenStatus) {...return function(fn, newPromise) { try { newPromise.PromiseState = status = 'fulfilled' if (this.PromiseState === thenStatus && type) { if (fn) { // Check whether the passed resolveFn is a function value = fn(this.PromiseResult) // Value is a global value, and the value returned by the previous thenable is cached if (typeof value === 'undefined') value = this.PromiseResult // If thenable does not return a value, the value will be passed through newPromise.PromiseResult = value // Update the value of the next Promise (when a Promise is asynchronous) } else { value = this.PromiseResult } } } catch(err) { status = thenStatus value = err } } ... }}}Copy the code
So this might be a little bit convoluted, but let’s make sure we understand the relationship between new promises and promises
var p = new AWeSomePromise() var p1 = p.then() p.newPromise === p1 Copy the code
The relationship is as follows: promise.newPromise === nextPromise So that the basic principles of Thenable have been developed, let’s look at the catch function
-
The catch function is similar to the Thenable development process, It can also be developed based on features such as multiple catches acting on a single Promise instance, a catch hijacking thenable error, and a Reject that triggers a Promise in the Rejected state when the reject function is unregistered
reject() { this.excuteStatus(rejectvalue, 'rejected'.() = > { this.triggerErrorCallback() // All catch callbacks registered under the current Promise need to be triggered this.triggerFn.bind(this.'rejected')})}get errorCallbacks() { return[]}catch(callback) { this.errorCallbacks.push(callback) return this.newPromise || (this.newPromise = new AWeSomePromise()) // Catch may not have registered thenable before, so this. NewPromise needs to be handled } triggerErrorCallback(errorCallbacks = this.errorCallbacks) { let currentFn = null while(currentFn = errorCallbacks.shift()) { if (currentFn) { currentFn(this.PromiseResult) } } } then() { function excute() { return function(fn, newPromise) { const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction try{ if (thenStatus === 'rejected' && !type) { Reject (reject); reject (reject); reject (reject) const errorCallbacks = newPromise.errorCallbacks this.triggerErrorCallback(errorCallbacks) } } catch(err) { newPromise.PromiseState = status = 'fulfilled' setTimeout(() = > { this.triggerErrorCallback(newPromise.errorCallbacks) }, 0)}}}}Copy the code
-
The finally function executes whether resolve or reject is fired
excuteStatus(cb) {
setTimeout(() = > {
if (cb) {
cb()
}
this.triggerFn(this.finallyCallbacks) // The finally callback under the current Promise is invoked. Promise.finally ()
}, 0)}then() {
function excuteFn() {
return function() {...this.triggerFn(null, newPromise.finallyCallbacks) // Then ().finally}}}Copy the code
- Dealing with asynchrony, which is the core case for Promise, lets start with an example of asynchrony:
var p = new Promise(resolve= > {
setTimeout(() = > { resolve('martin')},1000)
})
p.then(() = > {}).then(() = > {})...
Copy the code
The above p.tenable function and all subsequent chained thenable functions will be executed after 1000ms delay. According to the second and subsequent Thenable functions we wrote above, excuteThen will be triggered when the call is registered. ResolveFns will be triggered after a delay of 10ms, but the above case obviously needs to be delayed for another 1000ms. Therefore, we do not know that the resolve function will be delayed for a few seconds, but when it is executed, it means that the subsequent thenable callback can be executed. Therefore, we only need to cache the Promise instances returned by Thenable into the array, detect the subscript value of the current Promise instance in the array after the execution of resolve function, and trigger resolveFns one by one
let promiseArray = []
class AWeSomePromise {
excuteStatus() {
this.reconnect()
}
then() {
promiseArray.push(newPromise)
}
reconnect() {
if (promiseArray.length) {
const index = promiseArray.indexOf(this)
if (~index) {
promiseArray.slice(index).forEach(context= > {
if (context instanceof AWeSomePromise) {
context.excuteThen()
}
})
}
}
}
}
Copy the code
In this way, we can preliminarily complete the simulation of Promise
The complete code
let status = 'pending'
let value = void 0
let promiseArray = []
class MyPromise {
constructor(callback) {
this.PromiseState = status
this.PromiseResult = value
this.resolveFns = []
this.rejectFns = []
this.errorCallbacks = []
this.finallyCallbacks = []
this.done = false
if (callback) {
callback(this.resolve.bind(this), this.reject.bind(this))}}resolve(resvalue) {
this.excuteStatus(resvalue, 'fulfilled'.this.triggerFn.bind(this.'fulfilled'))}reject(rejectvalue) {
this.excuteStatus(rejectvalue, 'rejected'.() = > {
this.triggerErrorCallback()
this.triggerFn.bind(this.'rejected')})}excuteStatus(promiseResult, promiseStatus, cb) {
this.PromiseResult = value = promiseResult
status = promiseStatus
this.PromiseState = status
if (this.newPromise) {
this.newPromise.PromiseResult = promiseResult
this.newPromise.PromiseState = promiseStatus
}
this.reconnect()
setTimeout(() = > {
if (cb) {
cb()
}
this.triggerFn(this.finallyCallbacks)
}, 0)}then(resolveFn, rejectFn) {
this.newPromise = new MyPromise()
const isResolveFunction = typeof resolveFn === 'function'
const isRejectFunction = typeof rejectFn === 'function'
function excuteFn(thenStatus) {
return function(fn, newPromise) {
const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction
try {
newPromise.PromiseState = status = 'fulfilled'
if (this.PromiseState === thenStatus && type) {
if (fn) {
value = fn(this.PromiseResult)
newPromise.PromiseResult = value
} else {
value = this.PromiseResult
}
}
if (thenStatus === 'rejected' && !type) {
const errorCallbacks = newPromise.errorCallbacks
this.triggerErrorCallback(errorCallbacks)
}
} catch(err) {
status = thenStatus
value = err
newPromise.PromiseState = status = 'fulfilled'
setTimeout(() = > {
this.triggerErrorCallback(newPromise.errorCallbacks)
}, 0)}this.triggerFn(null, newPromise.finallyCallbacks)
}
}
this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise))
this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise))
if(status ! = ='pending') {
this.excuteThen()
}
promiseArray.push(this)
return this.newPromise
}
catch(callback) {
this.errorCallbacks.push(callback)
return this.newPromise || (this.newPromise = new MyPromise())
}
finally(callback) {
this.finallyCallbacks.push(callback)
return this.newPromise || (this.newPromise = new MyPromise())
}
reconnect() {
if (promiseArray.length) {
const index = promiseArray.indexOf(this)
if (~index) {
promiseArray.slice(index).forEach(context= > {
if (context instanceof MyPromise) {
context.excuteThen()
}
})
}
}
}
triggerErrorCallback(errorCallbacks = this.errorCallbacks) {
let currentFn = null
while(currentFn = errorCallbacks.shift()) {
if (currentFn) {
currentFn(this.PromiseResult)
}
}
}
triggerFn(thenStatus, fn) {
if(! fn) { fn = thenStatus ==='fulfilled' ? this.resolveFns : this.rejectFns
}
let currentFn = null
while(currentFn = fn.shift()) {
if (currentFn) {
currentFn()
}
}
}
excuteThen() {
setTimeout(() = > {
if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled')
else if (this.PromiseState === 'rejected') this.triggerFn('rejected')},10)}}Copy the code
The articles
🌲 mid-advanced front-end does not necessarily understand setTimeout | netease practice summary
80 lines of code to achieve Vue skeleton screen 🏆 | netease small practice
You misunderstood Vue nextTick