8 advanced usages of Promise that you don’t know

Scott Hu - Aug 22 '23 - - Dev Community

I found that many people still only know the regular usage of promise

In javascript projects, the use of promise should be essential, but I found that among my colleagues and interviewers, many intermediate or above front-ends are still stuck in promiseInst.then(), promiseInst.catch() , Promise.all and other conventional usages, even async/await just knows what it is, but don't know more.

But in fact, there are many clever advanced usages of promise, and some advanced usages are also widely used in the alova request strategy library.

But in fact, there are many clever advanced usages of promise, and some advanced usages are also widely used in the alova request strategy library.
Now, I will share these with everyone here without reservation. After reading it, you should never be overwhelmed by the question again. There is a finale question at the end.

If you think it is helpful to you, please like, collect and comment!

and many advanced usages below has been used at alova, but what is alova? it is a request strategy library that allows you to write less code to achieve efficient data interaction in specific business scenarios. It has received 1500+ stars on github since its release in the past 3 months, alova official website, you can take a look if you are interested.

1. Serial execution of promise array

For example, if you have a set of interfaces that need to be executed serially, you may first think of using await

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
for (const requestItem of requestAry) {
   await requestItem();
}
Enter fullscreen mode Exit fullscreen mode

If you use promise instead of async/await, you can use then function to connect multiple promises in series to achieve serial execution.

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
const finallyPromise = requestAry. reduce(
     (currentPromise, nextRequest) => currentPromise.then(() => nextRequest()),
     Promise.resolve() // Create an initial promise to link the promises in the array
);

Enter fullscreen mode Exit fullscreen mode

2. Changing state outside the scope of new Promise

Suppose you have some functions on multiple pages that need to collect user information before allowing them to be used. Before clicking to use a certain function, a pop-up box for information collection pops up. How would you implement it?
The following are the implementation ideas of FE developments of different levels:

Junior FE: I will write a modal, then copy and paste the codes to other pages, the efficiency is very good!

Intermediate FE: The above is not easy to maintain, we need to wrap this component, and import it on the specific page!

High level FE: does that need to wrap as a component??? Wouldn't it be better to write a method call where all pages can be called?

Now we see how the high level FE implement it, take vue3 as an example to see the following example.

<!-- App.vue -->
<template>

   <!-- The following is the modal box component -->
   <div class="modal" v-show="visible">
     <div>
       User name: <input v-model="info.name" />
     </div>
     <!-- Additional Information -->
     <button @click="handleCancel">Cancel</button>
     <button @click="handleConfirm">Submit</button>
   </div>

   <!-- Page Components -->
</template>

<script setup>
import { provide } from 'vue';

const visible = ref(false);
const info = reactive({
   name: ''
});
let resolveFn, rejectFn;

// Pass the information collection function to the following
provide('getInfoByModal', () => {
   visible. value = true;
   return new Promise((resolve, reject) => {
     // Assign the two functions to the outside, breaking the promise scope
     resolveFn = resolve;
     rejectFn = reject;
   });
})

const handleConfirm = () => {
   resolveFn && resolveFn(info);
};
const handleCancel = () => {
   rejectFn && rejectFn(new Error('User has canceled'));
};
</script>
Enter fullscreen mode Exit fullscreen mode

Next, directly call getInfoByModal to use the modal to easily get the data filled in by the user.

<template>
   <button @click="handleClick">Fill in information</button>
</template>

<script setup>
import { inject } from 'vue';

const getInfoByModal = inject('getInfoByModal');
const handleClick = async () => {
   // After the call, a modal box will be displayed. After the user clicks Confirm, the promise will be changed to the fullfilled state, so as to get the user information
   const info = await getInfoByModal();
   await api. submitInfo(info);
}
</script>
Enter fullscreen mode Exit fullscreen mode

This is also a way of encapsulating commonly used components in many UI component libraries.

3. Alternative usage of async/await

Many people only know that await is used to receive the return value when async function is called, but they don’t know that async function is actually a function that returns a promise. For example, the following two functions are equivalent:

