New Git knowledge repositoryFront end from entry to groundFor attention, star and collation needs, update from time to time ~


Overview: A journey of front End Foundation building from scratch

Give it a thumbs up if you think it’s good

Why is there a Promise

Before you get to know Promise, it’s important to know what Callback Hell is.

High concurrency without blocking is nodeJS ‘signature, and asynchrony is its basic guarantee to achieve high concurrency without blocking. Previously asynchrony was handled through pure callback functions, such as making an asynchronous request using Ajax and then using callback functions to handle the return result.

If there’s only one callback, that’s fine. However, if we were to use the results of the first async to determine the second async request, and the results of the second async request to determine the third async request, layer upon layer, we would form what is known as callback hell.

For example

function loadImg(src,cb){ const img = new Img(); img.src = src img.onload=()=>{cb()} img.onerror=()=>{cb(new Error('failed to load '+src )) } } loadImg('img1', err=>{ if(err===undefined) { f1(); } else { console.log(err.message); }})Copy the code

Single processing logic, very easy to handle, but if you add two more layers of logic

loadImg('img1', err=>{ if(err===undefined) { f1(); loadImg('img2', err=>{ if(err===undefined) { f2(); loadImg('img3', err=>{ if(err===undefined) { f3(); } else { console.log(err.message); } }) } else { console.log(err.message); } }) } else { console.log(err.message); }})Copy the code

Each additional layer of callback creates two more layers of judgment, and eventually, the problem is solvable, but difficult to read and maintain.

Promise was born to solve this problem — essentially turning deep callbacks into chained calls, which were more human logical.

 function loadImg(src){
     const img = new Img();
      img.src = src 
      return new Promise((resolve, reject)=>{
          img.onload=()=>{resolve('success '+ src)}
          img.onerror=()=>{reject(new Error('failed to load '+src ))  }
      })
  }
Copy the code

Change the nested callback to a chained call

