In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-17 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article introduces you how to achieve the Promise/A+ specification, the content is very detailed, interested friends can refer to, hope to be helpful to you.
For a time, I thought I knew a lot about Promise, but when I tried to implement the Promise/A+ specification some time ago, I found that my understanding of Promise was too shallow. In the process of writing specific code implementation according to the Promise/A+ specification, I have experienced a roller coaster cognitive change from "knowing very well" to "unfamiliar" to "understanding", and have a deeper understanding of Promise!
TL;DR: since many people don't want to read long articles, here is the Javascript implementation of the Promise/A+ specification that I wrote.
Github Warehouse: promises-aplus-robin [1] (it would be better to click star)
Source code [2]
Source code annotated version [3]
The promises-tests test cases are all passed.
Promise comes from the real world.
The literal translation of Promise is a promise, and the latest Red Treasure Book has been translated for a period of time. Of course, it doesn't matter, programmers only need a look to understand.
Make a promise
As migrant workers, we will inevitably receive all kinds of cakes, such as flattering cakes, appreciation and salary increases cakes, equity incentive cakes.
Some cakes are cashed immediately, such as verbal praise, because it does not bring any cost to the enterprise itself; some cakes are related to the actual interests of the enterprise, and they may be expected in the future, or they may die without a disease, or directly declare the failure of cake painting.
For Javascript, the action of drawing a cake is to create a Promise instance:
Const bing = new Promise ((resolve, reject) = > {/ / wish all your cakes a complete success if ('cake painting success') {resolve ('everybody happy')} else {reject (' sharing difficulties')})
Like these pancakes, Promise is divided into three states:
Pending: the cake has been drawn, wait for it to be realized.
Fulfilled: the cake has really come true, reaching the pinnacle of life.
Rejected: sorry, failed to draw cakes, emmm...
Subscription commitment
If someone draws a cake, someone will pick it up. The so-called "pick up the cake" is to imagine the possibility of this cake. If the cake is really realized, I will put the villa by the sea; if the cake fails, the wage earners will wash their faces with tears.
Convert to the concept in Promise, this is a subscription model, we have to subscribe to and respond to both successes and failures. Subscriptions are implemented through methods such as then,catch.
/ subscribe to bing.then through the then method (/ / respond to successful cake painting success = > {console.log ('villa near the sea')}, / / respond to cake painting failure fail = > {console.log ('face with tears.')
Chain propagation
As we all know, the boss can draw cakes for senior executives or leaders, and when leaders take the cakes drawn by the boss, they must also continue to draw cakes for the lower employees, so that the workers can keep on drawing cakes, so that everyone's cakes can be cashed.
This top-down behavior coincides with the chain call of Promise.
BossBing.then (success = > {/ / leader takes over the cake from boss and continues to send the cake below return leaderBing}). Then (success = > {console.log ('the cake painted by leader is really realized, the villa is near the sea')}, fail = > {console.log (the cake painted by leader is fried and washed with tears.)
Overall, Promise's promises are similar to those of the real world.
However, there are many details in the implementation of Promise, such as the details of asynchronous processing, Resolution algorithm, and so on, which will be discussed later. Below, I will start with my first impression of Promise, then transition to the understanding of macro and micro tasks, and finally unravel the mystery of the Promise/A+ specification.
First acquaintance of Promise
I still remember that when I first came into contact with Promise, I felt it was "awesome" to be able to encapsulate the ajax process. At that time, the impression of Promise was probably: elegant asynchronous encapsulation, no longer need to write high-coupling callback.
Here is a simple ajax package temporarily handmailed as an example:
Function isObject (val) {return Object.prototype.toString.call (val) = ='[object Object]';} function serialize (params) {let result =''; if (isObject (params)) {Object.keys (params). ForEach ((key) = > {let val = encodeURIComponent (params [key]); result + = `$ {key} = ${val} & `;} return result } const defaultHeaders = {"Content-Type": "application/x-www-form-urlencoded"} / / ajax simple package function request (options) {return new Promise ((resolve, reject) = > {const {method, url, params, headers} = options const xhr = new XMLHttpRequest () If (method = = 'GET' | | method = =' DELETE') {/ / GET and DELETE generally use querystring to pass parameters const requestURL = url +'?'+ serialize (params) xhr.open (method, requestURL, true);} else {xhr.open (method, url, true) } / / set request header const mergedHeaders = Object.assign ({}, defaultHeaders, headers) Object.keys (mergedHeaders) .forEach (key = > {xhr.setRequestHeader (key, mergedHeaders [key])) }) / / status monitoring xhr.onreadystatechange = function () {if (xhr.readyState = 4) {if (xhr.status = 4) {resolve (xhr.response)} else {reject (xhr.status)} xhr.onerror = function (e) {reject (e) } / / processing body data Send the request const data = method = 'POST' | | method = =' PUT'? Serialize (params): null xhr.send (data);} const options = {method: 'GET', url:' / user/page', params: {pageNo: 1, pageSize: 10}} / / call interface request (options) through Promise (res = > {/ / request succeeded}, fail = > {/ / request failed})
The above code encapsulates the main process of ajax, while many other details and scenarios cannot be covered by dozens of lines of code. However, we can see that the core of Promise encapsulation is:
Encapsulate a function that wraps the code containing the asynchronous process in the executor that constructs the Promise. The encapsulated function finally requires the Promise instance of return.
Promise has three states, Pending, Fulfilled, and Rejected. Resolve () and reject () are triggers for state transitions.
Determine the conditions for the state transition. In this example, we think that when the ajax responds and the status code is 200, the request succeeds (execute resolve ()), otherwise the request fails (execute reject ()).
Ps: in the actual business, in addition to judging the HTTP status code, we will also judge the internal error code (the status code agreed upon by the front and back end in the business system).
In fact, now that we have solutions like axios, we don't easily choose to package ajax ourselves, and we don't encourage the repetition of such basic and important wheels, not to mention some scenarios that are often difficult for us to think through. Of course, if time permits, you can learn its source code implementation.
Macro task and micro task
To understand the Promise/A+ specification, we must first trace the source. Promise is closely related to micro tasks, so it is necessary for us to have a basic understanding of macro tasks and micro tasks.
For a long time, I didn't pay much attention to macro tasks (Task) and micro tasks (Microtask). There was even a time when I thought setTimeout (fn, 0) was very useful when manipulating dynamically generated DOM elements, but I didn't know the principle behind it, which was essentially closely related to Task.
Var button = document.createElement ('button'); button.innerText =' add input box 'document.body.append (button) button.onmousedown = function () {var input = document.createElement (' input'); document.body.appendChild (input); setTimeout (function () {input.focus ();}, 0)}
If you do not use setTimeout 0focus (), it will have no effect.
So, what are macro tasks and micro tasks? Let's take our time to uncover the answer.
Modern browsers adopt a multi-process architecture, which can be found in Inside look at modern web browser [4]. And the most closely related to our front end is that the Renderer Process,Javascript runs in the Main Thread of Renderer Process.
Renderer: Controls anything inside of the tab where a website is displayed.
The rendering process controls everything about the web page displayed in the Tab page. It can be understood that the rendering process is designed to serve a specific web page.
We know that Javascript can interact directly with the interface. Imagine that if Javascript uses a multithreaded strategy and each thread can operate on DOM, who will be the final interface rendering? This is obviously contradictory. Therefore, an important reason why Javascript chooses to use the single-threaded model is to ensure the strong consistency of the user interface.
In order to ensure the consistency and smoothness of the interface interaction, Javascript execution and page rendering will be performed alternately in Main Thread (for performance reasons, the browser determines that there is no need to perform interface rendering and will skip the rendering step). At present, the screen refresh rate of most devices is 60 times per second, and one frame is about 16.67ms. In the cycle of this frame, it is necessary to complete not only the execution of Javascript, but also the rendering (if necessary) of the interface, making use of the residual shadow effect of the human eye to make users feel that the interface interaction is very smooth.
Look at the basic process of 1 frame with a picture, quoted from https://aerotwist.com/blog/the-anatomy-of-a-frame/
PS:requestIdleCallback is an idle callback, and requestIdleCallback is called at the end of 1 frame if there is time left. Be careful not to modify DOM in requestIdleCallback, or read layout information to trigger Forced Synchronized Layout, otherwise it will cause performance and experience problems. See Using requestIdleCallback [5] for details.
We know that a Render Process in a web page has only one Main Thread. In essence, Javascript tasks are executed sequentially during the execution phase, but when the JS engine parses Javascript code, it divides the code into synchronous tasks and asynchronous tasks. Synchronous tasks go directly to Main Thread execution; asynchronous tasks enter the task queue and are associated with an asynchronous callback.
In a web app, we will write some Javascript code or reference some scripts to initialize the application. In this initial code, the synchronization code is executed sequentially. During the execution of these synchronous codes, some events will be monitored or some asynchronous API will be registered (network related, IO related, etc.) These event handlers and callbacks are asynchronous tasks, which are queued and processed in the following Event Loop
Asynchronous tasks are divided into Task and Microtask, each with separate data structure and memory to maintain.
Feel it with a simple example:
Var a = 1; console.log ('a res, a) var b = 2; console.log ('b) setTimeout (function task1 () {console.log ('task1:', 5) Promise.resolve (6). Then (function microtask2 (res) {console.log (' microtask2:', res)})}, 0) Promise.resolve (4). Then (function microtask1 (res) {console.log ('microtask1:', res)}) var b = 3 Console.log ('c _ v _ v, c)
After the above code is executed, it is output in the console in turn:
A: 1 b: 2 c: 3 microtask1: 4 task1: 5 microtask2: 6
It is not difficult to take a closer look, but it is still necessary to explore the details behind this. We might as well ask ourselves a few questions and take a look at them together.
What do Task and Microtask have?
Tasks:
SetTimeout
SetInterval
MessageChannel
API related to iPax 0 (file, network)
DOM event snooping: browser environment
SetImmediate:Node environment, IE also seems to support (see caniuse data)
Microtasks:
RequestAnimationFrame: browser environment
MutationObserver: browser environment
Promise.prototype.then, Promise.prototype.catch, Promise.prototype.finally
Process.nextTick:Node environment
QueueMicrotask
Is requestAnimationFrame a micro task?
RequestAnimationFrame, referred to as rAF for short, is often used to make animation effects, because its callback function is executed at the same frequency as the browser screen refresh frequency, that is, we usually say that it can achieve the effect of 60FPS. Before rAF was widely used, we often used setTimeout to deal with animation. However, when the main thread is busy, setTimeout may not be scheduled in time, which leads to stutter.
So is rAF a macro task or a micro task? In fact, many websites do not give a definition, including the description on MDN is very simple.
We might as well ask ourselves, is rAF a macro task? I thought for a moment, obviously not, rAF can be used instead of timer animation, how can it be scheduled by Event Loop like timer tasks?
I asked myself again, is rAF a micro task? the timing of rAF is before the next browser redrawing, which seems to be about the same time as the micro task, which once made me think that rAF is a micro task, but in fact rAF is not a micro task. Why do you say that? Please run this code.
Function recursionRaf () {requestAnimationFrame (()) = > {console.log ('raf callback') recursionRaf ()} recursionRaf ()
You will find that in the case of infinite recursion, the rAF callback executes normally, and the browser can interact properly without blocking.
If rAF were a micro task, there would be no such treatment. If you don't believe it, you can turn to the following section, "what if Microtask creates a Microtask when it is executed?"
Therefore, the task level of rAF is very high, with separate queue maintenance. In the browser 1 frame cycle, rAF and Javascript are executed, and the browser redrawing is the same Level. (in fact, you can also see it in the previous picture of "dissection 1 frame".)
Task and Microtask each have 1 queue?
At first, I thought that since browsers distinguish between Task and Microtask, I just need to schedule a queue to store the task. In fact, Task arranges separate queues depending on the task source. For example, Dom events belong to Task, but there are many types of Dom events. In order to facilitate user agent to subdivide Task and finely prioritize different types of Task, and even do some optimization work, there must be a task source to distinguish. Similarly, Microtask has its own microtask task source.
For a specific explanation, see a paragraph in the HTML standard:
Essentially, task sources are used within standards to separate logically-different types of tasks, which a user agent might wish to distinguish between. Task queues * are used by user agents to coalesce task sources within a given event loop.
What is the consumption mechanism of Task and Microtask?
An event loop has one or more task queues. A task queue is a set of tasks.
Javascript is event-driven, so Event Loop is the core of asynchronous task scheduling. Although we always talk about task queues, Tasks is not a Queue but a Set in terms of data structure. In each round of Event Loop, the Task of the first runnable (the first executable Task, not necessarily the first Task in sequence) is fetched into Main Thread execution, and then the Microtask queue is checked and all the Microtask in the queue is executed.
No matter how much you say, it is not as intuitive as a picture, please see!
When will Task and Microtask enter the appropriate queue?
In retrospect, we have been talking about the concept of "asynchronous tasks entering the queue", so there is a question: when did Task and Microtask enter the corresponding queue? Let's get this straight again. Asynchronous tasks have three key behaviors: registration, queuing, and callback execution. The registration is easy to understand, indicating that the task has been created, while the callback being executed means that the task has been picked up and executed by the main thread. However, in the behavior of queuing, macro tasks and micro tasks behave differently.
Macro tasks are queued
For Task, the task will enter the queue when it is registered, but the status of the task is not runnable yet, so it does not have the condition to be picked up by Event Loop.
Let's first take the Dom event as an example.
Document.body.addEventListener ('click', function (e) {console.log (' clicked', e)})
When the addEventListener line of code is executed, the task is registered, indicating that a user clicks on the Task associated with the event to enter the task queue. So when will this macro task become runnable? Of course, after the user clicks on the Main Thread that occurs and the signal is passed to the browser Render Process, the macro task becomes runnable before it can be picked up by Event Loop and executed in Main Thread.
Here's another example, by the way, to explain why setTimeout 0 has a delay.
SetTimeout (function () {console.log ('I am a setTimeout registered macro task')}, 0)
When the setTimeout line of code is executed, the corresponding macro task is registered, and Main Thread tells the timer thread, "you give me a message after 0 milliseconds." The timer thread receives the message and finds that it only needs to wait for 0 milliseconds and immediately sends a message to Main Thread, "it's been 0 milliseconds on my side." After receiving this reply message, Main Thread sets the status of the corresponding macro task to runnable, and the macro task can be picked up by Event Loop.
As you can see, after such a process of inter-thread communication, even with a timer with a delay of 0 milliseconds, the callback is not executed after 0 milliseconds, because the communication process takes time. There is a view on the Internet that the response time of setTimeout 0 is at least 4ms. In fact, there is a basis, but it is also conditional.
HTML Living Standard: If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
For this statement, I think I just have an idea. Different browsers must have different details in implementing the specification, and the specific communication process is unknown, and it is hard to say whether it is 4ms or not. the key is whether you have figured out what has been experienced behind it.
Micro tasks are queued
We mentioned a point earlier that after executing a Task, if the Microtask queue is not empty, all the Microtask in the Microtask queue will be taken out for execution. In my opinion, Microtask does not enter the Microtask queue when it registers, because Event Loop does not determine the status of the Microtask when it processes the Microtask queue. On the other hand, if Microtask enters the Microtask queue when registering, it is obviously unreasonable that the Microtask will be executed before it becomes runnable. My point is that Microtask only enters the Microtask queue when it becomes runnable.
So let's analyze when Microtask becomes runnable. Let's first take a look at Promise.
Var promise1 = new Promise ((resolve, reject) = > {resolve (1);}) promise1.then (res = > {console.log ('roome1 micro task is executed')})
Readers, my first question is, when will Promise's microtasks be registered? when is new Promise? Or when? You might as well guess!
The answer is when .then is executed. (of course, there is also the case of. Catch, which is just an example.)
So when does the state of the promise microtask become runnable? I believe many readers already have a clue, yes, that is, when the Promise state is transferred, in this case, when resolve (1) is executed, the Promise state is transferred from pending to fulfilled. After the execution of resolve (1), the promise micro task is queued for Microtask and will be executed in this Event Loop.
Based on this example, let's make it more difficult.
Var promise1 = new Promise ((resolve, reject) = > {setTimeout (() = > {resolve (1);}, 0);}); promise1.then (res = > {console.log ('roome1 micro task is executed');})
In this example, the registration and queuing of the promise microtask are not at the same Event Loop. It's hard to say? In the first Event Loop, micro tasks are registered through .then, but we can find that when new Promise, a setTimeout is executed, which is equivalent to registering a macro task. Resolve (1) must be executed only when the macro task is executed. Obviously, there is at least one Event Loop between the two.
If you can analyze the process of the promise micro-task, you will naturally know how to analyze the process of the ObserverMutationmicro-task. I will not repeat it here.
What if the Microtask is created when Microtask executes?
We know that Event Loop executes at most one Task of runnable at a time, but executes all Microtask in the Microtask queue. If a new Microtask is created when the Microtask is executed, will the new Microtask be executed in the next Event Loop? The answer is no. Microtasks can add new microtasks to the queue and execute all microtasks before the next task starts and before the current Event Loop ends. Be careful not to create microtasks recursively, or you will fall into an endless loop.
Here is a bad example.
/ / bad case function recursionMicrotask () {Promise.resolve () .then () = > {recursionMicrotask ()}} recursionMicrotask ()
Please don't try it easily, or the page will get stuck. (because Microtask does not release Main Thread, browser rendering cannot be carried out.)
Why distinguish between Task and Microtask?
This is a very important question. Why not perform browser rendering directly after Task, and add the Microtask step instead? In fact, it has already been answered in the previous question. Event Loop consumes only one macro task at a time, and the micro task queue has a "continue to get on the car" mechanism when it is consumed, which gives developers more imagination and more control over the code.
How many questions to warm up?
Before hitting the Promise/A+ specification, you might as well use a few exercises to test your understanding of Promise.
Basic operation
Function mutationCallback (mutationRecords, observer) {console.log ('mt1')} const observer = new MutationObserver (mutationCallback) observer.observe (document.body, {attributes: true}) Promise.resolve (). Then () = > {console.log (' mt2') setTimeout () = > {console.log ('t1')}, 0) document.body.setAttribute (' test') "a")}) .then (() = > {console.log ('mt3')}) setTimeout (() = > {console.log (' t2')}, 0)
This question will not be analyzed, the answer: mt2 mt1 mt3 T2 T1
Browsers don't talk about martial arts?
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);})
This problem is said to be a problem coming out of the byte, and to be honest, I was confused when I first saw it. After my test in Chrome, the answer is really regular, that is: 0 1 2 3 4 5 6.
First output 0, and then output 1, I can still understand, why output 2 and 3 and then suddenly jump to 4, browser you do not talk about martial arts ah!
Emm... I was wearing a mask of pain!
So what is the order of execution behind this? After careful analysis, you will find that there are still traces to follow.
The old rule, the first question, how many micro-tasks have been generated during the code execution of this problem? Many people may think it is seven, but in fact it should be eight.
Number registration opportunity asynchronous callback mt1.then () console.log (0); return Promise.resolve (4); mt2.then (res) console.log (res) mt3.then () console.log (1); mt4.then () console.log (2); mt5.then () console.log (3); mt6.then () console.log (5); mt7.then () console.log (6) After mt8return Promise.resolve (4) executes and execution context stack is cleared, implicit callbacks (not reflected in the code) are implicitly registered in order to make mt2 runnable state
Synchronous task execution, registering mt1~ mt7 seven microtasks, when execution context stack is empty, and the status of mt1 and mt3 becomes runnable. The JS engine places mt1 and mt3 into the Microtask queue (via HostEnqueuePromiseJob).
Perform a microtask checkpoint. Since mt1 and mt3 become runnable in the same JS call, the callbacks of mt1 and mt3 go into execution context stack successively.
The mt1 callback goes into execution context stack execution, outputs 0, and returns Promise.resolve (4). Mt1 is dequeued. Since the mt1 callback returns a Promise with a status of fulfilled, the JS engine will then arrange a job (job is the concept of ecma, which is equivalent to the concept of micro-task, which is numbered mt8 first). The purpose of the callback is to change the status of mt2 to fulfilled (provided that the current execution context stack is empty is used). So immediately after that, execute the callback of mt3.
The mt3 callback enters the execution context stack execution, and the output of 1j mt4 becomes runnable, and the execution context stack is empty,mt3 is out of queue.
Since the mt4 is already in the runnable state, the JS engine will queue the mt4, and then the JS engine will queue the mt8.
Next, the mt4 callback goes into execution context stack execution, and the output 2j mt5 becomes runnable,mt4 out-of-queue. The JS engine arranges mt5 to enter the Microtask queue.
The mt8 callback is executed to make mt2 become runnable and mt8 dequeued. Mt2 is queued.
The mt5 callback is executed, and the output 3 _ mt6 becomes runnable,mt5 out of queue. Mt6 is queued.
The mt2 callback is executed, and the output is 4 min mt4 out of the queue.
The mt6 callback is executed, and the output 5pm mt7 becomes runnable,mt6 out of queue. Mt7 is queued.
The mt7 callback is executed, and the output is 6 min mt7 out of the queue. Execution complete! On the whole, the output result is 0.123456 in turn.
Friends who still have doubts about this implementation process can first take a look at the agreement on Promise in the Promise/A+ specification and ECMAScript262 specification, and then think about it back. You are also welcome to leave a message to communicate with me!
After my test in Edge browser, the result is: 0 1 2 4 3 5 6. As you can see, different browsers are consistent in the main process of implementing Promise, but there are still inconsistencies in some details. In practical application, we only need to pay attention to avoid this problem.
Implement Promise/A+
After the warm-up, the next step is to face the big boss Promise/A+ specification [6]. The Promise/A+ specification lists more than 30 detailed rules, large and small, and it is quite dizzy at first glance.
After carefully reading the specification many times, I have a basic understanding that the key to implementing the Promise/A+ specification is to sort out a few core points.
Relational link
I was a little tired after writing thousands of words, so I thought that the last part would come to a quick end with words, but in the middle of the last section, I felt that I could not write any more. The pure text was too dry to absorb, which is quite unfriendly to those readers who do not have a good grasp of Promise. So, I think it's better to use a diagram to describe the relationship link of Promise.
First of all, Promise is an object, while the Promise/A+ specification revolves around Promise's prototype method. Then ().
The particularity of .then () is that it returns a new Promise instance, and in the case of this continuous call to .then (), a Promise chain is strung together, which has some similarities with the prototype chain. "shamelessly" recommend another article "mind Mapping Front end" 6k to understand Javascript objects, prototypes, inheritance [7], .
Another flexibility is that the state transition of the new Promise instance p2 returned by p1.then (onFulfilled, onRejected) occurs after the state transition of p1 occurs (after here refers to asynchronous). Moreover, whether the state transition of p2 is Fulfilled or Rejected depends on the return value of onFulfilled or onRejected, and there is a more complex analysis process, that is, the Promise Resolution Procedure algorithm described later.
I have drawn a simple timing diagram here, the drawing level is very poor, just to give readers a basic impression.
There are many details are not mentioned (because there are too many details, all drawn out is quite complex, the specific process, please see the source code attached at the end of my article).
NextTick
After reading the previous content, I believe everyone has an idea that a microtask is an asynchronous task, and if we want to implement the whole asynchronous mechanism of Promise, we must have the ability to simulate the asynchronous callback of microtasks. Such a message is also mentioned in the specification:
This can be implemented with either a "macro-task" mechanism such as setTimeout or setImmediate, or with a "micro-task" mechanism such as MutationObserver or process.nextTick.
What I choose here is to use micro tasks to implement asynchronous callbacks. If we use macro tasks to implement asynchronous callbacks, macro tasks may be interspersed with macro tasks during the execution of the promise micro task queue, which is not quite in line with the scheduling logic of the micro task queue. There is also compatibility between the Node environment and the browser environment. Process.nextTick callbacks can be used to simulate the execution of micro tasks in the Node environment, while we can choose MutationObserver in the browser environment.
Function nextTick (callback) {if (typeof process! = = 'undefined' & & typeof process.nextTick = =' function') {process.nextTick (callback)} else {const observer = new MutationObserver (callback) const textNode = document.createTextNode ('1') observer.observe (textNode, {characterData: true}) textNode.data ='2'}}
State transition
Promise instances have a total of three states, namely Pending, Fulfilled and Rejected, and the initial state is Pending.
Const PROMISE_STATES = {PENDING: 'pending', FULFILLED:' fulfilled', REJECTED: 'rejected'} class MyPromise {constructor (executor) {this.state = PROMISE_STATES.PENDING;} / /. Other codes}
Once the state of the Promise is transferred, it can no longer be transferred to another state.
/ * encapsulates the process of Promise state transition * @ param {MyPromise} promise Promise instance with state transition * @ param {*} targetState target state * @ param {*} value accompanying state transition value, which may be the value of fulfilled It may also be the reason for rejected * / function transition (promise, targetState, value) {if (promise.state = PROMISE_STATES.PENDING & & targetState! = = PROMISE_STATES.PENDING) {/ / 2.1: state can only be transferred from pending to other states after the state transfer. The values of state and value no longer change Object.defineProperty (promise, 'state', {configurable: false, writable: false, enumerable: true, value: targetState}) / /. Other codes}}
The state transition is triggered by calling resolve () or reject (). When resolve () is called, the current Promise may not immediately become Fulfilled, because the value passed into the resolve (value) method may also be a Promise. At this time, the current Promise must track the state of the passed Promise. The whole process of determining the Promise state is implemented through the Promise Resolution Procedure algorithm, and the details are encapsulated in the resolvePromiseWithValue function in the following code. When reject () is called, the current state of the Promise is determined, which must be Rejected. At this time, the state of the Promise can be transferred through the transition function (which encapsulates the details of the state transition), and subsequent actions can be performed.
/ / the execution of resolve is a trigger signal, based on which the next operation function resolve (value) {resolvePromiseWithValue (this, value)} / / reject execution is the signal that the state can be changed into Rejected function reject (reason) {transition (this, PROMISE_STATES.REJECTED, reason)} class MyPromise {constructor (executor) {this.state = PROMISE_STATES.PENDING; this.fulfillQueue = []; this.rejectQueue = [] / / immediately after constructing the Promise instance, call executor executor (resolve.bind (this), reject.bind (this))}}
Chain tracking
Suppose you now have an instance of Promise, which we call p1. Because promise1.then (onFulfilled, onRejected) returns a new Promise (we call it p2), at the same time, a microtask mt1 is registered, and the new p2 tracks the state changes of its associated p1.
When the state of p1 is transferred, the microtask mt1 callback will be executed next, and if the state is Fulfilled, onFulfilled will be executed, otherwise onRejected will be executed. The result of the microtask mt1 callback execution will be used as the basis for determining the p2 status. Here are some of the key code in the case of Fulfilled, where promise refers to p1 and chainedPromise refers to p2.
/ / callbacks should be executed asynchronously, so nextTick nextTick (()) = > {/ / then may be called multiple times So the asynchronous callback should use an array to maintain promise.fulfillQueue.forEach (({handler, chainedPromise}) = > {try {if (typeof handler = 'function') {const adoptedValue = handler (value) / / the value returned by the asynchronous callback will determine the state of the derived Promise resolvePromiseWithValue (chainedPromise, adoptedValue)} else {/ / there is no possibility of calling then as a parameter At this point, the state of the derived Promise directly adopts the state of its associated Promise. Transition (chainedPromise, PROMISE_STATES.FULFILLED, promise.value)} catch (error) {/ / if the callback throws an exception, directly transfer the state of the derived Promise to rejected, and use the exception error as reason transition (chainedPromise, PROMISE_STATES.REJECTED, error)}) / / finally clear the callback queue associated with the Promise promise.fulfillQueue = [];})
Promise Resolution Procedure algorithm
The Promise Resolution Procedure algorithm is an abstract execution process. Its syntax is [[Resolve]] (promise, x), and the accepted parameters are a Promise instance and a value x. The state direction of this Promise instance is determined by the possibility of the value x. If you take a hard look at the norms directly, it will be a bit difficult. Here, speak human words directly to explain some details.
2.3.1
If promise and the value x refer to the same object, you should directly set the state of promise to Rejected and use a TypeError as the reason for reject.
If promise and x refer to the same object, reject promise with a TypeError as the reason.
For example, the boss said that as long as the performance exceeded 1 billion this year, the performance would exceed 1 billion. This is obviously a bad sentence, and you can't take expectation itself as a condition. The right way to play is that the boss says that as long as the performance exceeds 1 billion this year, he will pay a bonus of 10 million (hey, just look forward to this).
Code implementation:
If (promise = = x) {/ / 2.3.1 due to the mechanism of Promise adoption status, congruence judgment must be made here to prevent the occurrence of an endless loop transition (promise, PROMISE_STATES.REJECTED, new TypeError ('promise and x cannot refer to a same object.'))}
2.3.2
If x is an instance of Promise, promise should adopt the state of x.
2.3.2 If x is a promise, adopt its state [3.4]: 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected. 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value. 2.3.2.3 If/when x is rejected, reject promise with the same reason.
[words] Xiao Wang asked the leader, "will there be a year-end bonus this year? how much?" The leader thought to himself, "I've been asking about this before, but it hasn't been decided yet. It depends on what the boss means." So the leader said to Xiao Wang, "I will send it, but wait for the news!"
Note that at this time, the leader made a promise to Xiao Wang, but the status of the promise p2 is still pending, which depends on the status of the promise p1 given by the boss.
Possibility 1: after a few days, the boss said to the leader, "the business has been done well this year, and the year-end bonus is 10 million." This is equivalent to the fact that p1 is already in fulfilled state, and value is 10 million. When the leader got this letter of approval, he could naturally fulfill his promise p2 with Xiao Wang, so he said to Xiao Wang, "the year-end bonus can come down, it's 10 million!" At this point, the status of commitment p2 is fulfilled and value is 10 million. Xiao Wang will be "the villa facing the sea" at this time.
Possibility 2: after a few days, the boss was a little worried and said to the leader: "this year's performance is not very good, ah, the year-end bonus will not be distributed, next year, we will send more next year." Obviously, the p1 here is the rejected. The leader saw that the situation was wrong, but he had no choice but to say to Xiao Wang, "Xiao Wang, the company is in a special situation this year, so the year-end bonus will not be paid." This p2 also rejected with it, and Xiao Wang's heart burst a little bit.
Note that there are two major directions here in section 2.3.2 of the Promise Athumb + specification, one is that the state of x is undetermined, and the other is that the state of x has been determined. In terms of code implementation, here is a trick: for cases of undetermined state, it must be implemented by subscription, and .then is a great way to subscribe.
Else if (isPromise (x)) {/ / 2.3.2 if x is an Promise instance, track and adopt its state if (x.state! = = PROMISE_STATES.PENDING) {/ / assume that the state of x has been transferred, then directly adopt its state transition (promise, x.state, x.state = PROMISE_STATES.FULFILLED? X.value: x.reason)} else {/ / assuming that the state of x is still pending, you only need to wait for the state of x to be determined before the state transition of promise / /, and the result of the state transition of x is uncertain. So in both cases we need to subscribe / / here we skillfully complete the subscription action x.then (value = > {/ / x status transition to fulfilled, because the value passed by callback is of an uncertain type. Therefore, we need to continue to apply Promise Resolution Procedure algorithm resolvePromiseWithValue (promise, value, thenableValues)}, reason = > {/ / x state transition to rejected transition (promise, PROMISE_STATES.REJECTED, reason)})}
We will not analyze the details of this article one by one, writing nearly 10,000 words, let's end it first, interested readers can directly open the source code (read on).
This is the effect diagram of the running test case, and you can see that all 872 case passed.
Complete code
Here I directly give the Javascript implementation of the Promise/A+ specification I wrote for your reference. Later, if there is time, we will consider a detailed analysis.
Github Warehouse: promises-aplus-robin [1] (it would be better to click star)
Source code [2]
Source code annotated version [3]
Defect
My implementation of the Promise/A+ specification does not have the ability to detect that the execution context stack is empty, so there will be a problem with the details (microtasks are inserted before the execution context stack is emptied) and cannot adapt to the scenario described in the above title "browsers don't talk about martial arts?".
Methodology
Whether it is a handwritten implementation of the Promise/A+ specification or other Native Code implementations, it essentially cannot bypass the following points:
Accurately understand the capabilities of Native Code implementation, just as you understand what functional points a requirement needs to achieve and prioritize its implementation.
For each function point or function description, one by one with code implementation, priority to open up the backbone process.
Design enough rich test cases, regression testing, iterate constantly, ensure the coverage of the scene, and finally create a high-quality code.
On how to achieve the Promise/A+ specification to share here, I hope that the above content can be of some help to you, can learn more knowledge. If you think the article is good, you can share it for more people to see.
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.