Write in front:

This is a summary article, but can also be understood as a translation, the main context of reference from the following article:

www.mattgreer.org/articles/pr…

If English reading is accessible, wall crack recommends reading this article.

preface

I often use Promises grammar in my daily code writing. When I thought I knew the details of Promises, I was asked in a discussion, “Do you know what promises are like inside?” Yes, in retrospect, I just knew how to use it, but I didn’t really know how it worked. This article is my own review and summary of Promises. If you read the whole article, I hope you will have a better understanding of how Promises are made and how they work.

We will start from scratch and make promises of our own. The final code will be similar to Promises/A+ specification and will understand how important Promises are in asynchronous programming. Of course, this article assumes that the reader already has a basic knowledge of Promises.

Promises are the simplest

Let’s start with The simplest promises. When we want to put the following code

doSomething(function(value) {
  console.log('Got a value:' + value);
});
Copy the code

Into a

doSomething().then(function(value) {
  console.log('Got a value:' + value);
});
Copy the code

At this point, what do we need to do? A very simple way to do this is to write the doSomething() function out of its original form

function doSomething(callback) {
  var value = 42;
  callback(value);
}
Copy the code

Change to writing ‘promise’ like this:

function doSomething() {
  return {
    then: function(callback) {
      var value = 42; callback(value); }}; }Copy the code

The above is just a syntactic sugar wrapper for the callback method, which seems meaningless. But this is a very important change. We have begun to touch one of the core ideas of Promises:

Promises catch the eventual values and put them into an Object.

Ps: It is necessary to explain the concept of “final value”. It is the return value of an asynchronous function, and its status is uncertain, and it may succeed or fail (figure below).

More on Promises and Eventual Values comes later.

Define a simple Promise function

The simple rewriting above doesn’t say anything about promise’s features, so let’s define an actual promise function:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    callback(value);
  }

  fn(resolve);
}
Copy the code

Code parsing: Split the writing of THEN and introduce the resolve function to handle incoming Promise objects. Also, use callback as a bridge between the THEN and resolve functions. This code implementation, it kind of looks like a Promise, doesn’t it?

Based on this, our doSomething() function will be written in this form:

function doSomething() {
  return new Promise(function(resolve) {
    var value = 42;
    resolve(value);
  });
}
Copy the code

When we try to execute, we will find that the execution will report an error. This is because, in the code implementation above, resolve() is called earlier than then, when the callback is null. To solve this problem, we can use setTimeout to hack:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    // Force the callback here to be next in the event loop
    // called in an iteration, so that then() will be executed before it
    setTimeout(function() {
      callback(value);
    }, 1);
  }

  fn(resolve);
}
Copy the code

With this modification, our code will run successfully.

This code sucks

The implementation we envision is one that can work well in asynchronous situations. But the code, at this point, is very fragile. As long as our then() function contains asynchronous cases, the variable callback will once again become NULL. If the code is so lame, why even write it down? Because the above pattern is very convenient for us to expand later, at the same time, this simple writing method, can also let the brain have a preliminary understanding of the work of then and resolve. Now let’s consider making some improvements on this basis.

Promises are big.

Promises are states. We need to know what states are in Promises:

A promise will be in the pending state when it waits for a final value, and in the Resolved state when it gets a final value.

When a promise succeeds in reaching a final value, it holds that value and will not resolve again.

(Of course, a promise can also be rejected, as discussed below)

To introduce state into our code implementation, we rewrote the original code as follows:

function Promise(fn) {
  var state = 'pending';
  //value represents the argument passed through the resolve function
  var value;
  // Deferred holds function arguments inside then()
  var deferred;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) { handle(deferred); }}function handle(onResolved) {
    if(state === 'pending') {
      deferred = onResolved;
      return;
    }

    onResolved(value);
  }

  this.then = function(onResolved) {
    handle(onResolved);
  };

  fn(resolve);
}
Copy the code

This code looks even more complicated. At this point, however, the code lets the caller call either the THEN () method or the resolve() method at will. It can also run synchronously and asynchronously.

Code parsing: The code uses the state flag. Meanwhile, then() and resolve() extract the common logic into a new function handle() :

  • whenthen()thanresolve()When called earlier, the state was pending and the corresponding value was not ready. We will bethen()The corresponding callback parameters are stored in the Deferred so that promise can call when the resolved message is obtained.
  • whenresolve()thanthen()When it is called earlier, the state is set to Resolved and the corresponding value has been obtained. whenthen()When called, call directlythen()Inside the corresponding callback parameters can be.
  • Due to thethen()withresolve()Extract the common logic into a new functionhandle()So no matter which of the above two cases is triggered, the Handle function will eventually be executed.

