8
头图

Welcome to the front-end chat here, talk about the front-end

The code is on github

"Handwritten Promise" is a classic question. Basically, everyone can write a promise according to their own understanding. One day, a friend asked me, "How far does a handwritten Promise need to be?", which is also It aroused my interest and thought, "What kind of Promise is perfect?"

Perfect Promise

The first question is what constitutes a perfect Promise. In fact, this problem is not difficult. To achieve a Promsie that is "same" as the native Promise is not perfect. Then the second question comes, the native Promise. What standard is the Promise implemented according to? After consulting the information, I know that it is implemented according to the [Promises/A+] ( https://promisesaplus.com/ ) standard. The specific implementation is on ECMA - sec-promise-objects For the record, now that the standard is in place, we can implement a "perfect Promise"

Promises/A+

Next, let's take a look at Promises/A+ what the standard says, mainly in two parts, one is the noun definition and the other is the standard description. The standard description consists of three parts. Next, we briefly introduce:

Terminology

This part is the definition of nouns, mainly describing the definition of each noun in the standard

  • promise : 是then object function , 这里需要注意的是不是function --Yes function then ,Yes function with then method
  • thenable : 是定义then方法的object 函数 ,这个和上面---0c313592e580eaf08ab77ce5514587b9 promise then is a function that does not necessarily need to conform to canonical behavior
  • value : 是任何合法的javascript 值, undefinedthenablepromise ,这里的value thenable and promise , combined with the following specifications, it will be found that it is a nestable relationship
  • exception : is a value thrown by the throw keyword
  • reason : Indicates the reason for a promise status is rejected

Requirements

This part is the definition of the standard, divided into the following three parts

Promise States

A promise must be in one of the following three states

  • pending

    • Can be converted to fulfilled or rejected status
  • fulfilled

    • There needs to be a value
  • rejected

    • There needs to be a reason

When the state is fulfilled or rejected , the state can no longer be changed to another state, and value and reason can no longer be changed

The then Method

promisethen方法的行为, then方法是promise fulfilled rejected value fulfilled reason then with two parameters, ---7e5074e1ffc756e52aa6f21fe76adff

promise.then(onFulfilled,onRejected)
  • onFulfilled / onRejected

    • Both are optional parameters, if these two parameters are not function types, then ignore
    • promise 046fbec5919c67229ac2cb0e404d0537---状态变成fulfilled / rejected ,会value / reason函数the parameters
    • will only be called once
    • This needs to be done in the 宏任务 or 微任务 event loop. Note: The description of execution timing is interesting here, you can see document 2.2.4
    • Both functions need to be bound to run on global this
  • then Promise可以被多次---89d18e0187aff63dcd8582e9279ba0c9---调用, then中的onFulfilled onRejected then的call sequence call
  • then After the function call, it needs to return a promise , which is also the basis of promise can be chained call then

    promise2 = promise1.then(onFulfilled,onRejected)
    • If the onFulfilled or onRejected function returns the value x , then run the Promise Resolution Procedure
    • onFulfilled onRejected epromise2 rejected ,并且reason Yes e
    • onFulfilled onRejected函数, promise1fulfilled/rejected , promise2

The Promise Resolution Procedure

其实Promise States The then Method完了,这部分主要规定了一个抽象的操作promise resolution procedure , 用来描述当thenonFulfilled onRejected返回值x时,需要怎么样去进行操作,把表达式记[[Resolve]](promise,x) , This part is also the most complex part of the entire Promise implementation, let's take a look at what he stipulates

[[Resolve]](promise,x)
  • promise x是同一个对象时, promise 7decbbf3adef1f41da9a2e8cf2243461 rejected , reasonTypeError

     const promise = Promise.resolve().then(()=>promise); // TypeError
  • If x is a Promise, then the state of ---b8c51e8ff6519fa395799022faef6690 promise should be synchronized with x
  • If x is a object or a function , this part is the most complicated

    • First of all, store x.then in an intermediate variable then , why to do this can see document 3.5 , and then process it according to different conditions
    • 如果获取x.then的时候就抛出错误---e2db07b1f63217fc1cfc508e3b9782ff e ,则---076a50e185dafeba03535f63fb211fd8 promise状态变成rejected , reasone
    • If then is a function, then this is what we defined in thenable , then bind x for this and call then promise in resolvePromise and rejectPromise as two parameters

      then.call(x, resolvePromise, rejectPromise)

      Next, judge the result of the call

      • If resolvePromise is called, value is y , then call [[Resolve]](promise,y)
      • rejectPromise 9d72b529ea4cf9cbf90c04c1339fb239---被调用, reasone , promise c44535cd53feb7ccde228f5718885c16---状态变成rejected , reason Yes e
      • If resolvePromise and rejectPromise are called, the first call will prevail and subsequent calls will be ignored
      • If an error is thrown during the call e

        • If resolvePromise or rejectPromise has been called before throwing, then ignore the error
        • In the latter case, the status of promise becomes rejected , reason is e
    • then不是一个函数,那么promise fulfilled , valuex
  • x不是object 8d2906acb4f13608ffbfd8d1fd0e241d---或者function , promise fulfilled , value --Yes value x

The most complicated thing here is that when resolvePromise is called, value is y this part implements the recursive function thenable

The above is the specification of how to implement a "perfect" Promise. In general, the more complicated ones are The Promise Resolution Procedure and for the case of errors and call boundaries, we will start to implement a "perfect" Promises

How to test your promises

The Promise/A+ specification was introduced earlier, so how to test that your implementation fully implements the specification? Here Promise/A+ provides [promises-tests
]( https://github.com/promises-aplus/promises-tests ), which currently contains 872 test cases to test whether Promises are standard

text begins

First of all, here is the introduction of the implementation of promise according to the completed code. The code is here , the final version is used here, and the comments in it roughly indicate the rule number of the implementation. In fact, it has undergone many modifications as a whole. Portable process, you can commit history , pay attention to the two files promise_2.js and promise.js

Writing key points

The overall implementation idea is mainly the above specifications. Of course, we do not mean to implement one by one, but to classify the specifications and implement them uniformly:

Promise state definition and transition rules and basic operation

 const Promise_State = {
  PENDING: "pending",
  FULFILLED: "fulfilled",
  REJECTED: "rejected",
};

class MyPromise {
  constructor(executerFn) {
    this.state = Promise_State.PENDING;
    this.thenSet = [];
    try {
      executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
    } catch (e) {
      this._rejectedFn.call(this, e);
    }
  }
}

In the constructor, the initialization state is pending , and run the incoming constructor executerFn , pass in resovlePromise rejectePromise two parameters , ---450caddfddf45

Then we will implement resolvePromise , rejectPromise these two functions

 _resolveFn(result) {
    // 2.1.2
    if (this._checkStateCanChange()) {
      this.state = Promise_State.FULFILLED;
      this.result = result;
      this._tryRunThen();
    }
  }

  _rejectedFn(rejectedReason) {
    //2.1.3
    if (this._checkStateCanChange()) {
      this.state = Promise_State.REJECTED;
      this.rejectedReason = rejectedReason;
      this._tryRunThen();
    }
  }

  _checkStateCanChange() {
    //2.1.1
    return this.state === Promise_State.PENDING;
  }

Here is mainly through _checkStateCanChange to determine whether the executable state, and then change the state, value , reason assignment, and then try to run then the function registered by the method

At this time, our promise can already be called like this

 const p = new MyPromise((resolve,reject)=>{
   resolve('do resolve');
   // reject('do reject');
});

implementation of then

Next, we implement the then function. First, there is a simple question: "When is the then method executed?", someone will answer, it is when the promise state becomes resolve rejected , this seems to be fine at first glance, but it is actually wrong. The correct statement should be

『then方法是立即执行的,then方法onFulfilledonRejected参数会在promise resolve rejected post execution

Let's code first

 then(onFulfilled, onRejected) {
    const nextThen = [];
    const nextPromise = new MyPromise((resolve, reject) => {
      nextThen[1] = resolve;
      nextThen[2] = reject;
    });
    nextThen[0] = nextPromise;

    //2.2.6
    this.thenSet.push([onFulfilled, onRejected, nextThen]);
    this._runMicroTask(() => this._tryRunThen());
    return nextThen[0];
  }

The code looks very simple, the main logic is to construct a new promise, and then put onFulfilled , onRejected and the newly constructed promise resolve reject stored in thenSet set, and then returned this newly constructed promise, at this time our code can already be called like this

 const p = new MyPromise((resolve,reject)=>{
   resolve('do resolve');
   // reject('do reject');
});

p.then((value)=>{
  console.log(`resolve p1 ${value}`);
},(reason)=>{
  console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));

p.then((value)=>{
  console.log(`resolve p2 ${value}`);
},(reason)=>{
  console.log(`reject p2 ${reason}`);
});

Execution and execution timing of onFulfilled and onRejected

onFulFilled onRejected promise fulfilled rejected ,结合then The timing of the call, when judging that the state can be called, need to be done in two places

  • When resolvePromise , resolvePromise are called (judging whether there is a call to then registered onFulfilled and onRejected )
  • When the then function is called (to determine whether the promise state has become fulfilled or rejected )

    These two timings will call the following functions

     _tryRunThen() {
     if (this.state !== Promise_State.PENDING) {
       //2.2.6
       while (this.thenSet.length) {
         const thenFn = this.thenSet.shift();
         if (this.state === Promise_State.FULFILLED) {
           this._runThenFulfilled(thenFn);
         } else if (this.state === Promise_State.REJECTED) {
           this._runThenRejected(thenFn);
         }
       }
     }
    }

    Here it will be judged that the function registered by then needs to be called, and then the function in thenSet will be called correspondingly according to the state of the promise

 _runThenFulfilled(thenFn) {
    const onFulfilledFn = thenFn[0];
    const [resolve, reject] = this._runBothOneTimeFunction(
      thenFn[2][1],
      thenFn[2][2]
    );
    if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
      // 2.2.73
      resolve(this.result);
    } else {
      this._runThenWrap(
        onFulfilledFn,
        this.result,
        thenFn[2][0],
        resolve,
        reject
      );
    }
  }

_runThenFulfilled and _runThenRejected are similar, here is an explanation,
First we judge the legitimacy of onFulfilled or onRejected

  • 如果不合法则不执行,直接将promise 的value reason透传给之前返回给then promise, then The state of the promise is the same as the state of the original promise
  • If legal, execute onFulfilled or onRejected
 _runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {
     this._runMicroTask(() => {
        try {
          const thenResult = onFn(fnVal);
          if (thenResult instanceof MyPromise) {
            if (prevPromise === thenResult) {
              //2.3.1
              reject(new TypeError());
            } else {
              //2.3.2
              thenResult.then(resolve, reject);
            }
          } else {
            // ... thenable handler code
            // 2.3.3.4
            // 2.3.4
            resolve(thenResult);
          }
        } catch (e) {
          reject(e);
        }
     });
  }

Here is a short section of _runThenWrap , mainly to illustrate the operation of onFulfilled or onRejected , which is described in the specification.

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

To put it simply, onFulfilled and onRejected can only be executed after there is nothing in the execution context except platform code . It is often said in 微任务 , 宏任务
So we wrap the _runMicroTask method here to encapsulate the logic executed in this part

 _runMicroTask(fn) {
    // 2.2.4
    queueMicrotask(fn);
  }

Here we use queueMicrotask as the implementation of micro-tasks, of course, this has compatibility issues, you can see caniuse for details

There are many ways to achieve, such as setTimeout , setImmediate , MutationObserver , process.nextTick

value reason onFulfilled onRejected ,然后获取返回值thenResult ,接下来There will be several branches of judgment

  • if thenResult is a promise

    • Determine if the promise returned by then is the same, if it is thrown TypeError
    • 传递then返回的promise 的resolve reject , thenResult.thenonFulFilled onRejected
  • if thenResult is not a promise

    • To determine whether it is thenable , we will explain this part below
    • If none of the above judgments are true, then use thenResult as a parameter and call resolvePromise

thenable handling

thenable should be said to be the most complicated part of the implementation. First of all, we have to judge the upper part according to the definition thenResult is thenable

 if (
      typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function"
    ) {
      //2.3.3.1
      const thenFunction = thenResult.then;
      if (typeOf(thenFunction) === "Function") {
        // is thenable
      }
    }

Object d14abe064d49d8764b17175fbcdfbdc9---或者Function ,然后thenResult.then是不是个Function ,那么有人会问, Can it be written like this

 if (
      (typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
    ) {
        // is thenable
    }

At the beginning, I also wrote it like this, and then I found that the test case could not run, and finally I read the specification, there is such a paragraph 3.5

Simply put, in order to ensure the consistency of testing and calling, it is necessary to store thenResult.then before judging and running it. Multiple accesses to attributes may return different values.

Next is the processing logic of ---9b9df011938e1626dbaa289c99419075 thenable In short, the processing logic of thenable has two cases

  • In the case of then or resolve in promises that handle thenable
  • In the thenable of then callback of value or thenable

This is described here with the thenable call in promise then :

 _thenableResolve(result, resolve, reject) {
      try {
        if (result instanceof MyPromise) {
          // 2.3.2
          result.then(resolve, reject);
          return true;
        }

        if (typeOf(result) === "Object" || typeOf(result) === "Function") {
          const thenFn = result.then;
          if (typeOf(thenFn) === "Function") {
            // 2.3.3.3
            thenFn(resolve, reject);
            return true;
          }
        }
      } catch (e) {
        //2.3.3.3.4
        reject(e);
        return true;
      }
    }

    const [resolvePromise, rejectPromise] =
          this._runBothOneTimeFunction(
            (result) => {
              if (!this._thenableResolve(result, resolve, reject)) {
                resolve(result);
              }
            },
            (errorReason) => {
              reject(errorReason);
            }
          );

    try {
      thenFunction.call(thenResult, resolvePromise, rejectPromise);
    } catch (e) {
      //2.3.3.2
      rejectPromise(e);
    }

Here we construct resolvePromise and rejectPromise , and then call thenFunction , which will be called after processing in the function logic resolvePromise rejectPromise f216e7 rejectPromise , result thenable ,那么就会继续传递下去, thenable ,调用resolve reject

What we should pay attention to is that the then method of promise is different from the --- ---dccf78fd7533655f6529a7b8afa62fb2--- method of thenable then

  • The promise then has two parameters, one is fulfilled , and the other is rejected , the corresponding function will be called back after the previous promise state changes
  • thenable then also has two parameters, these two parameters are provided to thenable call completion for callback resolve reject call back reject方法, thenable thenable ,那么会按照这个逻辑调用下去,直到是一个非thenable ,就会调用From thenable back to the nearest promise resolve or reject

    At this point, our promise can already support the operation of thenable

     new MyPromise((resolve)=>{
     resolve({
       then:(onFulfilled,onRejected)=>{
         console.log('do something');
         onFulfilled('hello');
       }
     })
    }).then((result)=>{
    
     return {
       then:(onFulfilled,onRejected)=>{
         onRejected('world');
       }
     }
    });

Error handling in promise and then and thenable

Error handling refers to the error that occurs during the running process to be captured and processed, basically using try/catch call after the error is caught reject callback, this part is relatively simple, you can see it directly code

The number of calls of resolve and reject functions

The resolve and reject calls in a promise can be said to be mutually exclusive and unique, that is, only one of these two functions can be called, and it is called once, which is relatively simple to say , but with error scenarios there is a certain level of complexity that could have been

 if(something true){
  resolve();
}else {
  reject();
}

After adding the error scene

 try{
  if(something true){
    resolve();
    throw "some error";
  }else {
    reject();
  }
}catch(e){
  reject(e);
}

At this time, the judgment will be invalid, so we can achieve the goal by wrapping two mutually exclusive functions through a tool class.

 _runBothOneTimeFunction(resolveFn, rejectFn) {
    let isRun = false;

    function getMutuallyExclusiveFn(fn) {
      return function (val) {
        if (!isRun) {
          isRun = true;
          fn(val);
        }
      };
    }
    return [
      getMutuallyExclusiveFn(resolveFn),
      getMutuallyExclusiveFn(rejectFn),
    ];
  }

At this point, we have a Promise that fully complies with the Promise/A+ standard, and it is completed. The complete code is here

Wait, is there something missing?

Some people will see this and say, is this the end?
catchfinally ,还有静态方法Promise.resolvePromise.rejectPromise.all/race/any/allSettled方法呢?

In fact, from the standard, the standard of Promise/A+ is the part described above, only defines the then method, and the other methods we use every day are actually in the then The method is derived from the above, such as catch method

 MyPromise.prototype.catch = function (catchFn) {
  return this.then(null, catchFn);
};

The specific method is actually implemented, you can see promise_api for details.

at last

Finally, I want to share the process of writing this promise. From the above description, it seems to be very smooth, but in fact, when writing, I basically simply passed the following standards, and then combined it according to my own understanding promises-tests Written by unit test cases, this development mode is actually TDD (Test-driven development) , this development mode will greatly reduce the mental burden of developers who do not cover boundary scenarios when programming, But on the other hand, the portable quality requirements for test cases are very high. Overall, this portable promise is a more interesting process. If there is any problem with the above, please leave a message for more exchanges.


斑驳光影
2.2k 声望336 粉丝