From practice to principle

Promise/A+ specification, we’ll look at it later

Many people will tell you that the first step to writing A Promise is to read the Promise/A+ specification carefully.

Why read the specification first? Because a specification is a standard, it’s a constraint on the characteristics of your target product — it has to match the characteristics I’m talking about here for you to be a Promise. We can think of the process of developing promises as writing A requirement, and the Promise/A+ specification is our requirements document.

For most of you, however, at this stage of your study, what you need is not a requirements document, but a study guide. One glance at this “requirements document” would probably have stopped most students from writing promises — a specification, in essence, a blunt expression of knowledge that is obscure to the novice. To master, according to the specification to achieve, not what difficult. However, for learners, it is no doubt that they are required to take the exam without finishing the textbook and doing the exercises. It is very easy to feel frustrated and give up.

In fact, the reverse order is more reasonable – first follow me out a Promise, in the process of writing, I will tell you, bit by bit, why to do this, how the specification describes it. In the process, your knowledge and understanding of the Promise/A+ specification will grow from nothing, from vague to transparent. When you’re done, go back and read the original specification for yourself, and you’ll find that the previously obscure rules and regulations suddenly come to life. At this point to carefully mull over every word inside, will read more and more taste. This is the meaning of “from practice to principle”.

Quick start: Executor with three states

Let’s remember the Promise we used earlier. In terms of the feeling of use, a Promise should have at least the following two basic features:

  • You can accept an executor as an input parameter
  • There are three states: Pending, depressing and Rejected

Let’s start with the basic outline. (Parsing is important in line-by-line comments.)

function MyPromise(executor) {
    // value Records the successful execution result of the asynchronous task
    this.value = null;
  
    // reason Records the reason for asynchronous task failure
    this.reason = null;
  
    // status Records the current status. The initialization is pending
    this.status = 'pending';
     
    // Save this for later use
    var self = this;
  
    // Define resolve
    function resolve(value) {
        // The asynchronous task succeeds, and the result is assigned to value
        self.value = value;
      
        // The current status changes to depressing
        self.status = 'fulfilled'; 
    }
    
    // Define reject
    function reject(reason) {
        // The asynchronous task fails, and the result is assigned to value
        self.reason = reason; 
      
        // The current state switches to Rejected
        self.status = 'rejected';
    }
  
    // Assign resolve and reject capabilities to the actuator
    executor(resolve, reject);
}
Copy the code

Behavior of then methods

Every promise instance must have a THEN method, so it’s not hard to imagine that the THEN method should be installed on the prototype object of the Promise constructor (parsing in the line-by-line comments is important in this section).