const fn1 = async () => 1;
const fn2 = () => Promise. resolve(1);

fn1(); // Also returns a promise with value 1
Enter fullscreen mode Exit fullscreen mode

In most cases, await is followed by a promise object and waits for it to become fullyfilled, so the following fn1 function waits are also equivalent:

await fn1();

const promiseInst = fn1();
await promiseInst;
Enter fullscreen mode Exit fullscreen mode

However, await also has a little-known secret. When it is followed by a value of a non-promise instance, it will wrap this value with a promise instance, so the code after await must be executed asynchronously. For example:

Promise. resolve(). then(() => {
   console. log(1);
});
await 2;
console. log(2);
// print order bits: 1 2
Enter fullscreen mode Exit fullscreen mode

Equivalent to

Promise. resolve(). then(() => {
   console. log(1);
});
Promise. resolve(). then(() => {
   console. log(2);
});
Enter fullscreen mode Exit fullscreen mode

4. Promise implements request sharing

When a request has been sent but has not yet been responded to, the same request is sent again, which will cause a waste of requests. At this time, we can share the response of the first request to the second request.

request('GET', '/test-api').then(response1 => {
   //...
});
request('GET', '/test-api').then(response2 => {
   //...
});
Enter fullscreen mode Exit fullscreen mode

The above two requests are actually sent only once, and the same response value is received at the same time.

So, what are the usage scenarios for request sharing? I think there are three of the following:

  1. When a page renders multiple internal self-obtaining components at the same time.
  2. The submit button is not disabled, and the user clicks the submit button multiple times in succession.
  3. In the case of preloading data, the preloading page is entered before the preloading is completed.

This is also one of the advanced functions of alova, to achieve request sharing requires the cache function of promise, that is, a promise object can pass through multiple The second await obtains the data, and the simple implementation idea is as follows:

const pendingPromises = {};
function request(type, url, data) {
   // Use the request information as the only request key to cache the promise object being requested
   // Requests with the same key will reuse the promise
   const requestKey = JSON. stringify([type, url, data]);
   if (pendingPromises[requestKey]) {
     return pendingPromises[requestKey];
   }
   const fetchPromise = fetch(url, {
     method: type,
     data: JSON.stringify(data)
   })
   .then(response => response.json())
   .finally(() => {
     delete pendingPromises[requestKey];
   });
   return pendingPromises[requestKey] = fetchPromise;
}
Enter fullscreen mode Exit fullscreen mode

5. What happens if resolve and reject are called at the same time?

Everyone knows that promises have three states of pending/fullfilled/rejected, but for example, in the example below, what is the final state of promise?

const promise = new Promise((resolve, reject) => {
   resolve();
   reject();
});
Enter fullscreen mode Exit fullscreen mode

The correct answer is the fullfilled, we just need to remember that once the promise is transferred from the pending to another status, it cannot be changed, so in the example it is transferred to the fullfilled first, and then reject is called will no longer change to the rejected.

6. Thoroughly clarify the return value of then/catch/finally

To sum it up in one sentence, the above three functions will all return a new promise wrapper object, the wrapped value is the return value of the executed callback function, and if the callback function throws an error, it will wrap a rejected promise. **, it seems that it is not very easy to understand, let's take a look at an example:

//then function
Promise.resolve().then(() => 1); // return value is new Promise(resolve => resolve(1))
Promise.resolve().then(() => Promise.resolve(2)); // returns new Promise(resolve => resolve(Promise.resolve(2)))
Promise. resolve(). then(() => {
   throw new Error('abc')
}); // return new Promise(resolve => resolve(Promise. reject(new Error('abc'))))
Promise.reject().then(() => 1, () = 2); // return value is new Promise(resolve => resolve(2))

// catch function
Promise.reject().catch(() => 3); // return value is new Promise(resolve => resolve(3))
Promise.resolve().catch(() => 4); // The return value is new Promise(resolve => resolve(promise object calling catch))

