Handwritten wheel series - handwritten promise (smooth as silk)

Handwritten wheel series - handwritten promise (smooth as silk) Promise is a solution for JS asynchronous programming, w...
1. Promises/A + specification
2. Realize Promise by touching hands
3. Optimization
4. Summary
5. Attach all codes

Handwritten wheel series - handwritten promise (smooth as silk)

Promise is a solution for JS asynchronous programming, which is more reasonable and powerful than the traditional solutions - callback functions and events.

Promise is simply a container that holds the results of an event (usually an asynchronous operation) that will not end in the future. It can be said that promise perfectly solves the callback hell. With promise objects, asynchronous operations can be expressed in the process of synchronous operations, avoiding layers of nested callback functions. In addition, promise objects provide a unified interface, making it easier to control asynchronous operations.

So how to implement a Promise manually? At first glance, we may not be able to start, but as long as we follow Promises/A + specification (Chinese translation) Step by step, you will find that it is so simple to implement a Promise. It is strongly recommended to skim through this article before reading it Promises/A + specification (Chinese translation) , reading this article after reading it will make you feel enlightened

1. Promises/A + specification

The concept of promise was first put forward by the community, and many specifications were put forward, of which the commonly accepted one was put forward by the commonjs community Promises/A standard. However, there are still some deficiencies in this specification, so the community later proposed the plus specification based on this specification, namely Promises/A + specification (Chinese translation) , this specification has been unanimously recognized by the community. Promise in ES6 is implemented based on this specification. In addition, ES6 also adds some methods not mentioned in the specification, such as promise.resolve, promise.reject, promise.all, promise.race, and promise.catch.

The case is distinguished here. The upper case is promise static method and the lower case is promise instance method; Similarly below, promise represents an example

This handwritten Promise only implements the methods mentioned in Promises/A + specification, and other methods implemented in ES6 will not be implemented temporarily (but the implementation method is also very simple, You can view the source code in this warehouse).

2. Realize Promise by touching hands

2.1 Promise basic

2.1.1 basic structure of promise

Let's briefly review how Promise is used in ES6.

const p = new Promise((resolve, reject) => { console.log('Promise The execution function is executed immediately'); setTimeout(() => resolve(0), 1000); }); p.then( value => console.log('value:', value), reason => console.log('reason:', reason) ); // Promise execution function is executed immediately // value: 0 (after 1 second)

As can be seen from the above review:

  1. Promise is a constructor (class used in ES6)
  2. When new Promise, an execution function is passed in, and the execution function is executed immediately
  3. The execution function receives two parameters, the resolve function and the reject function, and both can receive parameters
  4. Promise has a then method on its instance, which receives two parameters

Therefore, use the syntax of ES6 to implement Promise's infrastructure:

class BasicPromise { constructor(executor) { const resolve = value => { console.log('Called resolve Will value Pass to then Method'); }; const reject = reason => { console.log('Called reject Will reason Pass to then Second function of method'); }; try { executor(resolve, reject); // There may be an error executing this function } catch (error) { reject(error); }; }; then(fn1, fn2) {}; // Example method };
2.1.2 status in promise

The Promises/A + specification states that a promise must be in one of the following three states: pending, fulfilled (sometimes referred to as "resolved"), rejected, and the initialization state is pending. It can only be from "pending" to "cashing", or from "pending" to "rejected" , once the status is confirmed, it cannot be changed.

As can be seen from ES6 Promise:

  • In the execution function of promise, resolve is invoked to solve promise and pass value value, that is, to transfer promise successfully from "pending" state to "cashing" state.
  • In the execution function of promise, reject is invoked to solve promise and pass value reason (cause), that is, to transfer promise successfully from "undetermined" state to "reject" state.

In combination with 2.1.1, the implementation process is the same:

  • Promise requires a status value
  • Change the state when executing the function call resolve or reject, and can only change from pending to cashing, or from pending to reject
const PENDING = 'pending'; const FULFILLED = 'fulfilled'; const REJECTED = 'rejected'; class BasicPromise { state = PENDING; constructor(executor) { const resolve = value => { // Pending - > full, solve promise if (this.state === PENDING) { this.state = FULFILLED; // State flow console.log('Also pass parameters value'); } }; const reject = reason => { if (this.state === PENDING) { // Pending - > rejected, reject promise this.state = REJECTED; // State flow console.log('Also pass parameters reason'); } }; ...(If the code is omitted, the omitted code is exactly the same as the above code) }; ...(If the code is omitted, the omitted code is exactly the same as the above code) };
2.1.3 then method in promise

The Promises/A + specification provides 7 detailed descriptions for the implementation of then method, which can be implemented step by step according to the specification:

A promise must provide a then method to access its current or final value or denial.

promise's then method receives two parameters:

promise.then(onFulfilled, onRejected)
  1. onFulfilled and onRejected are optional parameters:

    • If onFulfilled is not a function, it must be ignored

    • If onRejected is not a function, it must be ignored

    The implementation of Article 1 is very simple. You only need to judge whether the incoming value is a function, not a function, and ignore it directly.

  2. If onFulfilled is a function:

    • After promise is solved, it must be called, and its first parameter is the final value of promise

    • It cannot be called until promise is resolved

    • It cannot be called more than once

  3. If onRejected is a function:

    • When promise is rejected, it must be called, and its first parameter is the denial reason of promise
    • It cannot be called until promise rejects it
    • It cannot be called more than once

    Second and third are the same way of processing, that is, onFulfilled or onRejected functions need to be called after promise is resolved or refused, and the corresponding first parameter is the final value or the cause of rejection. From 2.1.2, we change the state of promise (that is, solve or reject promise). Is it possible to call resolve or reject function in the execution function? Is it possible to call onFulfilled or onRejected function directly in resolve or reject function? It seems feasible, but there is a problem that resolve or reject functions are executing functions, while onFulfilled or onRejected functions are in the then method. Let's discuss them. Row function, assuming that the resolve function is invoked in the execution function:

    • The execution function is a synchronization function: when const p = new Promise(executor), the execution function will be executed immediately and synchronously, and the resolve function will be executed synchronously. At this time, the state of promise has changed and the value value has been obtained; then, the synchronous execution p.then(onFulfilled, onRejected) At this time, you can directly determine the state of promise to determine the function to be executed.
    • The execution function is asynchronous: when const p = new Promise(executor), the execution function will be executed immediately, but the resolve function is executed asynchronously. At this time, the promise status is still pending and the value value is still undefined; Then execute p.then(onFulfilled, onRejected) synchronously. At this time, you can save the onFulfilled and onRejected functions (directly under this in class). When the resolve function is executed asynchronously, execute onFulfilled in the resolve function and pass parameters (similarly, onRejected in the reject function).
    // then method in 03 promise ...(Omit code) class BasicPromise { state = PENDING; value = undefined; reason = undefined; onFulfilledCallback = undefined; onRejectedCallback = undefined; constructor(executor) { const resolve = value => { // PENDING -> FULFILLED if (this.state === PENDING) { this.state = FULFILLED; // State flow this.value = value; // The execution function saves value for synchronization this.onFulfilledCallback && this.onFulfilledCallback(value); // When the execution function is asynchronous, wait until resolve is executed, and then execute onFulfilled } }; const reject = reason => { if (this.state === PENDING) { // PENDING -> REJECTED this.state = REJECTED; // State flow this.reason = reason; // Save reason when the execution function is synchronization this.onRejectedCallback && this.onRejectedCallback(reason); // When the execution function is asynchronous, wait until reject is executed before onRejected is executed } }; ...(Omit code) }; then(onFulfilled, onRejected) { if (this.state === FULFILLED) { // The execution function is synchronous and resolve s typeof onFulfilled === 'function' && onFulfilled(this.value); } else if (this.state === REJECTED) { // The execution function is synchronous and reject is executed typeof onRejected === 'function' && onRejected(this.reason); } else { // The execution function is asynchronous typeof onFulfilled === 'function' && (this.onFulfilledCallback = onFulfilled); typeof onRejected === 'function' && (this.onRejectedCallback = onRejected); }; }; };
  4. Call time:

    The Promises/A + specification indicates that onFulfilled and onRejected are not called immediately after the promise is resolved or rejected, but placed in the task queue. The specific execution time needs to be determined according to the implementation mechanism. In practice, ensure that the onFulfilled and onRejected functions are executed asynchronously, and should be executed in the new execution stack of the new round of event loop after the then method is called. This mechanism can be implemented by "macro task" mechanism, such as setTimeout or setImmediate; You can also use the "micro task" mechanism, such as MutationObserver or process.nextTick.

    Here, the macro task setTimeout is used:

    // Call timing of onFulfilled and onRejected in 04 promise ...(Omit code) class BasicPromise { ...(Omit code) then(onFulfilled, onRejected) { if (this.state === FULFILLED) { // The execution function is synchronous and resolve s typeof onFulfilled === 'function' && setTimeout(() => { onFulfilled(this.value); }, 0); } else if (this.state === REJECTED) { // The execution function is synchronous and reject is executed typeof onRejected === 'function' && setTimeout(() => { onRejected(this.reason); }, 0); } else { // The execution function is asynchronous if (typeof onFulfilled === 'function') { this.onFulfilledCallback = value => setTimeout(() => onFulfilled(value), 0); }; if (typeof onRejected === 'function') { this.onRejectedCallback = reason => setTimeout(() => onRejected(reason), 0); }; }; }; };
  5. Call requirements:

    onFulfilled and onRejected must be called as functions, that is, in strict mode, the value of this function is undefined; In non strict mode, it is a global object.

    To be honest, Xiaosheng, I don't fully understand this, but it doesn't affect our realization of promise

  6. The then method can be called multiple times by the same promise

    • After promise is resolved, all onFulfilled must be called back in turn according to their registration order

    • After the promise is rejected, all onRejected must be called back in turn according to their registration order

    Previously, only the onFulfilled and onRejected functions of a then method are saved. If the then method is called multiple times, there will be multiple onFulfilled and onRejected functions. At this time, we still need to discuss the execution functions:

    • The execution function is a synchronization function: it can be executed directly in the then method in turn
    • The execution function is asynchronous: all onFulfilled and onRejected functions need to be saved and executed when the resolve or reject function is called in turn. The array can be implemented, the data can be saved in turn, and then executed in turn.
    // The then method in 05 promise can be called multiple times ...(Omit code) class BasicPromise { ...(Omit code) onFulfilledCallback = []; onRejectedCallback = []; constructor(executor) { const resolve = value => { // PENDING -> FULFILLED if (this.state === PENDING) { ...(Omit code) this.onFulfilledCallback.forEach(onFulfilled => onFulfilled(value)); // Execute all onFulfilled } }; const reject = reason => { if (this.state === PENDING) { // PENDING -> REJECTED ...(Omit code) this.onRejectedCallback.forEach(onRejected => onRejected(reason)); // Execute all onRejected } }; ...(Omit code) }; then(onFulfilled, onRejected) { ...(Omit code) } else { // The execution function is asynchronous if (typeof onFulfilled === 'function') { this.onFulfilledCallback.push(value => setTimeout(() => onFulfilled(value), 0)); }; if (typeof onRejected === 'function') { this.onRejectedCallback.push(reason => setTimeout(() => onRejected(reason), 0)); }; }; }; };
  7. The then method must return a Promise object

    This article is the content of the full version, so it will be described in 2.2.

So far, the basic version of promise has been implemented (that is, the code in "then method in 05 promise can be called multiple times"). In order to save space, there will be no repeated display here. Click to view the source code or the end of the text.

2.2 full Promise

Promise in ES6 can realize chain call and value penetration:

  • Chain call: Promise's then function will return a new Promise, and when the then function returns a value, no matter what value it is, it can be obtained in the next then. This is then's chain call
  • Value penetration: when the parameter in then is empty or non function, such as promise. Then ('test '). Then(). Then (v = > console. Log (V)), if the value of promise resolve is 10, then the last then can still get the value 10 returned by then and print 10, which is the so-called value penetration

How to achieve it? Don't worry. Article 7 of the Promises/A + specification on the implementation of the then method describes the implementation method in detail.

2.2.1 the then method must return a Promise object
promise2 = promise1.then(onFulfilled, onRejected)
  1. If onFulfilled or onRejected is a function and returns a value of X, run the Promise resolution process: [[Resolve]](promise2, x) (set as the resolvePromise function first)

    "Promise solution process: [[resolve]] (promise 2, x)" refers to an abstract execution process, which can be directly understood as a function, which will be described in detail in 2.2.2

  2. If onFulfilled or onRejected throws an exception e, promise 2 must reject and return the rejection E

  3. If onFulfilled is not a function and promise1 is resolved, promise2 must resolve and return the same value as promise1

  4. If onRejected is not a function and promise1 has rejected, promise2 must reject and return the same rejection factor as promise1

The FullPromise code is the same except that the then method is different from BasicPromise, so it will not be repeated here; The following code is written completely according to the above four points, so it is very smooth.

// The then method in 06 Promise must return a Promise object ...(Omit code, and BasicPromise agreement) function resolvePromise() {} class FullPromise { ...(Omit code, and BasicPromise agreement) then(onFulfilled, onRejected) { const promise2 = new FullPromise((resolve, reject) => { // Returns a new promise if (this.state === FULFILLED) { if (typeof onFulfilled === 'function') { // onFulfilled is a function and promise1 has been resolved setTimeout(() => { // Asynchronous execution try { // Capture error const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); // Run Promise resolution process } catch (error) { // promise2 rejects and returns the rejection error reject(error); }; }, 0); } else { resolve(this.value); // onFulfilled is not a function and promise1 is resolved. promise2 resolves and returns the same value as promise1 }; } else if (this.state === REJECTED) { if (typeof onRejected === 'function') { // onRejected is a function and promise1 has been rejected setTimeout(() => { try { const x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); // Run Promise resolution process } catch (error) { // promise2 rejects and returns the rejection error reject(error); }; }, 0); } else { reject(this.reason); // onRejected is not a function, and promise1 has rejected it. promise2 rejects it and returns the same rejection factor as promise1 }; } else { this.onFulfilledCallback.push(value => setTimeout(() => { // Promise 1 status is undetermined, put onFulfilled into the array if (typeof onFulfilled === 'function') { // It is the same as the onFulfilled processing method of synchronization, but the execution time is different try { const x = onFulfilled(value); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } } else { resolve(value); }; }, 0)); this.onRejectedCallback.push(reason => setTimeout(() => { if (typeof onRejected === 'function') { // It is the same as the onRejected processing method of synchronization, but the execution time is different try { const x = onRejected(reason); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } } else { reject(reason); }; }, 0)); } }); return promise2; }; };
2.2.2 Promise solution process

Section 2.3 of the Promises/A + specification states that the promise resolution process is an abstract operation, which requires a promise and value as inputs, expressed as [[resolve]] (promise 2, x). If x has the then method and looks like a promise, the resolution program attempts to make promise 2 accept the state of X; Otherwise, use the x value to solve promise2 directly.

The [[Resolve]](promise, x) of the original text is a promise written directly. Here, in order to make readers understand more intuitively, change promise to promise 2, so as to more clearly understand which promise is resolved or rejected in the following text
x mentioned here and below refers to the value returned by the onFulfilled or onRejected function in the then method of promise1

To execute [[Resolve]](promise2, x), follow the following steps:

  1. x is equal to promise2

    • If promise2 and x point to the same object, reject promise2 with TypeError as the rejection reason
  2. If x is Promise, depending on the status:

    • If x is pending, promise 2 needs to remain pending until x is resolved or rejected

    • If x is in the cashing state, solve promise2 with the same final value as X

    • If x is in the reject state, reject promise2 with the same reject reason as X

  3. If x is an object or function

    • Assign x.then to then

    • If an error e is thrown when taking the value of x.then, promise 2 is rejected with e as the rejection factor

    • If then is a function, call x as the scope of this function. Pass two callback functions as parameters. The first parameter is called resolvePromise and the second parameter is called rejectPromise:

      • If resolvePromise is called with the value y as the parameter, run [[Resolve]](promise2, y)

      • If rejectproject is called with reject r as the parameter, then reject promise2 with reject R

      • If both resolvePromise and rejectPromise are called, or are called more than once by the same parameter, the first call is preferred and the remaining calls are ignored

        The main purpose here is to better fit the then method. onFulfilled and onRejected will execute only one of them and only once

    • If calling the then method throws an exception e:

      • If resolvePromise or rejectPromise has already been called, it is ignored

      • Otherwise, promise 2 will be rejected for e

    • If then is not a function, solve promise2 with x as the parameter

  4. If x is not an object or function, use X as a parameter to solve promise2

function resolvePromise(promise, x, resolve, reject) { if (promise === x) { // promise2 and x point to the same object, and promise2 is rejected with TypeError as the rejection reason reject(new TypeError('Chaining cycle detected for promise')); } else if (typeof x === 'function' || (typeof x === 'object' && x !== null)) { // If x is an object or function, point 2 above. If x is Promise, Promise is also an object, so it doesn't need to be processed separately let called = false; // Whether it is called, which is used to process. When both resolvePromise and rejectPromise are called, or are called multiple times by the same parameter, only the first call is made and the remaining calls are ignored try { const then = x.then; if (typeof then === 'function') { // then is a function then.call(x, y => { // The then function executes and receives two callback functions if (called) return; called = true; resolvePromise(promise, y, resolve, reject); }, r => { if (called) return; called = true; reject(r); }); } else { resolve(x); // Deal with point 4 in point 3 above. then is not a function. Take x as the parameter to solve promise 2 }; } catch (error) { if (called) return; called = true; reject(error); } } else { resolve(x); // If x is not an object or function, use X as a parameter to solve promise2 } };

In this way, the full version of Promise is realized. As long as you follow the Promises/A + specification, it can be said to be silky all the way.

2.3 testing

Promises/A + specification Corresponding git warehouse One warehouse in is dedicated to testing Promise implemented with reference to Promises/A + specification—— promises-tests , refer to the method provided in the warehouse to test the Promise written by the opponent.

  1. Install Promise test dependencies

    yarn add promises-aplus-tests -D
  2. Add script in package.json

    "scripts": { "test": "promises-aplus-tests Fill in the test document address" // For example. / src/testFullPromise.js },
  3. Write test documents

    1. First, export FullPromise in FullPromise.js

      module.exports = FullPromise;
    2. Then write the test file

      const FullPromise = require('../FullPromise'); // Import FullPromise // Refer to the methods provided by the promises tests warehouse FullPromise.defer = FullPromise.deferred = function(){ let dfd = {}; dfd.promise = new FullPromise((resolve, reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }); return dfd; }; module.exports = FullPromise; // Finally, export it
  4. Run the command to start the test

    yarn test

Promises aplus tests has 872 test cases, and the FullPromise implemented in this paper has passed.

3. Optimization

The FullPromise code mentioned above is completely implemented in accordance with the Promises/A + specification. Therefore, there are many redundant codes, especially the then method. The following optimization is mainly to optimize the then method:

This part may be difficult. You need to look at it after fully understanding FullPromise and Promise solution process

3.1 unified processing of onFulfilled and onRejected functions

  1. When onFulfilled is not a function, promise 2 is finally solved and the same value as promise 1 is passed. Therefore, it can be processed into a transfer value function: value = > value
  2. When onRejected is not a function, promise 2 is rejected and the same reason as promise 1 is passed. Therefore, it can be processed into a function that throws an error: reason = >
const onFulfilledNow = typeof onFulfilled === 'function' ? onFulfilled : value => value; const onRejectedNow = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

3.2 encapsulate the execution logic after promise is resolved or rejected

const handleResolve = value => { // Encapsulate the execution logic after promise solution try { const x = onFulfilledNow(value); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); }; }; const handleReject = reason => { // Encapsulates the execution logic after promise rejection try { const x = onRejectedNow(reason); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); }; };

The optimized complete code is as follows:

...(Omit the code. The omitted code is and FullPromise The code is identical) class FullPromisePerfect { ...(Omit the code. The omitted code is and FullPromise The code is identical) then(onFulfilled, onRejected) { const onFulfilledNow = typeof onFulfilled === 'function' ? onFulfilled : value => value; const onRejectedNow = typeof onRejected === 'function' ? onRejected : reason => { throw reason }; const promise2 = new FullPromisePerfect((resolve, reject) => { const handleResolve = value => { try { const x = onFulfilledNow(value); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); }; }; const handleReject = reason => { try { const x = onRejectedNow(reason); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); }; }; if (this.state === FULFILLED) { setTimeout(() => handleResolve(this.value), 0); } else if (this.state === REJECTED) { setTimeout(() => handleReject(this.reason), 0); } else { this.onFulfilledCallback.push(value => setTimeout(() => handleResolve(value), 0)); this.onRejectedCallback.push(reason => setTimeout(() => handleReject(reason), 0)); } }); return promise2; }; };

4. Summary

About a year ago, I wrote a complete Promise with reference to the code of my predecessors, but at that time, I was more imitation and didn't understand its original meaning.

I saw Promise again some time ago, and then I carefully read and translated the Promises/A + specification. It turned out that it was like this.

The original Promises/A + specification has given the implementation steps (or pseudo code). As long as you follow this specification step by step, the implementation of a Promise is absolutely silky.

After this rewrite, the understanding of Promise has reached a new level, thanks to the Promises/A + specification, so if you want to write Promise, Be sure to refer to this specification.

18 October 2021, 17:34 | Views: 7665

Add new comment

For adding a comment, please log in
or create account

0 comments