// The then method takes two functions as inputs (optional)
MyPromise.prototype.then = function(onResolved, onRejected) {
  
    // Note that onResolved and onRejected must be functions; If not, we use a pass-through here
    if (typeofonResolved ! = ='function') {
        onResolved = function(x) {return x};
    }
    if (typeofonRejected ! = ='function') {
        onRejected = function(e) {throw e};
    }

    // Still save this
    var self = this;
  
    // Judge whether it is a pity
    if (self.status === 'fulfilled') {
        // If yes, execute the corresponding processing method
        onResolved(self.value);
    } else if (self.status === 'rejected') {
        // In the rejected state, the rejected method is executedonRejected(self.reason); }};Copy the code

run

Throw our MyPromise in the console and run with it

new MyPromise(function(resolve, reject){
    resolve('成了!');
}).then(function(value){
    console.log(value);
}, function(reason){
    console.log(reason);
});

// Output "done!"

new MyPromise(function(resolve, reject){
    reject('wrong! ');
}).then(function(value){
    console.log(value);
}, function(reason){
    console.log(reason);
});

// Output "Wrong!"
Copy the code

OK! If you’re typing correctly, our handwritten version of MyPromise is running fine. Now that we have the skeleton, we add flesh and blood to it, eyebrows and eyes, and it’s a human Promise

Chain calls

Remember that in Promise, both then and catch methods can be called indefinitely through chain calls. Here’s the caveat: The Promise/A+ specification doesn’t mention catch at all. It only emphasizes the existence of then and constrains its behavior. So what we’re going to do here is implement a chain call to THEN.

To implement chain calls, let’s consider the following major changes:

  • The then method should return this directly (chaining the general operation);
  • Chain call allows us to call THEN several times, and the onResolved (also called onFulFilled) and onRejected tasks passed in multiple THEN. We need to maintain them in a queue.
  • To ensure that the THEN method executes, do it before the onResolved queue and the onRejected queue execute in batches. Otherwise, when queue tasks are executed in batches, the task itself is not collected, and it will be an error. An easy way to do this is to wrap the batch execution as an asynchronous task, which ensures that it can always be executed after synchronizing the code.

OK, now that we know what to do, let’s go ahead and improve the constructor code

function MyPromise(executor) {
    // value Records the successful execution result of the asynchronous task
    this.value = null;
  
    // reason Records the reason for asynchronous task failure
    this.reason = null;
  
    // status Records the current status. The initialization is pending
    this.status = 'pending';
  
    // Cache the two queues, which will be fulfilled and rejected
    this.onResolvedQueue = [];
    this.onRejectedQueue = [];
         
    // Save this for later use
    var self = this;
  
    // Define resolve
    function resolve(value) {
        // If it is not pending, return it directly
        if(self.status ! = ='pending') {
            return;
        }
      
        // The asynchronous task succeeds, and the result is assigned to value
        self.value = value;
      
        // The current status changes to depressing
        self.status = 'fulfilled'; 
      
        // Use setTimeout to delay the execution of queue tasks
        setTimeout(function(){
            // Perform tasks in the fulfilled queue in batches
            self.onResolvedQueue.forEach(resolved= > resolved(self.value)); 
        });
    }
        
    // Define reject
    function reject(reason) {
        // If it is not pending, return it directly
        if(self.status ! = ='pending') {
            return;
        }
      
        // The asynchronous task fails, and the result is assigned to value
        self.reason = reason; 
      
        // The current state switches to Rejected
        self.status = 'rejected';
      
        // Use setTimeout to delay the execution of queue tasks
        setTimeout(function(){
            // Batch execute the tasks in the Rejected queue
            self.onRejectedQueue.forEach(rejected= > rejected(self.reason));
        });
    }
  
    // Assign resolve and reject capabilities to the actuator
    executor(resolve, reject);
}
Copy the code

Accordingly, the THEN method also needs to be revamped. In addition to returning this, we will now consider all the cases when the fulfilled and Rejected tasks are not completely pushed into the queue as pending states. So in the then method, we also need to do additional processing on Pending

// The then method takes two functions as inputs (optional)
MyPromise.prototype.then = function(onResolved, onRejected) {
  
    // Note that onResolved and onRejected must be functions; If not, we use a pass-through here
    if (typeofonResolved ! = ='function') {
        onResolved = function(x) {return x};
    }
    if (typeofonRejected ! = ='function') {
        onRejected = function(e) {throw e};
    }
 
    // Still save this
    var self = this;
  
    // Judge whether it is a pity
    if (self.status === 'fulfilled') {
        // If yes, execute the corresponding processing method
        onResolved(self.value);
    } else if (self.status === 'rejected') {
        // In the rejected state, the rejected method is executed
        onRejected(self.reason);
    } else if (self.status === 'pending') {
        // If the task is pending, only the task is queued
        self.onResolvedQueue.push(onResolved);
        self.onRejectedQueue.push(onRejected);
    }
    return this
};
Copy the code

Run again

Now let’s verify that the chain call works

const myPromise = new MyPromise(function (resolve, reject) {
    resolve('成了!');
});

myPromise.then((value) = > {
    console.log(value)
    console.log('I'm the first one.')
}).then(value= > {
    console.log('I'm mission two.')});// Output "Yes!" "I'm mission number one." "I'm mission number two."
Copy the code

The following output is displayed:

As you can see, our chain call is working!

However, careful students have already seen that the version of the chain call we are implementing now is still very thin compared to the chain call of real Promise. So how thin is it? To implement a more complete chain call, we also need to understand the Promise resolution program, so let’s move on

The resolution process

Analysis of existing chain-call defects

We just wrote this MyPromise, and one of the most obvious flaws is that the next THEN doesn’t get the result of the last THEN

const myPromise = new MyPromise(function (resolve, reject) {
    resolve('成了!');
});

myPromise.then((value) = > {
    console.log(value)
    console.log('I'm the first one.')
    return 'Results of the first task'
}).then(value= > {
    // where value is expected to output 'result of first task'
    console.log('The result of the second quest trying to get the first quest is:', value)
});
Copy the code

In this code, we try to get the result of the first THEN in the second THEN, but the actual output is:

The second THEN seems to ignore the result of the first THEN, and still fetches the original resolve value in the Promise executor — which is obviously unreasonable.

In fact, in addition to the most obvious flaw, there are a number of capability issues that we have now implemented with the Promise, such as missing special handling of thenable objects, missing exception handling, and so on, which can be summed up in one sentence: the processing of the THEN method is too crude.

Reexamine the THEN approach — understand the Promise resolution process

As we said earlier, the whole Promise specification, at the method level, basically revolves around THEN. One that needs the most attention is called the Promise Resolution Procedure. The translation of the name is tricky, especially the “resolution” action, which seems to scare people. The word resolve describes the action ‘resolve’. The resolution procedure constrains how resolve should behave. This action is closely related to then, so to perfect the THEN method, we must have a detailed understanding of the content of the resolution program. Let’s take A look at the Promise/A+ specification:

The resolver handles an abstract operation with a promise and a value as inputs, which we represent as

[[Resolve]](promise, x)
Copy the code

Don’t meng. This might seem a little too advanced and unfriendly, but you’ve certainly seen it before

promise2 = promise1.then(onResolved, onRejected);
Copy the code

[Resolve]](promise, x) if onResolved or onRejected returns x, execute the promise resolution process [[Resolve]](promise2, x).

As long as they both implement the PROMISE /A+ standard, different promises can be called to each other.

  1. If x and promise both point to the same object, the promise is rejected for reason as typeError.

  2. If x is a Promise object, the Promise takes the current state of X:

    A. If X is in the pending state, the Promise must keep its pending state until x becomes a pity or Rejected

    B. If X is a pity state, implement the promise with the same value value

    C. If x is in the Rejected state, implement the promise with the same reason.

  3. If x is an object or function:

    A. Point the promise’s then method to x. Chen

    B. If x. teng throws error, reject is called with error as reason

    C. If then is a function, call it with x as this, with resolvePromise as the first argument and rejectPromise as the second

    I. If resolvePromise is called with the value y, run '[[Resolve]](promise, y)' ii. If 'rejectPromise' is invoked with argument r, then 'Promise (reject)' iii is executed with reason R. If both resolvePromise and rejectPromise are called, or if the same parameter is called more than once, use the first call and ignore the remaining call iv. If the call to THEN throws an exception error (1). If either resolvePromise or rejectPromise has already been called, it is ignored. (2) otherwise use error as reason to refuse the promised. If then is not function, implement a promise with an x argumentCopy the code
  4. If x is not an object or function, implement a promise with x as an argument

Refine CutePromise with the resolution process

Our main idea is to put forward the logic of the resolution program, on this basis to improve the THEN method (because the resolution program we will call in the THEN method).

Constructor modification

The constructor side doesn’t need much modification. We’ll just take setTimeout out of it. This is because we will put the asynchronous processing into the then method resolveByStatus/ rejectByStatus.

function MyPromise(executor) {
    // value Records the successful execution result of the asynchronous task
    this.value = null;
  
    // reason Records the reason for asynchronous task failure
    this.reason = null;
  
    // status Records the current status. The initialization is pending
    this.status = 'pending';
  
    // Cache the two queues, which will be fulfilled and rejected
    this.onResolvedQueue = [];
    this.onRejectedQueue = [];
     
    // Save this for later use
    var self = this;
  
    // Define resolve
    function resolve(value) {
        // Return pending status
        if(self.status ! = ='pending') {
            return;
        }
      
        // The asynchronous task succeeds, and the result is assigned to value
        self.value = value;
      
        // The current status changes to depressing
        self.status = 'fulfilled'; 
      
        // Perform tasks in the fulfilled queue in batches
        self.onResolvedQueue.forEach(resolved= > resolved(self.value)); 
    }
    
    // Define reject
    function reject(reason) {
        // Return pending status
        if(self.status ! = ='pending') {
            return;
        }
      
        // The asynchronous task fails, and the result is assigned to value
        self.reason = reason; 
      
        // The current state switches to Rejected
        self.status = 'rejected';
      
        // Batch execute the tasks in the Rejected queue
        self.onRejectedQueue.forEach(rejected= > rejected(self.reason));
    }
  
    // Assign resolve and reject capabilities to the actuator
    executor(resolve, reject);
}
Copy the code

Let’s write the resolution program! The resolutionProcedure is a key part of this section, so keep an eye out for the line-by-line parsing in the comments

function resolutionProcedure(promise2, x, resolve, reject) {
    // hasCalled is used to ensure that resolve and reject are not repeated
    let hasCalled;
  	
  	
    if (x === promise2) {
      
        // Resolution procedure specification: If resolve is the same as promise2, reject is rejected to avoid an infinite loop
        return reject(new TypeError('To avoid an endless loop, throw an error here'));
      
    } else if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
      
        // Resolver specification: if x is an object or function, then additional processing is required
        try {
          
            // If it has a thenable then method
            let then = x.then;
          
            // If it is a thenable object, then the promise method points to x.teng.
            if (typeof then === 'function') {
              
                // If then is a function, call it with x for this
              	// The first parameter is resolvePromise and the second parameter is rejectPromise
                then.call(x, y= > {
                  
                    // 如果已经被 resolve/reject 过了,那么直接 return
                    if (hasCalled) return;
                  
                    hasCalled = true;
                  
                    // enter the resolution program (recursively calling itself)
                    resolutionProcedure(promise2, y, resolve, reject);
                }, err= > {
                  
                    Hascalled is used in the same way as above
                    if (hasCalled) return;
                  
                    hasCalled = true;
                  
                    reject(err);
                });
            } else {
              
                // If then is not function, implement promise with x as argumentresolve(x); }}catch (e) {
            if (hasCalled) return;
          
            hasCalled = true; reject(e); }}else {
      
        // If x is not an object or function, implement a promise with x as an argumentresolve(x); }}Copy the code

This resolution is called in the then method.

// The then method takes two functions as inputs (optional)
MyPromise.prototype.then = function(onResolved, onRejected) {
  
    // Note that onResolved and onRejected must be functions; If not, we use a pass-through here
    if (typeofonResolved ! = ='function') {
        onResolved = function(x) {return x};
    }
    if (typeofonRejected ! = ='function') {
        onRejected = function(e) {throw e};
    }
  
    // Still save this
    var self = this;
  
    // This variable is used to store the return value x
    let x;
    
    // Resolve state handler function
    function resolveByStatus(resolve, reject) {
      
        // Wrap it as an asynchronous task to ensure that the resolver executes after then
        setTimeout(function() {
            try { 
                // return value assigned to x
                x = onResolved(self.value);
              
                // Enter the resolution procedure
                resolutionProcedure(promise2, x, resolve, reject);
              
            } catch (e) {
                // If onResolved or onRejected raises an error
                // promise2 must be rejectedreject(e); }}); }// reject handler
    function rejectByStatus(resolve, reject) {
        // Wrap it as an asynchronous task to ensure that the resolver executes after then
        setTimeout(function() {
            try {
                // return value assigned to x
                x = onRejected(self.reason);
              
                // Enter the resolution procedure
                resolutionProcedure(promise2, x, resolve, reject);
            } catch(e) { reject(e); }}); }Return this; return a canonical Promise object
    var promise2 = new MyPromise(function(resolve, reject) {
        // Determine the state and assign the corresponding handler
        if (self.status === 'fulfilled') {
            // resolve handler function
            resolveByStatus(resolve, reject);
        } else if (self.status === 'rejected') {
            // reject handler function
            rejectByStatus(resolve, reject);
        } else if (self.status === 'pending') {
            // If pending, the task is pushed to the corresponding queue
            self.onResolvedQueue.push(function() {
                 resolveByStatus(resolve, reject);
            });
            self.onRejectedQueue.push(function() { rejectByStatus(resolve, reject); }); }});// Return the wrapped promise2
    return promise2;
};
Copy the code

As A result, we have A Promsie that meets our expectations and passes the test cases of the Promise/A+ specification.

tips

Handwritten promises have different standards for different interviewers and different teams. For some teams, completion to the point where we didn’t add the Promise/A+ specification was enough to get the full score. If you’re new to the Promise underlying principle, it’s not unusual for you to struggle with this article. Don’t be in a hurry. If you have time, try to read it several times and type it out line by line.

Learning to expand

Promises/A+ -英文 Promises/A+ – translation