If you look closely, the setTimeout is gone. We have the correct order of execution through state control. Of course, there will be times when setTimeout is used in the following articles.

By using promise, the order in which we invoke the corresponding methods is unaffected. Calling resolve() over then() at any point does not affect its internal logic as long as it meets our needs.

At this point, we can try calling the THEN method multiple times and find that we get the same value each time.

var promise = doSomething();

promise.then(function(value) {
  console.log('Got a value:', value);
});

promise.then(function(value) {
  console.log('Got the same value again:', value);
});
Copy the code

Chain Promises

In our daily programming for Promises, the following chained pattern is common:

getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
Copy the code

GetSomeData () returns a promise by calling the then() method. But remember, the return value of the first THEN () method must also be a promise. This promises the chain.

The then() method must always return a promise.

To achieve this, we have modified the code further:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) { handle(deferred); }}function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    if(! handler.onResolved) { handler.resolve(value);return;
    }

    var ret = handler.onResolved(value);
    handler.resolve(ret);
  }

  this.then = function(onResolved) {
    return new Promise(function(resolve) {
      handle({
        onResolved: onResolved,
        resolve: resolve
      });
    });
  };

  fn(resolve);
}
Copy the code

The current hula ~ code seems to be a bit crazy 😩. Hahaha, are you glad our code wasn’t that complicated in the first place? The real key here is this: the then() method always returns a new promise.

doSomething().then(function(result){
  console.log("first result : ", result);
  return 88;
}).then(function(secondResult){
  console.log("second result : ", secondResult);
  return 99;
})
Copy the code

Let’s take a closer look at the resolve process for the second promise. It accepts the value from the first promise. The detailed procedure occurs at the bottom of the handle() method. The input handler takes two arguments: an onResolved callback and a reference to the resolve() method. Here, each new promise has a copy of the internal method resolve() and its corresponding runtime closure. This is the bridge that connects the first promise to the second promise.

In the code, we get the value of the first promise:

var ret = handler.onResolved(value);
Copy the code

In the example above, handler.onResolved says:

function(result){
  console.log("first result : ", result);
  return 88;
}
Copy the code

That is, handler.onResolved is actually returning the argument (function) that was passed in when the first Promise’s then was called. The return value from the first handler is used for the resolve passed parameter of the second Promise.

That’s how the whole chain promise works.

What if we want to return the result of all then’s? We can use an array to store each return value:

doSomething().then(function(result) {
  var results = [result];
  results.push(88);
  return results;
}).then(function(results) {
  results.push(99);
  return results;
}).then(function(results) {
  console.log(results.join(', ');
});

// the output is
//
/ / 42, 88, 99
Copy the code

Promises forever Resolve returns a value. When you want to return multiple values, you can do so by creating some conformance structure (such as groups, objects, etc.).

The passed argument in then is optional

The passed argument (callback function) in then() is not required. If empty, the return value of the previous promise will be returned in the chained promise.

doSomething().then().then(function(result) {
  console.log('got a result', result);
});

// the output is
//
// got a result 42
Copy the code

If a promise has no then argument, it will resolve the value of the previous promise:

if(! handler.onResolved) { handler.resolve(value);return;
}
Copy the code

Returns the new promise in the chained promise

Our chained promise implementation is still a bit simple. Resolve returns a simple value. What if you want resolve to return a new promise? For example:

doSomething().then(function(result) {
  // doSomethingElse returns a Promise
  return doSomethingElse(result);
}).then(function(finalResult) {
  console.log("the final result is", finalResult);
});
Copy the code

If that’s the case, then our code above doesn’t seem to be able to handle that. For the promise that follows, the value it gets will be a promise. To get the expected value, we need to do this:

doSomething().then(function(result) {
  // doSomethingElse returns a Promise
  return doSomethingElse(result);
}).then(function(anotherPromise) {
  anotherPromise.then(function(finalResult) {
    console.log("the final result is", finalResult);
  });
});
Copy the code

OMG… This is a terrible implementation. Should I, as a user, have to manually write the redundant code every time? Can you handle this logic inside the Promise code? In fact, we just need to add a little judgment to resolve() in our existing code:

function resolve(newValue) {
  if(newValue && typeof newValue.then === 'function') {
    newValue.then(resolve);
    return;
  }
  state = 'resolved';
  value = newValue;

  if(deferred) { handle(deferred); }}Copy the code

As we saw in the code logic above, resolve() will be iterated over if a promise is encountered. Until the value finally obtained is no longer a promise, the existing logic continues.

One more point worth noting: how does the code determine if an object has a Promise property? By determining whether this object has a then method. This method of determination is called “duck type” (we don’t care what type of object it is, whether it’s a duck or not, just the behavior).

This loose definition makes specific promise implementations compatible with each other.

Promises of rejecting

In the chained Promise chapter, our implementation is relatively complete. But we didn’t talk about mistakes in Promises.

If something goes wrong during the promise decision, the promise will throw out a rejection decision with a reason. How does the caller know that an error has occurred? We can pass the second argument (function) to the then() method:

doSomething().then(function(value) {
  console.log('Success! ', value);
}, function(error) {
  console.log('Uh oh', error);
});
Copy the code

As mentioned above, a promise transitions from its initial state Pending to either Resolved or Rejected. Only one of these can be the final state. Only one of the two arguments corresponding to then() is actually executed.

In the promise internal implementation, a reject() function is also allowed to handle the reject state, which can be considered a twin of resolve(). The doSomething() function will also be rewritten to support error handling:

function doSomething() {
  return new Promise(function(resolve, reject) {
    var result = somehowGetTheValue();
    if(result.error) {
      reject(result.error);
    } else{ resolve(result.value); }}); }Copy the code

How can our code adapt to this? Look at the code:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    if(newValue && typeof newValue.then === 'function') {
      newValue.then(resolve, reject);
      return;
    }
    state = 'resolved';
    value = newValue;

    if(deferred) { handle(deferred); }}function reject(reason) {
    state = 'rejected';
    value = reason;

    if(deferred) { handle(deferred); }}function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    var handlerCallback;

    if(state === 'resolved') {
      handlerCallback = handler.onResolved;
    } else {
      handlerCallback = handler.onRejected;
    }

    if(! handlerCallback) {if(state === 'resolved') {
        handler.resolve(value);
      } else {
        handler.reject(value);
      }

      return;
    }

    var ret = handlerCallback(value);
    handler.resolve(ret);
  }

  this.then = function(onResolved, onRejected) {
    return new Promise(function(resolve, reject) {
      handle({
        onResolved: onResolved,
        onRejected: onRejected,
        resolve: resolve,
        reject: reject
      });
    });
  };

  fn(resolve, reject);
}
Copy the code

Not only a Reject () function has been added, but the handle() method has also been added to handle the reject() logic. The handle() method uses state to decide what to do with the Handler’s Reject /resolved code.

An unknowable error should also trigger rejection

The above code only handles known errors. Rejection should also be thrown when some unknowable error occurs. We need to add a try… to the corresponding handler. The catch:

The first is in the resolve() method:

function resolve(newValue) {
  try {
    // ... as before
  } catch(e) { reject(e); }}Copy the code

Similarly, an unknown error may occur when handle() executes a specific callback:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
Copy the code

Promises swallow mistakes

Sometimes a wrong interpretation of promises will lead to promises swallowing mistakes. This is a common pitfall for developers.

Let’s consider this example:

function getSomeJson() {
  return new Promise(function(resolve, reject) {
    var badJson = "
      
uh oh, this is not JSON at all!
"
; resolve(badJson); }); } getSomeJson().then(function(json) { var obj = JSON.parse(json); console.log(obj); }, function(error) { console.log('uh oh', error); }); Copy the code

How does this code work? Resolve in THEN () performs parsing of JSON. It thinks it will execute, but instead throws an exception because the value passed in is not in JSON format. We wrote an error callback to catch this error. There’s no problem with that, right?

No, the results may not be what you expected. The error callback does not fire at this point. The result will be no log output on the console. The mistake was quietly swallowed.

Why is that? Because our error occurred inside the resolve callback of then(), the source code says it occurred inside the handle() method. As a result, the new promise returned by then() will fire reject, instead of the existing promise firing reject:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
  	Handler. Reject ()
  	// This is the new promise reject() returned by then()
  	// Handler. OnRejected (ex) will trigger this promise reject()
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
Copy the code

