This paper understands Promise principle by implementing a simple class, compares Promise with traditional callback, shows Promise's advantages in asynchronous programming, and finally introduces Promise's application in practical development.
I concept
Promise decouples asynchronous operations from callbacks and associates them through execution state. After the asynchronous operation, promise is notified of the status, and promise is responsible for triggering the callback function.
II Promise principle
1. Status change
Input:
let p0 = new Promise((resolve, reject) => {}) console.log('p0', p0) let p1 = new Promise((resolve, reject) => { resolve('success') }) console.log('p1', p1) let p2 = new Promise((resolve, reject) => { reject('fail') }) console.log('p2', p2) let p3 = new Promise((resolve, reject) => { throw('report errors') }) console.log('p3', p3)
Output:
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-xc8bkt3w-1641122902483) (C: \ users \ ys \ appdata \ roaming \ typora \ user images \ image-20220102102219777. PNG)]
Conclusion:
- The Promise object just created is in pending state.
- When the asynchronous operation is completed, the resolve/reject change state is called fulfilled/rejected.
- When an exception (synchronization exception) occurs in the execution function, the change status is rejected.
realization:
class MyPromise { constructor(executor) { this.initValue(); // The myresolve / this function must be hard bound to the current myresolve object this.initBind(); try { // Execute the function passed in executor(this.resolve, this.reject); } catch (e) { // If an error is caught, execute reject directly this.reject(e); } } initBind() { this.resolve = this.resolve.bind(this); this.reject = this.reject.bind(this); } initValue() { this.PromiseResult = null; this.PromiseState = 'pending'; } resolve(value) { // Status can only be converted from pending to fully / rejected if (this.PromiseState == 'pending'){ this.PromiseState = 'fulfilled'; this.PromiseResult = value; } } reject(reason) { // Status can only be converted from pending to fully / rejected if (this.PromiseState !== 'pending'){ this.PromiseState = 'rejected'; this.PromiseResult = reason; } } }
2. Execute callback
Input / output:
// Output "resolve = success" immediately“ const p1 = new Promise((resolve, reject) => { resolve('success'); }).then(res => console.log('resolve=',res), err => console.log('reject=',err)) // Output "reject = failed after 1 second“ const p2 = new Promise((resolve, reject) => { setTimeout(() => { reject('fail'); }, 1000) }).then(res => console.log('resolve=',res), err => console.log('reject=',err))
Conclusion:
- then receives two parameters: a successful callback and a failed callback
- When the Promise status is full, execute a successful callback, and execute a failed callback for rejected
- You can register multiple callback functions by calling then multiple times. Pay attention to distinguish chain calls
realization:
When registering a callback through then, if the Promise status is fully / rejected, the callback function is executed;
If the Promise status is pending, save the callback function first, and then execute the callback after the asynchronous operation is completed.
initValue() { ... // Save callback function. this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; } resolve(value) { ... // Change the status to fully completed, and execute the saved successful callback while (this.onFulfilledCallbacks.length) { this.onFulfilledCallbacks.shift()(this.PromiseResult); } } reject(reason) { ... // Change the status to rejected and execute the saved failed callback while (this.onRejectedCallbacks.length) { this.onRejectedCallbacks.shift()(this.PromiseResult); } } then(onFulfilled, onRejected) { // Parameter verification to ensure that it must be a function onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val; onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }; if (this.PromiseState === 'fulfilled') { // Execute a fully completed callback onFulfilled(this.PromiseResult); } else if (this.PromiseState === 'rejected') { // Execute rejected callback onRejected(this.PromiseResult); }else if (this.PromiseState === 'pending') { // Promise is in pending status, and two callbacks are saved temporarily this.onFulfilledCallbacks.push(onFulfilled); this.onRejectedCallbacks.push(onRejected); } }
3. Chain call
Input / output
// Chained call output 200 const p3 = new Promise((resolve, reject) => { resolve(100); }).then(res => 2 * res) .then(res => console.log(res)) // Chained call output 300 const p4 = new Promise((resolve, reject) => { resolve(100); }).then(res => new Promise((resolve, reject) => resolve(3 * res))) .then(res => console.log(res))
conclusion
-
The then method itself returns a new Promise object.
-
If the return value of the callback function is a Promise object, the state of the new Promise object is determined by the Promise object.
-
If the return value of the callback function is not a Promise object, the status of the new Promise object is success.
realization
then(onFulfilled, onRejected) { ... var thenPromise = new MyPromise((resolve, reject) => { const resolvePromise = cb => { try { const x = cb(this.PromiseResult) if(x === thenPromise){ // Wait for yourself to complete, loop wait: the return value of then is returned in the then callback. reject(new TypeError('Chaining cycle detected for promise')); } if (x instanceof MyPromise) { // If the return value is a Promise object, the new Promise status is determined by the Promise. x.then(resolve, reject); } else { // Direct success without Promise resolve(x); } } catch (err) { // Error handling reject(err); } } if (this.PromiseState === 'fulfilled') { // If the current status is success, execute the first callback resolvePromise(onFulfilled); } else if (this.PromiseState === 'rejected') { // If the current state is failed, a second callback is executed resolvePromise(onRejected); } else if (this.PromiseState === 'pending') { // If the status is pending, save two callbacks temporarily this.onFulfilledCallback = resolvePromise(onFulfilled); this.onRejectedCallback = resolvePromise(onRejected); } }) // Returns the Promise of this wrapper return thenPromise; }
4. Call timing
Input and output
setTimeout(()=>{console.log(0)},0); const p = new Promise((resolve, reject) => { console.log(1); resolve() }).then(() => console.log(2)) console.log(3) // The output order is 1 3 2 0
conclusion
- Even if Promise's status is immediately updated to full, the callback function will not be executed immediately.
- The callback function is executed in the micro task of the event loop.
- Because callbacks are asynchronous, placing them in the execution of micro tasks can enable the subsequent synchronous code to be executed as soon as possible.
realization
const resolvePromise = cb => { setTimeout(() => { // Execute callback }) }
III Promise VS legacy callback
1. Callback hell
In the scenario of executing multiple asynchronous operations, the traditional callback needs to pass in the callback function as a parameter, which has the problem of "callback hell". Promise decouples the asynchronous operation from the callback function and does not need to pass in the callback function at the beginning.
// Traditional callbacks implement multiple asynchronous operations doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('Got the final result: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback); // Promise implements multiple asynchronous operations doSomething().then(function(result) { return doSomethingElse(result); }) .then(function(newResult) { return doThirdThing(newResult); }) .then(function(finalResult) { console.log('Got the final result: ' + finalResult); }) .catch(failureCallback);
2. Exception capture
Note: exceptions here refer to callback function exceptions, not asynchronous operation exceptions.
// The following is a typical timeout callback. Since timeoutCallback is called in asynchronous operation, try catch cannot catch asynchronous exceptions. try{ setTimeout(()=>timeoutCallback("3 seconds passed"), 3000); }catch(err){ // The timeoutCallback exception cannot be caught here. console.log(err); } // Promise is responsible for calling the callback function. After the asynchronous operation ends and promise state changes, promise will add try catch when calling the callback function. const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});
IV Promise application
1. Create the completed Promise
Promise.resolve() and Promise.reject()
2. Encapsulate traditional callback
Due to the problems of callback hell and lack of asynchronous exception capture in traditional callbacks, Promise is used to encapsulate the existing traditional callbacks.
// The following is a typical timeout callback. Because timeoutCallback is executed asynchronously, exceptions in asynchronous callback cannot be caught. try{ setTimeout(()=>timeoutCallback("3 seconds passed"), 3000); }catch(err){ // The timeoutCallback exception cannot be caught here. console.log(err); } // Promise can separate asynchronous operation from callback. When callback is invoked when the asynchronous operation is completed, it will automatically add try catch to the callback function. const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});
3. Perform multiple asynchronous operations
// Parallel multiple asynchronous operations, and then enter the next operation after all operations are completed Promise.all([promise1, promise2, promise3]) .then(([result1, result2, result3]) => { // next step }); .catch(err){ // When any operation reject, enter catch and return the corresponding reject information. } // Perform multiple asynchronous operations in sequence (chain call) [promise1, promise2, promise3].reduce((pre, cur) => pre.then(()=>{return cur}), Promise.resolve()) .then(result3 => { /* use result3 */ }).catch(err=>{ // When any operation reject, enter catch and return the corresponding reject information. }); // In es2017, you can also use async/await to optimize the above code let result; try{ for (const pro of [promise1, promise1, promise1]) { result = await pro(result); } }catch(err){ result = err; }
reference
After reading it, I will write Promise principle, the most easy to understand version