// finally function
// The return values below are all new Promise(resolve => resolve(call finally's promise object))
Promise. resolve(). finally(() => {});
Promise. reject(). finally(() => {});
Enter fullscreen mode Exit fullscreen mode

7. What is the difference between the second callback of the then function and the catch callback?

The second callback of promise's then function and catch function will both be triggered when the request fails. It seems that there is no difference, but in fact, the former cannot catch the error thrown by the current first callback function of then, but catch can.

Promise.resolve().then(
   () => {
     throw new Error('Error from success callback');
   },
   () => {
     // will not be executed
   }
).catch(reason => {
   console.log(reason.message); // will print "Error from success callback"
});
Enter fullscreen mode Exit fullscreen mode

The principle is as mentioned in the previous point, the catch function is called on the promise of the rejected state returned by the then function, and its errors can naturally be caught.

8. (Finale) Promise realizes koa2 onion middleware model

The koa2 framework introduces the onion model, which allows your request to be like peeling an onion, entering layer by layer and then reverse layer by layer, so as to achieve unified pre- and post-processing of requests.

Image description

Let's look at a simple koa2 onion model:

const app = new Koa();
app. use(async (ctx, next) => {
   console.log('a-start');
   await next();
   console.log('a-end');
});
app. use(async (ctx, next) => {
   console.log('b-start');
   await next();
   console.log('b-end');
});

app. listen(3000);
Enter fullscreen mode Exit fullscreen mode

The above output is a-start -> b-start -> b-end -> a-end.

how to achieve such a magical output sequence, the following is my code, about 20 lines, if there is any similarity with koa, it is purely coincidental.

Let's continue to take a look.

  1. First, save the middleware function, and call the execution of the onion model after receiving the request in the listen function.
function action(koaInstance, ctx) {
   //...
}

class Koa {
   middleware = [];
   use(mid) {
     this. middlewares. push(mid);
   }
   listen(port) {
     // Pseudo-code simulate receiving request
     http.on('request', ctx => {
       action(this, ctx);
     });
   }
}
Enter fullscreen mode Exit fullscreen mode
  1. After receiving the request, serially execute the pre-logic before next from the first middleware.
// start middleware call
function action(koaInstance, ctx) {
   let nextMiddlewareIndex = 1; // Identify the middleware index to be executed next

   // define the next function
   function next() {
     // Before peeling the onion, call next to call the next middleware function
     const nextMiddleware = middlewares[nextMiddlewareIndex];
     if (nextMiddleware) {
       nextMiddlewareIndex++;
       nextMiddleware(ctx, next);
     }
   }
   // Start execution from the first middleware function, and pass in the ctx and next functions
   middlewares[0](ctx, next);
}
Enter fullscreen mode Exit fullscreen mode
  1. Handle post logic after next
function action(koaInstance, ctx) {
   let nextMiddlewareIndex = 1;
   function next() {
     const nextMiddleware = middlewares[nextMiddlewareIndex];
     if (nextMiddleware) {
       nextMiddlewareIndex++;
       // A return is also added here, so that the execution of the middleware function is executed in series from back to front with promises (this return is recommended to be understood repeatedly)
       return Promise. resolve(nextMiddleware(ctx, next));
     } else {
       // After the pre-logic of the last middleware is executed, the promise that returns fullfilled starts to execute the post-logic after next
       return Promise. resolve();
     }
   }
   middlewares[0](ctx, next);
}
Enter fullscreen mode Exit fullscreen mode

So far, a simple onion model has been realized.

The end

Everyone, if you think it is useful to you, please leave a applaud or comment, and then is there any more advanced usage that you don't understand? Please express your thoughts in the comment area!

About my new creation alova

alova is a lightweight request strategy library, which provides targeted request strategies for different request scenarios to improve application availability and fluency, and reduce server-side Pressure, so that the application has excellent strategic thinking like a wise man.

. . . . . . . . . . . . . . . . . . . . . . . . .