Handwriting series – this time, thoroughly understand Promise

Original link: https://www.iyouhun.com/post-258.html

img

I. Introduction

To implement Promise, you must first understand what Promise is and what functions Promise has.

Students who are not particularly familiar with Promise, it is recommended to move to ES6 Getting Started-Promise Familiarization.

Promise is implemented based on the Promises/A+ specification . In other words, we can hand-write Promise according to the Promises/A+ specification .

1.1 Small example

Promise, literal translation is a promise, what does Promise promise?

When I order a hamburger set meal at McDonald’s, the cashier will give me a receipt. This receipt is a Promise, which means that I have paid for it. McDonald’s will make a promise of a hamburger set meal for me. I need to get this through the receipt. Hamburger set.

Then the commitment to buy a hamburger will have the following 3 states:

  1. Waiting state: I just placed an order and the burger is not ready yet, so I can do other things while waiting for the burger;
  2. Success status: the hamburger is ready, notify me to pick up the meal;
  3. Failure status: If it is found that it is sold out, notify me for a refund;

It should be noted that the modification of the state is irreversible. When the burger is ready and the promise is fulfilled, it cannot return to the waiting state.

To sum up, Promise is a promise that promises to give you a processing result, which may be successful or failed, and before returning the result, you can do other things at the same time.

2. Promises/A+

Next, implement Promise step by step according to the Promises/A+ specification .

1. Promise basic usage

Let’s take a look at the basic usage of ES6 Promise.

 const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功*/){ resolve(value) } else { reject(error) } }); promise.then(data => { console.log('请求成功') }, err => { console.log('请求失败') })

1.1 Promise state

Promise has its own state. When the initial state -> success state, the success callback is executed, and when the initial state -> failure state, the failure callback is executed.

  1. pending: initial state, which can be converted to fulfilled or rejected state;
  2. fulfilled: successful state, there must be a successful return value when transitioning to this state, and the state cannot be transformed again;
  3. rejected: failed state, there must be an error reason when transitioning to this state, and the state cannot be transformed again;

Through the known 3 states of Promise, the constant STATUS and MyPromise state status can be defined.

code show as below:

 // Promise 3 种状态const STATUS = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class MyPromise { // 初始状态为pending status = STATUS.PENDING }

1.2 Actuator

As you can see from the basic usage, Promise needs to receive an executor function as a parameter, and this function has 2 parameters.

  1. resolve: Change the Promise status to success;
  2. reject: Change the Promise status to failure;