loadImg('img1') .then(str=>{ console.log(str); f1(); Return loadImg('img2') // return a Promise}) then. Then (STR =>{console.log(STR); f2(); Return loadImg('img3') // return a Promise}). Then (STR =>{console.log(STR); f3(); }) .catch(er=>{ console.log(er.message); })Copy the code

The whole logic is clear and refreshing.

Know the Promise

The concept of Promise is described in MDN as follows:

A Promise object is a proxy object (proxy for a value), and the proxied value may not be known at the time the Promise object is created. It allows you to bind the respective handling methods for the success and failure of asynchronous operations. This allows asynchronous methods to return values as synchronous methods do, but instead of immediately returning final execution results, a Promise object that represents future results

At the heart of Promise are asynchronous chained calls. It uses three techniques to solve callback hell:

  • The callback function delays binding.
  • Return value through.
  • Error bubbling.

As in the previous example, the callback function is not declared directly, but is passed in via the later THEN method, that is, deferred. This is the callback function delayed binding.

Different types of Promises are created based on the incoming value of the callback function in then, and the returned Promise is then penetrated into the outer layer for subsequent calls. This is return value penetration. This, together with the delayed binding of the callback function, creates the effect of a chain call.

In the whole chain call process, once an error is reported by a process, the resulting error is passed back to the last catch. This is error bubbling.

Why microtasks

The execution function in the Promise is synchronous, but there is an asynchronous operation that calls either resolve when the asynchronous operation ends or Reject when it fails, both of which enter the EventLoop as microtasks.

Using synchronous calls? Synchronous calls block the entire script, with the current task waiting and subsequent tasks unable to execute, and the effect of delayed binding cannot be achieved.

Asynchronously called as a macro task?

In browsers, macro tasks and microtasks are executed alternately. In simple terms, one macro task is executed, followed by the entire list of microtasks, followed by another macro task.

Callbacks are executed as macro tasks and are placed at the end of the current macro task queue. If the current macro task queue is too long, a corresponding micro task is created for each macro task. When such callbacks are not executed, they cause application stalling.

Asynchronously called as a microtask

Resolve or Reject are placed at the end of the microtask queue. Once the macro task is complete, the browser processes the microtasks until the microtask queue is empty. In this way, delayed binding is not affected, and there is no application lag due to long waiting times.

Write a Promise

No matter how implemented, as long as you follow the Promise A + specification, that’s a Promise class.

The complete code for this example moves to code

Promise specification

  1. A Promise is a class that needs to pass in an Executor executor, which is executed immediately by default.
  2. Promises internally provide two methods (on non-prototype objects) that are passed to the Executor executor methods to change the state of the promise.
  3. A Promise has three states: Pending, depressing or Rejected. A Promise must be in one of these states
  4. A Promise can only change from a wait to a success or a wait to a failure, and once the change is complete, the state is not allowed to change.
  5. There will be a THEN method on each promise instance that accepts two optional parameters, onFulfilledandOnRejected, the successful and failed callback

Implement then and catch

  1. So let’s write an initial version and implement it
  • The callback function delays binding
  • Multiple binding callback functions are supported
Const PENDING = "PENDING "; const RESOLVE = "resolve"; const REJECTED = "rejected"; class MyPromise { constructor(exector) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onFulfilledCbs = []; this.onRejectedCbs = []; const resolve = (value) => { if (this.status ! == PENDING) {// Only allow the state to change from waiting to success or failure, and do not allow multiple calls to return; } setTimeout(() => {// the js code itself can not set the microtask, here uses macro task to implement asynchronous operation this.value = value; // Store the result of successful execution this.status = RESOLVE; / / set the status to this success. OnFulfilledCbs. ForEach (= > {(cb) / / in order to perform the current storage of all successful callback function cb (. This value); }); }); }; const rejected = (reason) => { if (this.status ! == PENDING) {// Only allow the state to change from waiting to success or failure, and do not allow multiple calls to return; } setTimeout(() => { this.reason = reason; this.status = REJECTED; this.onRejectedCbs.forEach((cb) => { cb(reason); }); }); }; exector(resolve, rejected); } then = (this is a big pity, onRejected) => {if (this. Status === PENDING) {// If (this. Then store the successful and failed callback this. ononledCBs. push(ondepressing); this.onRejectedCbs.push(onRejected); This. Status === RESOLVE) {// This. } else { onRejected(this.reason); } return this; }; }Copy the code

Let’s do that

let readFilePromise = (filename) => { return new MyPromise((resolve, reject) => { setTimeout(() => { resolve(filename); }, 1000); }); }; const p1 = readFilePromise("./001.txt"); Then ((data) => {console.log(' first callback '+ data.tostring ()); }); P1. then((data) => {console.log(' second callback '+ data.tostring ()); }); Then ((data) => {console.log(' third callback '+ data.tostring ()); }); TXT Second callback./001.txt Third callback./001.txtCopy the code
  1. The second step is to add the chain call function.

In the original example, each loadImg function returns a Promise after execution.

 function loadImg(src){
     const img = new Img();
      img.src = src 
      return new Promise((resolve, reject)=>{
          img.onload=()=>{resolve('success '+ src)}
          img.onerror=()=>{reject(new Error('failed to load '+src ))  }
      })
  }
Copy the code

Thus, in a chain call, each function in the THEN method is executed after the execution of the previous THEN method. This means that when the then method completes, it must return a promise that the next.THEN will be executed correctly.

In the code we implemented, the then method returns itself

 then = (onFulfilled, onRejected) => {
    // ...
    // ...
    return this;
  };
Copy the code

This ensures that we can use chained calls, but the chained calls essentially bind the callback function multiple times.

const p1 = readFilePromise("./001.txt"); Then ((data) => {console.log(' first callback '+ data.tostring ()); }). Then (= > {(data). The console log (' second callback + data. The toString ()); }). Then (= > {(data). The console log (' third callback + data. The toString ()); }); TXT Second callback./001.txt Third callback./001.txtCopy the code

In a change to the test code, each THEN function, after execution, returns a Promise object for the next action to be performed. As shown below, the returned Promise object is discarded.

const p1 = readFilePromise("./001.txt"); Then ((data) => {console.log(" first callback "+ data.tostring ()); return readFilePromise("./002.txt"); }). Then (= > {(data). The console log (" is the second callback "+ data. The toString ()); return readFilePromise("./003.txt"); }). Then (= > {(data). The console log (" is the third callback "+ data. The toString ()); Console. log(" 003 successfully read ", data); }); TXT Second callback./001.txt Third callback./001.txtCopy the code

The tricky part about Promsie is when the return value of then is a Promise object.

Inside the THEN method, we essentially return a new Promsie object, which we call thenPromsie, and then need to invoke thenPromsie’s reslove method to trigger the execution of the next THEN callback.

If the then internal return value is a value, thenPromsie’s resolve method is simply called. If the return value is a Promsie object, We decide whether thenPromsie is resolved based on whether the Promise object is resolved, and then whether to execute the next then callback.

Make a change to the then method, using PENDING state as an example

If (this.status === PENDING) {// Make a Promise, Let promise2 = new MyPromise((resolve, rejected) => { this.onFulfilledCbs.push((value) => { try { const x = onFulfilled(value); // If the promise returned by the callback function is the same as the promise constructed in the then method, throw an exception, If (x === promise2) {return Reject (new TypeError('[TypeError: Chaining cycle detected for promise #<Promise>]')); } if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise); } else { resolve(x); }} catch (error) {rejected(error); }}); this.onRejectedCbs.push((reason) => { try { const x = onRejected(reason); If (x === promise2) {return reject(new TypeError('[TypeError: Chaining cycle detected for promise #<Promise>]')); } if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise); } else { resolve(x); }} catch (error) {rejected(error); }}); }); return promise2;Copy the code

This is the logic that processes the onFulfilled/onRejected return value

resolvePromise = (promise2, x, resolve, If (x === promise2) {if (x == promise2) {if (x == promise2) {if (x == promise2) {if (x == promise2) {if (x == promise2) {if (x == promise2) {if (x == promise2) { Return Reject (new TypeError('[TypeError: Chaining cycle detected for Promise #< promise >]')); } if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise) {if (x instanceof MyPromise); } else { resolve(x); }}Copy the code

The current then function code is as follows

Then = (this. Status === PENDING) => {if (this. Status === PENDING) {// this. Let promise2 = new MyPromise((resolve, rejected) => { this.onFulfilledCbs.push((value) => { try { const x = onFulfilled(value); // Get the return value of this. ResolvePromise (promise2, x, resolve, rejected); } catch (error) { rejected(error); // Rejected}}); this.onRejectedCbs.push((reason) => { try { const x = onRejected(reason); this.resolvePromise(promise2, x, resolve, rejected); } catch (error) { rejected(error); }}); }); return promise2; } else if (this.status === RESOLVE) { let promise2 = new MyPromise((resolve, rejected) => { try { const x = onFulfilled(this.value); this.resolvePromise(promise2, x, resolve, rejected); } catch (error) { rejected(error); }}); return promise2; } else { let promise2 = new MyPromise((resolve, rejected) => { try { const x = onRejected(this.reason); this.resolvePromise(promise2, x, resolve, rejected); } catch (error) { rejected(error); }}); return promise2; }};Copy the code
  1. Fix a logic bug
  • Handles the case where two arguments in then are null

To judge the case that the parameters are not transmitted:

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; OnRejected = typeof onRejected === 'function'? onRejected : error => { throw error }; // Failed callback throws an exception directlyCopy the code
  1. implementationcatchmethods

The catch method is essentially a then method that has no successful callback. namely

catch = (onRejected) => {
  return this.then(null, onRejected);
}
Copy the code

If a Promise with a PENDING state fails, the state of the new Promise will fail, and then the onRejected function will be executed. If the then method fails, the default function will throw an exception, and the new Promise state will fail. The new Promise state changes to onRejected…… if it fails This continues until the error is caught and the drop is stopped. This is the Promise error bubble mechanism.

To realize the Resolve

Create a successful promise

static resolve = (param) => { if(param instanceof MyPromise){ return param; } return new MyPromise((resolve, reject) => { resolve(param); })};Copy the code

Realize the Reject

The passed argument is passed down as a reason

  static reject = (reason) => {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  };
Copy the code

Realize the Finall

The finally() method is used to specify actions that will be performed regardless of the final state of the Promise object. This method passes on the value exactly as it is.

finally = (callback) => {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => {
        return value;
      });
    }, error => {
      return MyPromise.resolve(callback()).then(() => {
        throw error;
      });
    });
  }
Copy the code

To achieve all

  • The argument is an array of promises that are executed in parallel.
  • Eventually a new promise is returned
  • All it takes is one promise state to changerejected, so the final state of the new pomise returned isrejected.
  • When all promises becomefulfilled, and eventually the returned state of the new promise will becomefulfilled, and returns an array.
static all = (promises) => { return new MyPromise((resolve, reject) => { let result = []; let count = 0; let len = promises.length; if (len === 0) { resolve(result); return; } promises.forEach((promise, Index) => {myPromise.resolve (promise[index]) // Promise [index] may not be a promise. Then ((data) => {result[index] = data; count++; if (count === len) { resolve(result); } }) .catch((err) => { reject(err); }); }); }); };Copy the code

Realize the race

Resolve and stop as soon as a promise completes.

static race = (promises) => { return new MyPromise((resolve, reject) => { let len = promises.length; if (len === 0) { resolve(result); return; } promises.forEach((promise, Index) => {myPromise.resolve (promise[index]) // Promise [index] may not be a promise.then ((data) => {resolve(data); return; }) .catch((err) => { reject(err); }); }); }}})Copy the code

Associated problems

Implement Ajax concurrent request control

This is an interview question that examines the use of recursion and promise.

Solution: Use a while loop to control the maximum number of concurrent tasks, using recursion to keep the task executing.

Function multiRequest(urls = [], maxNum) {// Total number of requests const len = urls.length; // Create an Array based on the number of requests const result = new Array(len).fill(false); let index = 0; function next() { new Promise((resolve, reject) => { const current = index; const url = urls[index]; index++; SetTimeout (()=>{result[current] = url; resolve(url) }, 1000); }). Then ((data) => {console.log(' finish task ->', data); If (index < len) {// Continue next() with recursive control task; }}); } while (maxNum) { next(); maxNum -= 1; }} multiRequest ([1, 2, 3, 4, 5], 2)Copy the code

Supplement knowledge

Promise/A + specification

At the beginning of the last section, I briefly introduced the Promise/A+ specification body structure in order to standardize the idea of the code. Many details have not been expanded and this article does not plan to introduce the specification in detail, attached here

  • A Baidu Promise/A+ specification article.
  • Address: promisesaplus.com
  • For each rule in this article, see its test repository: github.com/promises-ap…

As an additional note, to simplify the code, we assume that our code execution environment is only MyPromise, so no other implementation that conforms to the Promise specification is handled in the code above. To implement this, refer to documentation 3.

Design patterns

Promises inherit some of the ideas of the observer and publish-subscribe design patterns, and here’s a blog post looking at promises from a design pattern perspective: Handwritten promises are easy

Differences between Promise and Async

This information comes from reference 2

Async is the syntactic sugar of Generator functions. The difference is that Generator functions are called manually, whereas async functions are await before they automatically execute the next await statement, whether asynchronous or synchronous.

Await can be followed by a number of values such as basic data types, (characters, values, boilers, etc. will be automatically converted to immediate Resolved Promsie) Promise objects.

Internally async is executed asynchronously. No matter whether await is followed by synchronous or asynchronous task, eventually async function will return a Promise object, so async function can be regarded as a Promise object wrapped by multiple asynchronous operations. * Async makes Promsie easier to use. *

If we want to send a lot of requests sequentially

The use of Promise

Function logInOrder(urls) {const textPromises = urls.map(URL => {return fetch(URL). Then (response =>) response.text()); }); TextPromises. Reduce ((chain, textPromise) => { return chain.then(() => textPromise) .then(text => console.log(text)); }, Promise.resolve()); }Copy the code

Using async

async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); }}Copy the code

If you’re sending a lot of requests in parallel

Async function logInOrder(urls) {// Async function logInOrder(urls) {const textPromises = urls.map(async URL => {const response = await fetch(url); return response.text(); }); // Output for (const textPromise of textPromises) {console.log(await textPromise); }}Copy the code

In the above code, although the map method takes an async function as an argument, it is executed concurrently, because only the async function is executed internally, and the outside is not affected. Behind the for… The of loop uses await inside, so it outputs sequentially.

Reference documentation

  1. Intensive READING JS Series (9B) Promise — Callback hell, Promise constructor
  2. Twelve questions of the soul
  3. Promise (1) – written Promise