“This is the third day of my participation in the First Challenge 2022. For details: First Challenge 2022”
The words written in the front
What will you gain from reading this article?
- Understand the whole process of V8 Promise source code, the world is not able to trap your Promise title, I am so sure of this article
- Only understanding or implementing the Promise/A+ specification is A long way from JavaScript promises
- If you answer this Promise at the depth of this article, it’s a great way to get an SP or SSP offer, because the interviewer probably doesn’t know that either.
Do you know what the actual order of Promise execution looks like in browser & Node? If you’ve only seen the Promise implementation of the Promise/A+ specification, I’m pretty sure you’re wrong about the order of Promise execution. Take a look at these two questions if you don’t believe me.
Promise.resolve().then(() = > {
console.log(0);
return Promise.resolve(4)
}).then(res= > {
console.log(res);
})
Promise.resolve().then(() = > {
console.log(1);
}).then(() = > {
console.log(2);
}).then(() = > {
console.log(3);
}).then(() = > {
console.log(5);
}).then(() = > {
console.log(6);
})
// 0 1 2 3 5 6
new Promise((resolve, reject) = > {
Promise.resolve().then(() = > {
resolve({
then: (resolve, reject) = > resolve(1)});Promise.resolve().then(() = > console.log(2));
});
}).then(v= > console.log(v));
1 / / 2
Copy the code
According to the Promise/A+ specification, the above code should print 0, 1, 2, 4, 3, 5, 6, because when THEN returns A Promise, it waits for the Promise to complete before synchronizing the state and value to then.
But in V8 and even in every major browser that supports Promise, the result is 0, 1, 2, 3, 4, 5, 6
How do they manage to be different from the Promise/A+ specification (and not exactly the same, since promises don’t explicitly describe their execution logic, just give some specification) and be consistent?
Remember that promises are part of JavaScript, where the implementation specification for promises comes not from Promis/A+, but from the ECMAScript specification.
So to know the answer to this question, we can’t just look at Promise/A+, we should focus on ECMAScript or V8 for code execution and sequence.
I’ll take a look at the V8 Promise source code in conjunction with the ECMAScript specification.
Three more things to mention in advance:
- This article is more suitable for students who have a certain foundation of Promise to read, if the students do not know the Promise can take a look at these articles
- γ δΎ ε₯ γ He made a promise
- Promise won’t…? Look here!! The most accessible Promise ever!!
- For the following c++ code posts, you just need to focus on the place with Chinese comments
- Since code blocks do not wrap themselves, it is recommended that you read them on PC for a better reading experience
- The article is very long, can collect, have time to calm down to read slowly can also.
- Like ππ», ππ», and π»
Get into the business
PromiseState
The three states of Promise, pending, depressing and Rejected, are as follows:
// Promise constants
extern enum PromiseState extends int31 constexpr 'Promise::PromiseState' {
kPending,// Wait state
kFulfilled,// Success status
kRejected// Failed state
}
Copy the code
A newly created Promise is in a pending state. When the resolve or reject function is called, the Promise will be in the fulfilled or rejected state, after which the state of the Promise will remain unchanged, that is to say, the state change of the Promise is irreversible. Nothing happens if you call resolve or reject again. There are several state-related assert statements in the Promise source code, and you’re probably familiar with all three Promise states.
JSPromise
JSPromise describes the basic information of the Promise, the source code is as follows:
bitfield struct JSPromiseFlags extends uint31 {
/ / Promise state, kPending/kFulfilled/kRejected
status: PromiseState: 2 bit;
// This is a big pity /onRejected function,
// Promises that have not called the then method have no handlers
// The catch method is a then method.
has_handler: bool: 1 bit;
handled_hint: bool: 1 bit;
async_task_id: int32: 22 bit;
}
@generateCppClass
extern class JSPromise extends JSObject {
macro Status(a): PromiseState {
// Get the state of the Promise
In a / / kPending kFulfilled kRejected
return this.flags.status;
}
macro SetStatus(status: constexpr PromiseState): void {
// Only pending promises can be changed
assert(this.Status() == PromiseState::kPending);
// Promise cannot be set to pending after a successful creation
assert(status ! = PromiseState::kPending);this.flags.status = status;
}
macro HasHandler(a): bool {
// Determine if Promise has a handler
return this.flags.has_handler;
}
macro SetHasHandler(a): void {
this.flags.has_handler = true;
}
// Promise handles functions or results, which can be:
/ / empty
// This is a big pity/onFulfilled
// Promise validation value (resolve argument)
reactions_or_result: Zero|PromiseReaction|JSAny;
flags: SmiTagged<JSPromiseFlags>;
}
Copy the code
The SetStatus method is called when the Promise state changes, such as when resolve/reject is called. When the Javascript layer calls the resolve method, its parameter will be specified as either the ‘resolve’ or ‘or_result’ parameter. When the Javascript layer calls the then method, the handler is already there, and SetHasHandler() is called. The Status/SetStatus methods get the Promise state and set the Promise state.
other
- Executor: is a function,
Promise
The constructor receives arguments that are calledexecutor
The parameters passed in are respectivelyresolve
εreject
. - PromiseReaction: indicates an object
Promise
Because of aPromise
Multiple callsthen
Methods have multiple handlers, so the underlying data structure is a linked list, each node storedonFulfilled
εonRejected
Function.
let p = new Promise((resolve, reject) = > {
resolve(123)
// It will set its reaction to 123
// SetHasHandler is called
resolve(234)// Nothing happens
reject(234)// Nothing happens
})
Copy the code
The constructor
The constructor source is as follows:
PromiseConstructor(
js-implicit context: NativeContext, receiver: JSAny,
newTarget: JSAny)(executor: JSAny): JSAny {
// 1. Throw a TypeError exception if the new keyword does not exist.
if (newTarget == Undefined) {
ThrowTypeError(MessageTemplate::kNotAPromise, newTarget);
}
// 2. Throw a TypeError Exception if the argument passed is not a callback function.
if(! Is<Callable>(executor)) {ThrowTypeError(MessageTemplate::kResolverNotAFunction, executor);
}
let result: JSPromise;
// Construct a Promise object
result = NewJSPromise(a);Get the resolve and reject functions from the Promise object
const funcs = CreatePromiseResolvingFunctions(result, True, context);
const resolve = funcs.resolve;
const reject = funcs.reject;
try {
Call executor directly and synchronously, with resolve and reject as arguments
Call(context, UnsafeCast<Callable>(executor), Undefined, resolve, reject);
} catch (e) {
Call reject if an exception occurs
Call(context, reject, Undefined, e);
}
return result;
}
Copy the code
First, two ThrowTypeErrors are analyzed. The first ThrowTypeError is raised by the following code.
Promise(a)// Uncaught TypeError: undefined is not a promise
Copy the code
Reason is not use the new operator call Promise constructor, newTarget is Undefined, triggered ThrowTypeError (MessageTemplate: : kNotAPromise, newTarget).
The second ThrowTypeError is raised by the following code.
new Promise(a)// Uncaught TypeError: Promise resolver undefined is not a function
Copy the code
NewTarget is not Undefined and the first ThrowTypeError will not be raised. But a second ThrowTypeError is raised when the Promise constructor is called without passing executor.
The executor type is a function, and in the JavaScript world, callbacks are usually called asynchronously, but executors are called synchronously. Executor is called synchronously on the Call(Context, UnsafeCast(Executor), Undefined, resolve, reject) line.
console.log('Sync execution begins')
new Promise((resolve, reject) => {
resolve()
console.log('Executor synchronizes execution')
})
console.log('Synchronization completed')
// This code is printed in the following order:
// Synchronization starts
// Executor executes synchronously
// Synchronization is complete
Copy the code
Executor, the argument received by the Promise constructor, is called synchronously
then
ECMAScript specification
PromisePrototypeThen
The Promise’s then method passes in two callback functions, onFulfilled and onRejected, which are used to process the fulfilled and Rejected states, respectively, and return a new Promise.
The then function of the JavaScript layer is the PromisePrototypeThen function of V8.
PromisePrototypeThen(js-implicit context: NativeContext, receiver: JSAny)(
onFulfilled: JSAny, onRejected: JSAny): JSAny {
const promise = Cast<JSPromise>(receiver) otherwise ThrowTypeError(
MessageTemplate::kIncompatibleMethodReceiver, 'Promise.prototype.then',
receiver);
const promiseFun = UnsafeCast<JSFunction>(
context[NativeContextSlot::PROMISE_FUNCTION_INDEX]);
let resultPromiseOrCapability: JSPromise|PromiseCapability;
let resultPromise: JSAny;
label AllocateAndInit {
// Create a new promise to return as the result of this call to THEN
// Then returns a promise.
const resultJSPromise = NewJSPromise(promise);
resultPromiseOrCapability = resultJSPromise;
resultPromise = resultJSPromise;
}
// onFulfilled and onRejected are two parameters for then
// If not, the default value is Undefined
const onFulfilled = CastOrDefault<Callable>(onFulfilled, Undefined);
const onRejected = CastOrDefault<Callable>(onRejected, Undefined);
// Call PerformPromiseThenImpl
PerformPromiseThenImpl(
promise, onFulfilled, onRejected, resultPromiseOrCapability);
// Return a new Promise
return resultPromise;
}
Copy the code
The PromisePrototypeThen function creates a new Promise object, takes the two parameters received by the THEN, and calls PerformPromiseThenImpl to do most of the work. One thing to note here is that the then method returns a newly created Promise.
const myPromise2 = new Promise((resolve, reject) = > {
resolve('foo')})const myPromise3 = myPromise2.then(console.log)
MyPromise2 and myPromise3 are two different objects
// There are different states and different handlers
console.log(myPromise2 === myPromise3) / / print false
Copy the code
The then method returns a new Promise
PerformPromiseThenImpl
ECMAScript specification
PerformPromiseThenImpl has 4 parameters. Since PerformPromiseThenImpl is called then, the first three parameters are the Promise objects of the called THEN method. As well as the object of the two handlers onFulfilled and onRejected, the last parameter to invoke the resultPromiseOrCapability then return to the new Promise of object.
PerformPromiseThenImpl PerformPromiseThenImpl
transitioning macro PerformPromiseThenImpl(implicit context: Context)( promise: JSPromise, onFulfilled: Callable|Undefined, onRejected: Callable|Undefined, resultPromiseOrCapability: JSPromise|PromiseCapability|Undefined): void {
if (promise.Status() == PromiseState::kPending) {
// Pending state branch
// If the current Promise state is still pending
// Just store the handler for the then binding
const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
// Get the Promise field
const promiseReactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// Consider a case where a Promise might have multiple THEN's
// Reaction is a linked list, and each binding handler is inserted at the head of the list
// Save all the handlers for Promise
const reaction = NewPromiseReaction(
handlerContext, promiseReactions, resultPromiseOrCapability,
onFulfilled, onRejected);
// Either reaction - or_result or its promised handler list can be stored
// The final result of the Promise, because now the Promise is pending,
// So there is a linked list of functions reaction
promise.reactions_or_result = reaction;
} else {
// This is a big pity and rejected state
const reactionsOrResult = promise.reactions_or_result;
let microtask: PromiseReactionJobTask;
let handlerContext: Context;
/ / fulfilled branch
if (promise.Status() == PromiseState::kFulfilled) {
handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
// Generate microtask tasks
microtask = NewPromiseFulfillReactionJobTask(
handlerContext, reactionsOrResult, onFulfilled,
resultPromiseOrCapability);
} else / / rejected branch
deferred {
assert(promise.Status() == PromiseState::kRejected);
handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
// Generate microtask tasks
microtask = NewPromiseRejectReactionJobTask(
handlerContext, reactionsOrResult, onRejected,
resultPromiseOrCapability);
// If the promise has not yet been bound to a handler
if(! promise.HasHandler()) {
/ / specification of HostPromiseRejectionTracker (promise, "reject"),
// Generates a microTask to detect, which will be described separately later.
runtime::PromiseRevokeReject(promise); }}// Even if the promise is already in the fulfilled or rejected state when the then method is called,
// The onFulfilled or onRejected parameter of the then method will not be executed immediately.
// Instead, the microTask queue is executed
EnqueueMicrotask(handlerContext, microtask);
}
promise.SetHasHandler(a); }Copy the code
Pending branch of PerformPromiseThenImpl
PerformPromiseThenImpl has three branches, each corresponding to the three states of a Promise. The pending branch is entered when the Promise called by the THEN method is in a pending state. The pending branch calls the NewPromiseReaction function, which generates the PromiseReaction object based on the onFulfilled and onRejected parameters, and stores the Promise handler. And assigning it to the JSPromise field, then calling Promise.sethashandler () to set has_handler to True (indicating that the Promise object is bound to a handler)
Consider a case where a Promise can invoke multiple THEN calls in succession, such as:
const p = new Promise((resolve, reject) = > {
setTimeout(_= > {
resolve('my code delay 2000 ms')},2000)
})
p.then(result= > {
console.log('第 1 δΈͺ then')
})
p.then(result= > {
console.log('second then')})Copy the code
P calls the THEN methods twice, and each then method generates a PromiseReaction object. When the then method is first invoked, the object PromiseReaction1 is generated, and its reactions_or_result is stored as PromiseReaction1.
The second call to the then method generates the object PromiseReaction2. When the NewPromiseReaction function is called, promisereaction2. next = PromiseReaction1, PromiseReaction1 becomes the next node of PromiseReaction2, and its reactions_or_result is PromiseReaction2. PromiseReaction2 enters the list of Promise handlers, which is the head of the list. The NewPromiseReaction function is as follows:
macro NewPromiseReaction(implicit context: Context)( handlerContext: Context, next: Zero|PromiseReaction, promiseOrCapability: JSPromise|PromiseCapability|Undefined, fulfillHandler: Callable|Undefined, rejectHandler: Callable|Undefined): PromiseReaction {
const nativeContext = LoadNativeContext(handlerContext);
return new PromiseReaction{
map: PromiseReactionMapConstant(),
next: next, // Next stores the next node in the list
reject_handler: rejectHandler,// Failure handler
fulfill_handler: fulfillHandler,// Success handler function
promise_or_capability: promiseOrCapability,// Generates a new Promise object
continuation_preserved_embedder_data: nativeContext
[NativeContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX]
};
}
Copy the code
When p is in the pending state, the reactions_or_result field of p is depicted in the following figure.
The figure below is not a MicroTask queue, the figure below is not a MicroTask queue.
The depressing branch of the PerformPromiseThenImpl function
The fulfilled branch logic is much simpler, which deals with the logic of calling the then method when a Promise is fulfilled:
First call NewPromiseFulfillReactionJobTask generated microtask, Then EnqueueMicrotask(handlerContext, microTask) puts the microtask just generated into the microTask queue, Finally, call promise.sethashandler () to set has_handler to true.
new Promise((resolve, reject) = > {
resolve()
}).then(result= > {
console.log('Execute after entering microTask queue')})console.log('Synchronization completed')
// This code is printed in the following order:
// Synchronization is complete
// Execute after entering microtask queue
Copy the code
Although the Promise is already in the fulfilled state when the THEN method is called, the onFulfilled callback function of the THEN method will not execute immediately, but instead will enter the MicroTask queue for execution.
The Rejected branch of PerformPromiseThenImpl
The Rejected branch has roughly the same logic as the Fulfilled branch, but before the Rejected branch adds the onRejected handler to the microTask queue, it will judge whether the current promise already has a handler. If there have been a will invoke the runtime: first: PromiseRevokeReject (promise), the last call promise. SetHasHandler () will has_handler set to true.
if(! promise.HasHandler()) {
runtime::PromiseRevokeReject(promise);
}
Copy the code
Here’s the runtime: : PromiseRevokeReject (promise) is in the ECMAScript standard HostPromiseRejectionTracker (promise, “handle”), HostPromiseRejectionTracker is an abstract method, this means that it is not stated specific logic. Basically, it flags the Rejected state handler that promise has bound to. Don’t be confused about why you’re doing this. I’ll focus on it separately.
Note 1 HostPromiseRejectionTracker in both cases is called:
When a promise is rejected without any handlers, its action parameter is set to “reject.”
When a handler is first added to a rejected Promise, it is called and its action parameter is set to “Handle.”
Reference to the ECMAScript specification (translated by author)
summary
-
When a Promise is called to the then method, a new Promise object, resultPromise, is created
-
Different logic is then performed based on the current state of the promise
- Pending status: Indicates the pending status
then
The two handler functions passed become onePromiseReaction
Node inserted intopromise.reactions_or_result
The head (PromiseReaction
Is a linked list structure), this step is collecting dependencies, waitingpromise
Triggers when the status is complete. - The depressing state: A microtask will be created to invoke the ondepressing process passed in, and its parameter will be parameter S_or_result
reactions_or_result
ζ―promise
That is, the callresolve
Is passed as a parametervalue
) and insert it into the MicroTask queue. - The Rejected state: Similar to the fulfilled state, a microTask will be created to call the incoming one
onRejected
Process the function, and willreactions_or_result
As an argument to the invocation, if the current Promise does not have a processing function (that is, the Promsie of the fulfilled state is first called then method), it will be marked as boundonRejected
Function, and then inserts its Microtask into the MicroTask queue.
- Pending status: Indicates the pending status
-
Calling promise.sethashandler () sets the promise’s has_handler to true, indicating that its invoked then method is bound to a handler.
-
Finally, the new Promise object is returned.
To review the three states of reactions or_result, either null, chained, or promise:
When a promise was first created, its value was null,
When the state of the promise changes to pity/Rejected, its value is the parameter value passed in by the corresponding resolve(value)/ Reject (value) function, that is, the value of the promise.
When a promise is pending and THEN is called, either response or response is a linked list, each of which stores the handler passed in when the THEN is called.
reslove
new Promise((resolve, reject) = > {
setTimeout(_= > resolve('fulfilled'), 5000)
}).then(value= > {
console.log(value)
}, reason= > {
console.log('rejected')})Copy the code
After 5 seconds of the above code, execute the resolve function, and the console prints depressing.
FulfillPromise
ECMAScript specification
Reslove (value) is FulfillPromise(promise, value) of FulfillPromise(FulfillPromise, value) from the specification, which is fulfilled by changing the state of a promise from pending to FulfillPromise, And add all of the Promise’s handlers to the MicroTask queue for execution.
The resolve function finally calls V8’s FulfillPromise function, and the source code is as follows:
// https://tc39.es/ecma262/#sec-fulfillpromise
transitioning builtin
FulfillPromise(implicit context: Context)( promise: JSPromise, value: JSAny): Undefined {
// The current state of a PROMISE must be pending, because state changes are irreversible
assert(promise.Status() == PromiseState::kPending);
// Fetch the Promise handler. Before that, the state of the Promise is pending
// So the reaction list is reaction reaction,
// The reactions node stores the inside function
const reactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// A Promise needs to be fulfilled, so it will be a big song, or a big song
// It is no longer a handler, but the result of the Promise, the argument passed in when resolve is called
promise.reactions_or_result = value;
// Set the state of Promise to be fulfilled
promise.SetStatus(PromiseState::kFulfilled);
// Get the result of the Promise
TriggerPromiseReactions(reactions, value, kPromiseReactionFulfill);
return Undefined;
}
Copy the code
And the logic of FulfillPromise is to get the Promise handler of FulfillPromise into Make promises, which is of type PromiseReaction, which is a linked list, and those of you who forget can go back to that linked list picture; Set the promise’s parameter to value, which is the JavaScript parameter passed to resolve. Call promise.setStatus (PromiseState:: kdepressing) and set the state of the promise to depressing. Finally, call TriggerPromiseReactions to add the handler from These Reactions to the MicroTask queue.
TriggerPromiseReactions
The source code is as follows:
// https://tc39.es/ecma262/#sec-triggerpromisereactions
transitioning macro TriggerPromiseReactions(implicit context: Context)(
reactions: Zero|PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
// We need to reverse the {reactions} here, since we record them on the
// JSPromise in the reverse order.
let current = reactions;
let reversed: Zero|PromiseReaction = kZero;
// List inversion
while (true) {
typeswitch (current) {
case (Zero): {
break;
}
case (currentReaction: PromiseReaction): {
current = currentReaction.next;
currentReaction.next = reversed;
reversed = currentReaction;
}
}
}
current = reversed;
/ / the list after inversion, call MorphAndEnqueuePromiseReaction
// Put each item in the link into the MicroTask queue
while (true) {
typeswitch (current) {
case (Zero): {
break;
}
case (currentReaction: PromiseReaction): {
current = currentReaction.next;
MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType); }}}}Copy the code
TriggerPromiseReactions does two things:
- reverse
reactions
Linked list, the implementation of THEN method has been analyzed in the previous, then method parameters ultimately exist in the linked list. The last then method to be called, which receives wrapped arguments at the head of the list, does not conform to the specification, so it needs to be reversed - traverse
reactions
Object, call MorphAndEnqueuePromiseReaction put each element in microtask queue
MorphAndEnqueuePromiseReaction
MorphAndEnqueuePromiseReaction PromiseReaction into microtask, will eventually reach the insert microtask queue, morph itself in the sense of the shift/conversion, such as Polymorphism (Polymorphism).
MorphAndEnqueuePromiseReaction receive three parameters, PromiseReaction is mentioned packaging Promise handler chain table objects, argument is to resolve/reject parameters, ReactionType indicates the state of a Promise, which is gradually fulfilled and rejected which is promisereactionreject.
MorphAndEnqueuePromiseReaction logic is very simple, because already know the final status of Promise, at this time so you can get from promiseReaction object promiseReactionJobTask object, The variable name of the promiseReactionJobTask is consistent with the ECMAScript specification and is actually the legendary MicroTask. MorphAndEnqueuePromiseReaction source as follows, retain only the content related to this section.
transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(
promiseReaction: PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
let primaryHandler: Callable|Undefined;
let secondaryHandler: Callable|Undefined;
// Select different callbacks to execute based on different Promise states
if constexpr (reactionType == kPromiseReactionFulfill) {
primaryHandler = promiseReaction.fulfill_handler;
secondaryHandler = promiseReaction.reject_handler;
} else {
primaryHandler = promiseReaction.reject_handler;
secondaryHandler = promiseReaction.fulfill_handler;
}
const handlerContext: Context =
ExtractHandlerContext(primaryHandler, secondaryHandler);
if constexpr (reactionType == kPromiseReactionFulfill) {/ / fulfilled branch
* UnsafeConstCast(& promiseReaction.map) =
PromiseFulfillReactionJobTaskMapConstant(a);const promiseReactionJobTask =
UnsafeCast<PromiseFulfillReactionJobTask>(promiseReaction);
// Argument is reject
promiseReactionJobTask.argument = argument;
// Handler is the second argument to the JS layer then method, or catch method
promiseReactionJobTask.context = handlerContext;
// promiseReactionJobTask is that microtask that gets talked about over and over again at work
// EnqueueMicrotask inserts the microtask into the microtask queue
EnqueueMicrotask(handlerContext, promiseReactionJobTask);
/ / delete
} else {/ / rejected branch
// this is a big pity
* UnsafeConstCast(& promiseReaction.map) =
PromiseRejectReactionJobTaskMapConstant(a);const promiseReactionJobTask =
UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);
promiseReactionJobTask.argument = argument;
promiseReactionJobTask.context = handlerContext;
promiseReactionJobTask.handler = primaryHandler;
EnqueueMicrotask(handlerContext, promiseReactionJobTask); }}Copy the code
MorphAndEnqueuePromiseReaction function is very simple, is the state according to the Promise of selecting onFulfilled or onRejected microtask queue ready to perform. This is the depressing branch, so the one chosen is ondepressing.
const myPromise4 = new Promise((resolve, reject) = > {
setTimeout(_= > {
resolve('my code delay 1000')},1000)
})
myPromise4.then(result= > {
console.log('第 1 δΈͺ then')
})
myPromise4.then(result= > {
console.log('second then')})// Print order:
// 第 1 δΈͺ then
// then
// If you comment out the list inversion in TriggerPromiseReactions, print it in the following order
// then
// 第 1 δΈͺ then
Copy the code
summary
Resolve will only process pending promises, setting the Promise’s response to its incoming value, Moreover, the state of Promise will be modified as a pity.
Because promises use THEN to collect dependencies by putting the most recent dependencies in the head of the list, you need to reverse the list first and then place them one by one in a MicroTask queue for execution
The main job of Resolve is to iterate over the dependencies collected when the THEN method was called in the previous section and put them into a MicroTask queue for execution.
reject
Reject not so different from Reslove
ECMAScript specification
new Promise((resolve, reject) = > {
setTimeout(_= > reject('rejected'), 5000)
}).then(_= > {
console.log('fulfilled')},reason= > {
console.log(reason)
})
Copy the code
After 5s, reject is executed and the console prints Rejected.
RejectPromise
ECMAScript specification
The reject(season) function invokes V8’s RejectPromise(promise, season) function.
// https://tc39.es/ecma262/#sec-rejectpromise
transitioning builtin
RejectPromise(implicit context: Context)( promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
// If the current Promise does not bind a handler,
// The Runtime ::RejectPromise is called
if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() | |! promise.HasHandler()) {
return runtime::RejectPromise(promise, reason, debugEvent);
}
// Get the Promise's handler, PromiseReaction
const reactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// Reason is the reject argument
promise.reactions_or_result = reason;
// Set the state of the Promise to Rejected
promise.SetStatus(PromiseState::kRejected);
// Add all Promise handlers to the MicroTask queue
TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);
return Undefined;
}
Copy the code
HostPromiseRejectionTracker
In contrast to ReslovePromise, there is an additional judgment to determine whether Promsie is bound to a handler, Runtime ::RejectPromise(Promise, Reason, debugEvent) This is in fact is the ECMAScript standard of HostPromiseRejectionTracker (promise, “reject”), it is already the second mention HostPromiseRejectionTracker.
It is mentioned once in the Rejected branch of the PerformPromiseThenImpl function.
In ECMAScript standard HostPromiseRejectionTracker is an abstract method, he did not even clear implementation process, described his role in the specification.
HostPromiseRejectionTracker rejected for tracking Promise, for example, the global rejectionHandled event is implemented by it.
Note 1 HostPromiseRejectionTracker in both cases is called:
When a Promise is rejected without any handler, it is called and the second argument is passed “reject.”
When the first handler is bound to the Promise of the Rejected state, it is called and the second argument passes “handle”.
So here, passing “handle” marks the Promise as bound to a handler, while passing “reject” marks the Promise as not yet bound to a handler.
Let’s take a look at a couple of pieces of code to see it in action
JavaScript throws an error when we call a handler with a Reject Promise state and no onRejected binding
const myPromise1 = new Promise((resolve, reject) = > {
reject()
})
/ / an error
Copy the code
And checking whether or not to bind handlers is an asynchronous process
console.log(1);
const myPromise1 = new Promise((resolve, reject) = > {
reject()
})
console.log(2);
/ / 1
/ / 2
/ / an error
Copy the code
We can bind an onRejected handler to it to resolve our error
const myPromise1 = new Promise((resolve, reject) = > {
reject()
})// Get a Promise from the Rejected state
myPromise1.then(undefined.console.log)
Copy the code
You must be wondering when and how Promise checks if it is bound to the onRejected handler.
This is HostPromiseRejectionTracker role in ECMAScript also mentioned in the specification, when calling HostPromiseRejectionTracker (promise, ‘reject’), If promsie does not have a handler, a handler is set for it.
Back to the logic above, when a Promise’s reject function is called, runtime::RejectPromise is called to add a handler to it if the onRejected handler is not available. And then I’ll call TriggerPromiseReactions to add this handler to the MicroTask queue, The handler to do this is to detect whether the Promise was binding again new onRejected (that is, have carried out during this period HostPromiseRejectionTracker (Promise, ‘handle’)), if not throw an error, If so, nothing happens.
So in the Promise of a reject status call then method need to call the runtime: : PromiseRevokeReject (Promise) to represent the Promise binding new onRejected, Prevent errors from being thrown.
So you must bind handlers before the microTask executes to prevent this error from being thrown.
const myPromise1 = new Promise((resolve, reject) = > {
// Synchronize execution
reject()
// a check myPromise1 will be inserted into the MicroTask queue
// Whether the microtask of the new onRejected handler is bound
})
// macrotask
setTimeout(() = > {
// The microTask has been executed, the error has been thrown, and it is too late
myPromise1.then(undefined.console.log)
}, 0)
Copy the code
summary
Reject and resolve have the same logic in four steps:
- Set the reason, reject, argument for the Promise
- Set the Promise state: Rejected
- If the Promise does not have an onRejected handler, a handler is added to it that again checks if the Promise is bound to onRejected
- The dependencies collected from the previous then/catch calls, known as the promiseReaction object, get microtasks, which are inserted into the MicroTask queue
catch
new Promise((resolve, reject) = > {
setTimeout(reject, 2000)
}).catch(_= > {
console.log('rejected')})Copy the code
PromisePrototypeCatch
For example, when a catch method is invoked, V8’s PromisePrototypeCatch method is called:
transitioning javascript builtin
PromisePrototypeCatch( js-implicit context: Context, receiver: JSAny)(onRejected: JSAny): JSAny {
const nativeContext = LoadNativeContext(context);
return UnsafeCast<JSAny>(
InvokeThen(nativeContext, receiver, Undefined, onRejected));
}
Copy the code
The source code for PromisePrototypeCatch really only consists of these lines, and there is nothing more than invoking the InvokeThen method.
InvokeThen
Can infer from the name, InvokeThen call is then way to Promise, InvokeThen source code is as follows:
transitioning
macro InvokeThen<F: type>(implicit context: Context)(
nativeContext: NativeContext, receiver: JSAny, arg1: JSAny, arg2: JSAny,
callFunctor: F): JSAny {
if(! Is<Smi>(receiver) &&IsPromiseThenLookupChainIntact(
nativeContext, UnsafeCast<HeapObject>(receiver).map)) {
const then =
UnsafeCast<JSAny>(nativeContext[NativeContextSlot::PROMISE_THEN_INDEX]);
Call the then method and return it. Both branches are the same
return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);
} else
deferred {
const then = UnsafeCast<JSAny>(GetProperty(receiver, kThenString));
Call the then method and return it. Both branches are the same
return callFunctor.Call(nativeContext, then, receiver, arg1, arg2); }}Copy the code
The InvokeThen method has two if/else branches, which have similar logic. The JS sample code in this section follows the if branch. Get the V8 native THEN method, then Call the THEN method via CallFunctor. Call(nativeContext, then, Receiver, arg1, arg2). The then method was introduced before and will not be covered here.
Since the catch method calls the THEN method, the catch method returns the same value as the THEN method, and the catch method can continue to throw an exception, and the chain call can continue.
new Promise((resolve, reject) = > {
setTimeout(reject, 2000)
}).catch(_= > {
throw 'rejected'
}).catch(_= > {
console.log('last catch')})Copy the code
The code above catches the exception thrown by the first catch and prints the last catch.
summary
If obj is a Promise object, then obj. Catch (onRejected) equals obJ. Then (undefined, onRejected).
Chain calls to THEN and microTask queues
Promise.resolve('123')
.then(() = > {throw new Error('456')})
.then(_= > {
console.log('shouldnot be here')
})
.catch((e) = > console.log(e))
.then((data) = > console.log(data));
Copy the code
After the above code runs, print Error: 456 and undefined. For the sake of narrative, change the chained call writing of THEN to verbose writing.
const p0 = Promise.resolve('123')
const p1 = p0.then(() = > {throw new Error('456')})
const p2 = p1.then(_= > {
console.log('shouldnot be here')})const p3 = p2.catch((e) = > console.log(e))
const p4 = p3.then((data) = > console.log(data));
Copy the code
The then method returns a new Promise, so the five promises p0, P1, P2, P3, and P4 are not equal.
When a Promise is in the Rejected state, its state is passed down (the result of the then method). If the onRejected handler is not found, it is passed down until it is found.
The catch method binds to the onRejected function
The implementation of microtask
All synchronization code execution is completed, start executing microtask queue microtask execution, core method is MicrotaskQueueBuiltinsAssembler: : RunSingleMicrotask, Since there are many types of microtask, there are many branches of RunSingleMicrotask. I’m not going to list the code here.
PromiseReactionJob
In the process of executing microtask, MicrotaskQueueBuiltinsAssembler: : RunSingleMicrotask invokes PromiseReactionJob, source code is as follows:
transitioning
macro PromiseReactionJob(
context: Context, argument: JSAny, handler: Callable|Undefined,
promiseOrCapability: JSPromise|PromiseCapability|Undefined,
reactionType: constexpr PromiseReactionType): JSAny {
if (handler == Undefined) {
// The argument of a Promise is passed in without handling the case of the function
if constexpr (reactionType == kPromiseReactionFulfill) {
// Basically similar to JS layer resolve
return FuflfillPromiseReactionJob(
context, promiseOrCapability, argument, reactionType);
} else {
// Basically similar to reject in the JS layer
returnRejectPromiseReactionJob( context, promiseOrCapability, argument, reactionType); }}else {
try {
// Attempt to call the Promise handler, equivalent to handler(argument)
const result =
Call(context, UnsafeCast<Callable>(handler), Undefined, argument);
// Basically similar to JS layer resolve
return FuflfillPromiseReactionJob(
context, promiseOrCapability, result, reactionType);
} catch (e) {
// Basically similar to the REJECT handler, which throws an exception when executing the handler
returnRejectPromiseReactionJob( context, promiseOrCapability, e, reactionType); }}}Copy the code
PromiseReactionJob will determine whether the current task exists in the need to perform processing function, if does not exist, it will directly on a Promise of value as a parameter called FuflfillPromiseReactionJob, if there is to carry out the processing function, Will call FuflfillPromiseReactionJob execution results as parameters.
That is to say, as long as there is no exception thrown during the implementation of onFulfilled Promise or onFulfilled Promise, The Promise is executed FuflfillPromiseReactionJob amend the state to fulfilled. If an exception is thrown, run RejectPromiseReactionJob.
let p0 = new Promise((resolve, reject) = > {
reject(123)})// P1 is reject
let p1 = p0.then(value= > {
console.log(value);
}, reason= > {
console.log(reason);
return 2
})
// Add reason => {console.log(reason)} to the MicroTask queue
p1.then(_= > {
console.log('p1');
})
// Add PromiseReaction for p1
// Select the first microtask queue to execute,
// Handler for reason => {console.log(reason)},
/ / success handler, so call FuflfillPromiseReactionJob
// Execute p1 resolve
Copy the code
Note: FuflfillPromiseReactionJob do a lot of, the implementation of the resolve is just one of the branches
Let’s take a look at FuflfillPromiseReactionJob are doing specific things.
FuflfillPromiseReactionJob
The source code is as follows:
transitioning
macro FuflfillPromiseReactionJob(
context: Context,
promiseOrCapability: JSPromise|PromiseCapability|Undefined, result: JSAny,
reactionType: constexpr PromiseReactionType): JSAny {
typeswitch (promiseOrCapability) {
case (promise: JSPromise): {
ResolvePromise (resolve(result))
return ResolvePromise(context, promise, result);
}
case (Undefined): {
return Undefined;
}
case (capability: PromiseCapability): {
const resolve = UnsafeCast<Callable>(capability.resolve);
try {
return Call(context, resolve, Undefined, result);
} catch (e) {
return RejectPromiseReactionJob( context, promiseOrCapability, e, reactionType); }}}}Copy the code
FuflfillPromiseReactionJob has three branches, here is the first branch, call ResolvePromise, this method is very important, he is the Promise of the specification Resolve Functions provides, Its role is to synchronize the promsie generated by the result (value and state) of the current processing function to it. PromiseOrCapability.
In the example above, promiseOrCapability is P1 with a value of 2
ResolvePromise
This is a very important method that will be invoked almost every time a Promise’s state needs to become a pity, and its logic leads to many features that are not present in PromiseA+. I have removed the unimportant parts of the following code
// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning builtin
ResolvePromise(implicit context: Context)( promise: JSPromise, resolution: JSAny): JSAny {
/ / delete
let then: Object = Undefined;
try {
/ / call FulfillPromise
const heapResolution = UnsafeCast<HeapObject>(resolution);
const resolutionMap = heapResolution.map;
if (!IsJSReceiverMap(resolutionMap)) {
return FulfillPromise(promise, resolution);
}
/ / delete
const promisePrototype =
*NativeContextSlot(ContextSlot::PROMISE_PROTOTYPE_INDEX);
// Important: If resolution is a Promise object
if (resolutionMap.prototype == promisePrototype) {
then = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
static_assert(nativeContext == LoadNativeContext(context));
goto Enqueue;
}
goto Slow;
} label Slow deferred {
// If resolution is an object containing the then attribute, it will come here
try {
// Get the then attribute
then = GetProperty(resolution, kThenString);
} catch (e) {
return RejectPromise(promise, e, False);
}
// If the then attribute is not an executable method
if(! Is<Callable>(then)) {// Synchronize the execution result to the promise
return FulfillPromise(promise, resolution);
}
goto Enqueue;
} label Enqueue {
// Important: If the result is a Promise object
// Or objects that contain executable THEN methods will come here
const task = NewPromiseResolveThenableJobTask(
promise, UnsafeCast<JSReceiver>(resolution),
UnsafeCast<Callable>(then));
return EnqueueMicrotask(task.context, task); }}Copy the code
One is to call FulfillPromise, which is described in resolve, to modify the state of promise to fulfilled and set its value, The promise handler is then pushed onto the microtask queue.
let p0 = Promise.resolve()
let p1 = p0.then(() = > {
return 1;
})
p1.then(console.log)
// P0 then ondepressing will enter the queue
// Call ondepressing P0 in the PromiseReactionJob, which will result in 1
/ / call FuflfillPromiseReactionJob, then call ResolvePromise
ResolvePromise does the following
// This will be depressing, and p1's processing function console.log will be added to the queue with parameter 1
// The onFulfilled queue of P1 will be fulfilled, which will output 1
Copy the code
Another case is when resolution is a Promise object or an object that contains a then method. Invokes the NewPromiseResolveThenableJobTask generated a microtask, then add it to microtask queue.
let p0 = Promise.resolve()
// Two special cases
let p1 = p0.then(() = > {
return Promise.resolve(1);// Return a Promise object
})
let p2 = p0.then(() = > {
return {then(resolve, reject){resolve(1)};The return value contains the then method
})
p1.then(console.log)
Copy the code
NewPromiseResolveThenableJobTask
NewPromiseResolveThenableJobTask call then methods of resolution, is the purpose of synchronization state in the callback function to promise. This may not be easy to understand, but when I convert it to JS it looks something like this.
microtask(() = > {
resolution.then((value) = > {
ReslovePromise(promise, value)
})
})
Copy the code
Resolution. Then is called in this task and then synchronized to promsie. But this whole process needs to be queued up for execution by a MicroTask, and when that task runs, if resolution is also a Promise, Then (value) => {ReslovePromise(promise, value)} will be added to the MicroTask queue as a microTask waiting to run.
You may wonder why? Then ((value) => {ReslovePromise(promise, value)})) instead of encapsulating it as a microtask. I was confused at first, but the specification gives a reason.
Note: This job uses the provided Thenable and its then methods to resolve a given Promise. This process must be done as a job to ensure that the THEN methods are evaluated after the evaluation of any surrounding code is complete.
Led to the ECMAScript NewPromiseResolveThenableJobTask specification (translation)
What is Thenable:
A concept created by Javascript to recognize promises is simply that all objects that contain then methods are thenable.
What does “to ensure that the THEN method is evaluated after evaluation of any surrounding code is complete” mean? The only thing I can think of is this.
const p1 = new Promise((resolve, reject) = > {
const p2 = Promise.resolve().then(() = > {
resolve({
then: (resolve, reject) = > resolve(1)});const p3 = Promise.resolve().then(() = > console.log(2));
});
}).then(v= > console.log(v));
1 / / 2
Copy the code
The ondepressing callback of P2 above will first enter the MicroTask queue and wait for its execution to call P1’s resolve, but the parameter is an object containing the THEN method. Then P1 will not immediately change to a depressing, but create a microtask to perform the THEN method, and then add P2’s ondepressing to the microTask queue. Then there are two Microtasks in the microTask queue. One is to execute the THEN function in the resolve return value, and the other is the ondepressing function of P3.
Then the first microtask is taken out and executed (after the removal, only P3 ondepressing is left in the microtask queue). After the execution, the state of P1 will become fulfilled, and then THE ONdepressing of P1 will enter the queue. Later, it can be imagined that 2 and 1 will be output successively (because the ondepressing function of P1 enters the microtask queue after the ondepressing function of P3).
If there is no NewPromiseResolveThenableJobTask as a microtask. When the callback in THE THEN parameter is executed, the THEN method in the resolve parameter will be synchronously triggered. The depressing state will be immediately synchronized to P1, and then the ONdepressing of P1 will enter microTask first, resulting in the result becoming 12. The result can be confusing for JavaScript developers.
So ECMAScript executes it as an asynchronous task.
Returning the Promsie object to produce two microtasks seems even more confusing.
RejectPromiseReactionJob
In the PromiseReactionJob case, if the handler throws an exception, the RejectPromiseReactionJob is executed
let p0 = Promise.resolve()
let p1 = p0.then(() = > {
throw 'error'; // Handler failed to execute
})
Copy the code
This will call RejectPromiseReactionJob, the source code is as follows
macro RejectPromiseReactionJob(
context: Context,
promiseOrCapability: JSPromise|PromiseCapability|Undefined, reason: JSAny,
reactionType: constexpr PromiseReactionType): JSAny {
if constexpr (reactionType == kPromiseReactionReject) {
typeswitch (promiseOrCapability) {
case (promise: JSPromise): {
// promiseOrCapability is p1, a Promise object
// Execute the RejectPromise, calling P1's reject method
return RejectPromise(promise, reason, False);
}
case (Undefined): {
return Undefined;
}
case (capability: PromiseCapability): {
const reject = UnsafeCast<Callable>(capability.reject);
return Call(context, reject, Undefined, reason); }}}else {
StaticAssert(reactionType == kPromiseReactionFulfill);
return PromiseRejectReactionJob(reason, Undefined, promiseOrCapability); }}Copy the code
RejectPromiseReactionJob and FuflfillPromiseReactionJob are similar, it is call RejectPromise to invoke Promsie reject method, This was introduced in the Reject section above.
Handler of PromiseReactionJob == Undefined branch
There is also a branch in PromiseReactionJob where handler == Undefined, which is used when the handler in a task is Undefined
transitioning
macro PromiseReactionJob(
context: Context, argument: JSAny, handler: Callable|Undefined,
promiseOrCapability: JSPromise|PromiseCapability|Undefined,
reactionType: constexpr PromiseReactionType): JSAny {
if (handler == Undefined) {
// The argument of a Promise is passed in without handling the case of the function
if constexpr (reactionType == kPromiseReactionFulfill) {
// Basically similar to JS layer resolve
return FuflfillPromiseReactionJob(
context, promiseOrCapability, argument, reactionType);
} else {
// Basically similar to reject in the JS layer
return RejectPromiseReactionJob( context, promiseOrCapability, argument, reactionType); }}else {
/ / delete}}Copy the code
After entering the branch, the value and state of the previous Promise object will be directly synchronized to the current Promise. Let’s learn about it through a section of JS
let p0 = new Promise((resolve, reject) = > {
reject(123)})// P0 is in the rejected state
let p1 = p0.then(_= > {console.log('p0 onFulfilled')})
// P0's onRejected enters the MicroTask queue as a handler
// But since then does not pass the second argument
// So onRejected is undefined, so handler is undefined
let p2 = p1.then(_= > {console.log('p1 onFulfilled')})
/* PromiseReaction{console.log('p1 ondepressing ')}, onRejected:undefined} */
let p3 = p2.then(_= > {console.log('p2 onFulfilled')}, _= > {console.log('p2 onRejected')})
/* PromiseReaction{console.log('p2 ondepressing ')}, onRejected:_ => {console.log('p2 onRejected') } */
let p4 = p3.then(_= > {console.log('p3 onFulfilled')}, _= > {console.log('p3 onRejected')})
/* PromiseReaction{console.log('p3 ondepressing ')}, onRejected:_ => {console.log('p3 onRejected') } */
//p2 onRejected
//p3 onFulfilled
Copy the code
After the synchronization code is executed (the execution process is similar to the note), the microTask is enabled, and only one handler is undefined in the MicroTask queue. Enter the handler of PromiseReactionJob == Undefined branch.
When the state is rejected, run RejectPromiseReactionJob(context, promiseOrCapability, argument, reactionType), The parameter is “p1”, “argument” is “123”, and “reject” is reactionType.
After this reaction, the state of p0 will become parameter, when reactionType is rejected, and parameter p1 will become argument.
And then implement P1’s reject function (FulfillPromise(p1, 123)), / / assign onRejected (undefined) to the microTask queue as the handler in the PromiseReaction list bound to P1
/ / Synchronize the status rejected (123) to p2 (……….). / / synchronize the status rejected (123) to P2 (……….). / / synchronize the status rejected (123) to P2 (……….)
/ / handler == undefined; / / handler == undefined; / / microTask == undefined; / / microTask == undefined; / / MicroTask == undefined; / / MicroTask == undefined; / / MicroTask == undefined; This will be a pity state. This will be a pity state. Perform onRejected(123) and then set the result to value of P3.
Output the p2 onRejected
This will be the same thing, but the handler will be onFulfilled.
Output p3 onFulfilled
If you can read this, I think you can read this code as well
Promise.resolve('123')
.then(() = > {throw new Error('456')})
.then(_= > {
console.log('shouldnot be here')
})
.catch((e) = > console.log(e))
.then((data) = > console.log(data));
Copy the code
Catch (onRejected) = then(undefined)
This is the Promise rejected pass mechanism, passing down until you meet the onRejected handler
A few tough questions for Promise
Promise.resolve().then(() = > {
console.log(0);
return Promise.resolve(4);
}).then((res) = > {
console.log(res)
})
Promise.resolve().then(() = > {
console.log(1);
}).then(() = > {
console.log(2);
}).then(() = > {
console.log(3);
}).then(() = > {
console.log(5);
}).then(() = > {
console.log(6);
})
// 0 1 2 3 5 6
Copy the code
Consider what happens when a Promise’s value is a PROMsie object, starting at the end of this article’s **then chained calls and microTask queues **> ResolvePromise directory
Key words: thenable, NewPromiseResolveThenableJobTask
Promise.resolve().then(() = > {
console.log(0);
return {then(resolve){resolve(4)}};
}).then((res) = > {
console.log(res)
})
Promise.resolve().then(() = > {
console.log(1);
}).then(() = > {
console.log(2);
}).then(() = > {
console.log(3);
}).then(() = > {
console.log(5);
}).then(() = > {
console.log(6);
})
// 0 1 2 3 5 6
Copy the code
If a Promise value is an object that contains the then method, then the Promise value is an object that contains the then method
Key words: thenable, NewPromiseResolveThenableJobTask
const p1 = new Promise((resolve, reject) = > {
reject(0)})console.log(1);
setTimeout(() = > {
p1.then(undefined.console.log)
}, 0)
console.log(2);
/ / 1
/ / 2
/ / output error UnhandledPromiseRejection: This error originated either
const p1 = new Promise((resolve, reject) = > {
reject(0)})console.log(1);
p1.then(undefined.console.log)
console.log(2);
/ / 1
/ / 2
/ / 0
Copy the code
Why does the first method report an error?
Expedition is the specification of HostPromiseRejectionTracker, when a no binding processing functions are called the Promsie reject will create a small task to detect the Promise again whether there is a handler, if did not exist at this time the output error, The setTimeout callback executes after the microtask.
This paper reject > HostPromiseRejectionTracker directory in detail.
A few last things
-
If you have any questions about this article please leave a comment
-
If you review this article and still can’t get an answer to any of the above questions, please let me know in the comments section and I’ll explain how to do it later
-
If you have any Promise questions to comment on, the author contracts all Promise questions for free
-
If you saw this and haven’t liked it yet, please like it, thank you very much.
A link to the
Promise V8 source code analysis (a) — Xu Pengyue
What happens after promise. Then return Promise. Resolve?
Chromium Code Search
ECMA-262, 11th edition, June 2020