code show as below:

 class MyPromise { constructor (executor) { // 执行器executor(this.resolve, this.reject) } // 成功返回值value = null // 失败返回值reason = null // 修改Promise 状态,并定义成功返回值resolve = value => { if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED this.value = value } } // 修改Promise 状态,并定义失败返回值reject = reason => { if (this.status === STATUS.PENDING) { this.status = STATUS.REJECTED this.reason = reason } } } }

1.3 then

Promise has a then method. The first parameter of the then method is the callback function onFulfilled in the successful state, and the second parameter is the callback function onRejected in the failed state.

 promise.then(onFulfilled, onRejected)

onFulfilled requires the following:

  • It must be called when the promise is fulfilled, with the promise’s value as its first argument;
  • It cannot be called until the promise is fulfilled;
  • It cannot be called multiple times;

onRejected requires the following:

  • It must be called after the promise is rejected, with promise.reason as its first argument;
  • It cannot be called until the promise is rejected;
  • It cannot be called multiple times;

code show as below:

 class MyPromise { then = function (onFulfilled, onRejected) { if (this.status === STATUS.FULFILLED) { onFulfilled(this.value) } else if (this.status === STATUS.REJECTED) { onRejected(this.reason) } } }

1.4 Try it out

According to the basic usage of Promise, create MyPromise instance mypromise.

 const mypromise = new MyPromise((resolve, reject) => { resolve('成功') }) mypromise.then(data => { console.log(data, '请求成功') // 成功打印“成功请求成功” }, err => { console.log(err, '请求失败') })

2. Promise. then

The following will improve the MyPromise.then method according to the Promises/A+ specification .

The Promises/A+ specification states that then has the following requirements:

1. Optional parameters

onFulfilled, onRejected are optional parameters.

  • If onFulfilled is not a function, it must be ignored;
  • If onRejected is not a function, it must be ignored;

code show as below:

 class MyPromise { then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error } } }

2. Call then multiple times

then can be called multiple times on the same promise.

  • When the promise fulfills, all corresponding onFulfilled callbacks must execute then in the order they were originally called;
  • When the promise is rejected, all corresponding onRejected callbacks must execute in the order they were originally called on then;

2.1 Array cache callback

It can be understood as storing onFulfilled and onRejected in MyPromise as an array, and then executing them sequentially.

code show as below:

 class MyPromise { // 成功回调onFulfilledCallback = [] // 失败回调onRejectedCallback = [] // 修改Promise 状态,并定义成功返回值resolve = value => { if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED this.value = value while(this.onFulfilledCallback.length) { this.onFulfilledCallback.shift()(value) } } } // 修改Promise 状态,并定义失败返回值reject = reason => { if (this.status === STATUS.PENDING) { this.status = STATUS.REJECTED this.reason = reason while(this.onRejectedCallback.length) { this.onRejectedCallback.shift()(reason) } } } then = function (onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason } if (this.status === STATUS.PENDING) { this.onFulfilledCallback.push(onFulfilled) this.onRejectedCallback.push(onRejected) } else if (this.status === STATUS.FULFILLED) { onFulfilled(this.value) } else if (this.status === STATUS.REJECTED) { onRejected(this.reason) } } }

With this, we have implemented a basic Promise.

2.2 Try it out

After watching for so long, let’s try to see if MyPromise meets the requirements.

code show as below:

 const mypromise = new MyPromise((resolve, reject) => { resolve('成功') }) mypromise.then(data => { console.log(data, '1') }) mypromise.then(data => { console.log(data, '2') })

The output result is shown in the figure:

image-20230712140720270

It can be seen from the figure that it is as expected.

3. Chain call then

then must return a Promise to support chaining Promises.

The sample code is as follows:

 mypromise.then(data => { console.log(data, '请求成功') return '2' }).then(data => { console.log(data, '请求成功') return '3' })

3.1 Override the then method

The changes are as follows:

  • The then method needs to return an instance of MyPromise;
  • When the callback is called inside then, the return value needs to be processed by judging the type of the return value x through the resolvePromise method.
 class MyPromise { then = function (onFulfilled, onRejected) { // 返回MyPromise实例const promise2 = new MyPromise((resolve, reject) => { if (this.status === STATUS.PENDING) { this.onFulfilledCallback.push(() => { const x = onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) }) this.onRejectedCallback.push(() => { const x = onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) }) } else if (this.status === STATUS.FULFILLED) { const x = onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } else if (this.status === STATUS.REJECTED) { const x = onRejected(this.error) resolvePromise(promise2, x, resolve, reject) } }) return promise2 } }

The above code references resolvePromise to handle the return value of Promise.then

3.1.1 Attention

Promise2 here is not working properly yet, and an error may be reported: Cannot access 'promise2' before initialization | Cannot access promise2 before initialization.

Reason: promise2 has not been initialized yet when new promise is created. Therefore, promise2 cannot be accessed in resolvePromise. In the current execution context stack, onFulfilled or onRejected cannot be called directly. OnFulfilled or onRejected must be executed asynchronously after the current event loop.

Solution: You can use setTimeout, setImmediate, MutationObserever, process.nextTick to create a new stack after the then method is called, which we will deal with later, first look down normally.

3.2 resolvePromise

The Promises/A+ specification requires resolvePromise as follows:

image-20230712141403824

  • If promise2 === x, execute reject, the error reason is TypeError
  • if x is a function or object
    • If x.then is a function
      • execute x.then
    • if x.then is not a function
      • Execute resolve(x)
  • if x is not a function or object
    • Execute resolve(x)

code show as below:

 function resolvePromise (promise2, x, resolve, reject) { // 如果promise2 === x, 执行reject,错误原因为TypeError if (promise2 === x) { reject(new TypeError('The promise and the return value are the same')) } // 如果x 是函数或对象if ((typeof x === 'object' && x != null) || typeof x === 'function') { let then try { then = x.then } catch (error) { reject(error) } // 如果x.then 是函数if (typeof then === 'function') { // Promise/A+ 2.3.3.3.3 只能调用一次let called = false try { then.call(x, y => { // resolve的结果依旧是promise 那就继续解析| 递归解析if (called) return called = true resolvePromise(promise2, y, resolve, reject) }, err => { if (called) return called = true reject(err) }) } catch (error) { if (called) return reject(error) } } else { // 如果x.then 不是函数resolve(x) } } else { // 如果x 不是promise 实例resolve(x) } }

3.3 Try it out

Try to see if it meets expectations, and call then in a chain.

The output is:

image-20230712142155920

Success as expected!

4. Asynchronous events

The Promises/A+ specification requires that onFulfilled and onRejected must not be called before the execution context stack. That is, 3.1.1 indicates the points to pay attention to.

4.1 Event queue

When an asynchronous event is encountered, it does not wait for the asynchronous event to return the result, but hangs the event in a queue different from the execution stack, which we call the event queue.

The system will read the “Event Queue” only after all synchronization tasks are executed.

Events in the event queue are divided into macrotasks and microtasks:

  1. Macro tasks: tasks initiated by browser/Node, such as window.setTimeout;
  2. Microtask: initiated by Js itself, such as Promise;

The event queue is to execute microtasks first, and then execute macrotasks, and macrotasks and microtasks contain the following events:

macro task micro task
setTimeout Promises
setInterval queueMicrotask
script (overall code block)

Take a look at the example below, do you know the answer?

 setTimeout(function () { console.log(1) }) new Promise(function(resolve,reject){ console.log(2) resolve(3) }).then(function(val){ console.log(val) }) console.log(4)

The order of the printed results is 2->4->3->1. The event queue is as follows:

  1. main queue, synchronous task, synchronous task inside new Promise

     new Promise(function(resolve,reject){ console.log(2) })
  2. Main queue, synchronization task, console.log(4) after new Promise

     console.log(4)
  3. Microtasks for asynchronous tasks

     promise.then(function(val){ console.log(val) })
  4. Macro task for asynchronous task

     setTimeout(function () { console.log(1) })

Therefore, if we want to realize that onFulfilled and onRejected must not be called before executing the context stack, we need to transform onFulfilled and onRejected into microtasks . Here we use queueMicrotask to simulate the implementation of microtasks. The code is as follows:

 class MyPromise { then (onFulfilled, onRejected) { const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value const realOnRejected = typeof onRejected === 'function' ? onRejected : error => { throw error } const promise2 = new MyPromise((resolve, reject) => { const fulfilledMicrotask = () => { // 创建一个微任务等待promise2 完成初始化queueMicrotask(() => { try { // 获取成功回调函数的执行结果const x = realOnFulfilled(this.value) // 传入resolvePromise 集中处理resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) } const rejectedMicrotask = () => { // 创建一个微任务等待promise2 完成初始化queueMicrotask(() => { try { // 调用失败回调,并且把原因返回const x = realOnRejected(this.reason) // 传入resolvePromise 集中处理resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) } if (this.status === STATUS.PENDING) { this.onFulfilledCallbacks.push(fulfilledMicrotask) this.onRejectedCallbacks.push(rejectedMicrotask) } else if (this.status === STATUS.FULFILLED) { fulfilledMicrotask() } else if (this.status === STATUS.REJECTED) { rejectedMicrotask() } }) return promise2 } }

4.2 Try it out

The print result is as shown in the figure:

image-20230712142636615

Successfully printed sequentially.

3. Promise/A+ testing

Next, we will use the Promise/A+ testing tool promises-aplus-tests to test whether our handwritten Promise complies with the specification.

3.1 Install promises-aplus-tests

 npm install promises-aplus-tests -D

3.2 Add deferred to MyPromise

 MyPromise { ...... } MyPromise.deferred = function () { var result = {}; result.promise = new MyPromise(function (resolve, reject) { result.resolve = resolve result.reject = reject }); return result; } module.exports = MyPromise

3.3 Configure startup command

 "scripts": { "test:promise": "promises-aplus-tests ./Promise/index" }

3.4 Start the test

 npm run test:promise

Whoa, all successes! !

image-20230712142844849

3.5 Complete code

So far, the complete code is as follows:

 const STATUS = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class MyPromise { constructor (executor) { // 执行器try { executor(this.resolve, this.reject) } catch (error) { this.reject(error) } } // 初始状态为pending status = STATUS.PENDING // 成功返回值value = null // 失败返回值reason = null // 成功回调onFulfilledCallbacks = [] // 失败回调onRejectedCallbacks = [] // 修改Promise 状态,并定义成功返回值resolve = value => { if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED this.value = value // this.onFulfilledCallbacks.forEach(fn => fn()) while(this.onFulfilledCallbacks.length) { this.onFulfilledCallbacks.shift()(value) } } } // 修改Promise 状态,并定义失败返回值reject = reason => { if (this.status === STATUS.PENDING) { this.status = STATUS.REJECTED this.reason = reason // this.onRejectedCallbacks.forEach(fn => fn()) while(this.onRejectedCallbacks.length) { this.onRejectedCallbacks.shift()(reason) } } } then = function (onFulfilled, onRejected) { // onFulfilled、onRejected 是可选参数不是函数则必须忽略它const realOnFulfilled = onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value const realOnRejected = onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error } const promise2 = new MyPromise((resolve, reject) => { // 报错:Cannot access 'promise2' before initialization | 不能访问promise2 在初始化之前// 原因:在new promise 时,promise2 还没有完成初始化,所以resolvePromise 中不能访问到promise2 // 在当前的执行上下文栈中,onFulfilled 或onRejected 是不能被直接调用的// onFulfilled 或onRejected 得是在当前事件循环后异步执行的// 可以使用setTimeout、setImmediate、MutationObserever、process.nextTick在then 方法被调用后将创建一个新的栈const fulfilledMicrotask = () => { // 创建一个微任务等待promise2 完成初始化queueMicrotask(() => { try { // 获取成功回调函数的执行结果const x = realOnFulfilled(this.value) // 传入resolvePromise 集中处理resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) } const rejectedMicrotask = () => { queueMicrotask(() => { try { // 获取成功回调函数的执行结果const x = realOnRejected(this.reason) // 传入resolvePromise 集中处理resolvePromise(promise2, x, resolve, reject) } catch (error) { reject(error) } }) } if (this.status === STATUS.PENDING) { this.onFulfilledCallbacks.push(fulfilledMicrotask) this.onRejectedCallbacks.push(rejectedMicrotask) } else if (this.status === STATUS.FULFILLED) { fulfilledMicrotask() } else if (this.status === STATUS.REJECTED) { rejectedMicrotask() } }) return promise2 } } function resolvePromise(promise2, x, resolve, reject) { // 如果promise2 === x 执行reject,错误原因为TypeError if (promise2 === x) { reject(new TypeError('The promise and the return value are the same')) } // 如果x 是函数或对象if ((typeof x === 'object' && x != null) || typeof x === 'function') { let then try { then = x.then } catch (error) { reject(error) } // 如果x.then 是函数if (typeof then === 'function') { // Promise/A+ 2.3.3.3.3 只能调用一次let called = false try { then.call(x, y => { // resolve的结果依旧是promise 那就继续解析| 递归解析if (called) return called = true resolvePromise(promise2, y, resolve, reject) }, err => { if (called) return called = true reject(err) }) } catch (error) { if (called) return reject(error) } } else { // 如果x.then 不是函数resolve(x) } } else { // 如果x 是个普通值就直接返回resolve 作为结果Promise/A+ 2.3.4 resolve(x) } } // 测试Promise 是否符合规范MyPromise.deferred = function () { var result = {} result.promise = new MyPromise(function (resolve, reject) { result.resolve = resolve result.reject = reject }) return result } module.exports = MyPromise

The following complete Promise API will be based on this basic code.

4. Promise API

Although the above promise source code already complies with the Promise/A+ specification, the native Promise also provides some other methods, such as:

  • Promise. resolve()
  • Promise. reject()
  • Promise. prototype. catch()
  • Promise.prototype.finally()
  • Promise. all()
  • Promise. race()

Let’s talk about the implementation of each method in detail:

4.1 Promise. prototype. catch

Promise.prototype.catch is used to catch promise exceptions, which is equivalent to an unsuccessful then .

As for why this method is implemented first, it is to prevent errors from being reported when implementing other APIs.

 // ... catch(errCallback) { return this.then(null,errCallback) } // ...

4.2 Promise. prototype. finally

finally means that it is not the final meaning, but the meaning that will be executed anyway. If a promise is returned, it will wait for the promise to be fulfilled as well. If a successful promise is returned, the previous result will be used; if a failed promise is returned, the failed result will be passed to catch .

 finally(callback) { return this.then((value)=>{ return MyPromise.resolve(callback()).then(()=>value) },(reason)=>{ return MyPromise.resolve(callback()).then(()=>{throw reason}) }) }

have a test

 MyPromise.resolve(456).finally(()=>{ return new MyPromise((resolve,reject)=>{ setTimeout(() => { resolve(123) }, 3000) }) }).then(data=>{ console.log(data,'success') }).catch(err=>{ console.log(err,'error') })

The console outputs after waiting 3s : 456 'success' , on the contrary, if resolve(123) is changed to reject(123) and waits for 3s , it outputs 123 'error'

image-20230712151918462

4.3 Promise. resolve

Generates a successful promise by default.

 static resolve(data){ return new MyPromise((resolve,reject)=>{ resolve(data) }) }

It should be noted here that promise.resolve has a waiting function . If the parameter is a promise, it will wait for the promise to be resolved and execute it downwards, so here we need to do a small processing in the original resolve method:

 // 修改Promise 状态,并定义成功返回值resolve = value => { if(value instanceof MyPromise){ // 递归解析return value.then(this.resolve,this.reject) } if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED this.value = value while(this.onFulfilledCallbacks.length) { this.onFulfilledCallbacks.shift()(value) } } }

4.4 Promise. reject

 static reject(reason){ return new MyPromise((resolve,reject)=>{ reject(reason) }) }

4.5 Promise. all

Promise.all solves the concurrency problem, multiple asynchronous concurrent access to the final result (if one fails, it fails).

Promise.all method can receive an array of promises as a parameter and return a new promise object, which will only be resolved when all the promises in the array are successful. If one of the promises fails, Promise.all will reject it immediately, and will not wait for the execution results of other promises.

Note: This parameter array does not have to be all promises, and it can also be constant ordinary values.

 static all(values) { if (!Array.isArray(values)) { const type = typeof values return new TypeError(`TypeError: ${type} ${values} is not iterable`) } return new MyPromise((resolve, reject) => { let resultArr = [] let orderIndex = 0 const processResultByKey = (value, index) => { resultArr[index] = value if (++orderIndex === values.length) { resolve(resultArr) } } for (let i = 0; i < values.length; i++) { let value = values[i] if (value && typeof value.then === 'function') { value.then((value) => { processResultByKey(value, i) }, reject) } else { processResultByKey(value, i) } } }) }

have a test:

 let p1 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('ok1') }, 1000) }) let p2 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('ok2') }, 1000) }) MyPromise.all([1,2,3,p1,p2]).then(data => { console.log('resolve', data) }, err => { console.log('reject', err) })

The console outputs after waiting for 1s : resolve (5) [1, 2, 3, 'ok1', 'ok2']

4.6 Promise. race

Promise.race is used to handle multiple requests, and the fastest one is used (whoever completes first uses it).

 static race(promises) { return new MyPromise((resolve, reject) => { // 一起执行就是for循环for (let i = 0; i < promises.length; i++) { let val = promises[i] if (val && typeof val.then === 'function') { val.then(resolve, reject) } else { // 普通值resolve(val) } } }) }

Special attention should be paid to: because Promise has no interrupt method , xhr.abort() and ajax have their own interrupt methods, axios is implemented based on ajax; fetch is based on promise, so its request cannot be interrupted.

This is also the defect of promise, we can use race to encapsulate the interrupt method by ourselves:

 function wrap(promise) { // 在这里包装一个promise,可以控制原来的promise是成功还是失败let abort let newPromise = new MyPromise((resolve, reject) => { // defer 方法abort = reject }); let p = MyPromise.race([promise, newPromise]) // 任何一个先成功或者失败就可以获取到结果p.abort = abort return p } const promise = new MyPromise((resolve, reject) => { setTimeout(() => { // 模拟的接口调用ajax 肯定有超时设置resolve('成功') }, 1000); }); let newPromise = wrap(promise) setTimeout(() => { // 超过3秒就算超时应该让proimise 走到失败态newPromise.abort('超时了') }, 3000) newPromise.then((data => { console.log('成功的结果' + data) })).catch(e => { console.log('失败的结果' + e) })

The console outputs after waiting for 1s :成功的结果成功

5. Summary

Above, we have implemented a Promise that conforms to the Promises/A+ specification , and also implemented some common API methods Promise .

To sum up, Promise is actually an object that helps us execute asynchronous tasks. Because of the single-threaded nature of Javascript, it is necessary to add callbacks to asynchronous tasks to obtain the results of asynchronous tasks. In order to solve the callback hell, Promise came into being.

Promise allows us to obtain task results in Promise.then by processing the execution status of asynchronous tasks, making the code clearer and more elegant.

The chained calls of Promise.then express the asynchronous flow in a sequential manner, allowing us to better maintain asynchronous code.

Note: This article is reproduced and revised based on https://juejin.cn/post/6973155726302642206 , thank you.

This article is transferred from: https://www.iyouhun.com/post-258.html
This site is only for collection, and the copyright belongs to the original author.