Handler. OnRejected (ex); What will trigger is the Promise reject(). But this breaks the principle of Promises:

A promise transitions from its initial state pending to either Resolved or Rejected. Only one of these can be the final state. Only one of the two arguments corresponding to then() is actually executed.

Because the Resolved state has been triggered, it is impossible to trigger the Rejected state again. This error will be caught by the next promise when the resolved function is executed.

We can verify this as follows:

getSomeJson().then(function(json) {
  var obj = JSON.parse(json);
  console.log(obj);
}).then(null.function(error) {
  console.log("an error occured: ", error);
});
Copy the code

This is probably the most dishonest point in Promises. Of course, it can be easily avoided by understanding the causes. What solutions do we have to circumvent this pit for a better experience? Look at the next section:

Done () to help

Most promise libraries include a done() method. It implements similar functionality to the THEN () method, but nicely avoids the then() pitfalls mentioned earlier.

The done() method can be called just like then(). There are two main differences between the two:

  • done()The method does not return a promise
  • done()Any errors in the promise implementation will not be caught (thrown directly)

In our example, it would be safer to use the done() method:

getSomeJson().done(function(json) {
  // when this throws, it won't be swallowed
  var obj = JSON.parse(json);
  console.log(obj);
});
Copy the code

Recover from the Rejection

It is possible to recover from the Rejection in the promise. If you add more then() methods to a promise with rejection, then the normal flow of chained promises will continue from that THEN () :

aMethodThatRejects().then(function(result) {
  // won't get here
}, function(err) {
  // since aMethodThatRejects calls reject()
  // we end up here in the errback
  return "recovered!";
}).then(function(result) {
  console.log("after recovery: ", result);
}, function(err) {
  // we won't actually get here
  // since the rejected promise had an errback
});

// the output is
// after recovery: recovered!
Copy the code

The Promise resolution must be asynchronous

At the beginning of this article, we used a hack to make our simple code allow correctly. Remember? A setTimeout is used. After we perfected the logic, the hack was no longer used. But the truth is: Promises/A+ require Promises to be one step. To implement this requirement, the easiest way to do this is to wrap our handle() method with setTimeout again:

function handle(handler) {
  if(state === 'pending') {
    deferred = handler;
    return;
  }
  setTimeout(function() {/ /... as before }, 1); }Copy the code

Very simple implementation. However, actual Promises libraries do not favor setTimeout. They tend to use Process.nexttick if the corresponding library is for NodeJS, and setImmediate if the corresponding library is for browsers.

why

We know how to do it, but why is there a requirement in the specification?

To ensure a consistent and reliable execution process. Let’s consider this case:

var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
Copy the code

How would the above code be executed? Based on the naming, you might imagine that the execution would look like this: invokeSomething() -> invokeSomethingElse() -> wrapItAllUp(). But really, it depends on whether the Promise’s resolve process is synchronous or asynchronous in our current implementation. If the promise execution of doAnOperation() was asynchronous, its execution would be the envisaged flow. If doAnOperation() ‘s promise execution were synchronous, its actual execution would be invokeSomething() -> wrapItAllUp() -> invokeSomethingElse(). This can lead to some unintended consequences.

Therefore, in order to ensure consistency and reliable execution process. Promise’s resolve process is required to be asynchronous, even though it may itself be a simple synchronization process. By doing so, all user experiences are consistent and developers no longer need to worry about compatibility in different situations.

conclusion

If you’ve read this far, you know it’s true love! We talked about the core concepts of Promises. Of course, much of the code implementation in this article is rudimentary. There may also be some deviation from the actual code base implementation. But I hope it will not hinder your understanding of the overall promises. For more details on Promises (e.g., All (), Race, etc.), see the documentation and source code implementation.

When I really understood how Promises work and some of its boundary conditions, I really liked Promises. Since then, the code about Promises in my project has become more concise. There is much to discuss about Promises. This article is just the beginning.


JavaScript in-depth series:

“var a=1;” What’s going on in JS?

Why does 24. ToString report an error?

Here’s everything you need to know about “JavaScript scope.

There are a lot of things you don’t know about “this” in JS

JavaScript is an object-oriented language. Who is for it and who is against it?

Deep and shallow copy in JavaScript

JavaScript and the Event Loop

From Iterator to Async/Await

Explore the detailed implementation of JavaScript Promises