current position:Home>[hand tear series] hand tear promise -- this article takes you to realize promise perfectly according to promise a + specification!

[hand tear series] hand tear promise -- this article takes you to realize promise perfectly according to promise a + specification!

2022-04-29 13:13:29Straw hat plastic

This article is based on Promise A+ standard , Take you step by step from shallow to deep Promise, After reading patiently, you can definitely understand and realize !

The source code is in mine github in , You can take it yourself if you need it :github.com/Plasticine-…

1. Basic function realization

First of all, make it clear Promise It's a constructor , Able to receive a executor Parameters , This parameter is a function , It takes two parameters executor(resolve, reject)

So in our MyPromise Should this be executed in the constructor of executor function , And passed in resolve and reject Parameters , These two parameters are functions , And in our MyPromise Achieve in

Because of each Promise Example of resolve and reject Functions should be independent of each other , That is, they all belong to the instance itself , Therefore, it is not suitable to define in the constructor prototype On , stay ES6 class From the perspective of , That is to say... Should not be resolve and reject A function is defined as a method of a class , Instead, you should define... Inside the constructor , This ensures that they will create multiple in memory when each instance instantiates and calls the constructor , If defined as a method , Will hang to the constructor prototype On

According to the above characteristics , We can write a whole framework first

class MyPromise {
  constructor(executor) {
    const resolve = () => {}
    const reject = () => {}
    
    executor(resolve, reject)
  }
}
 Copy code 

We all know ,Promise It's stateful , according to Promise A+ standard , There are three states

A promise must be in one of three states: pending, fulfilled, or rejected.

So now we'll give us MyPromise Define three state constants

/** * @description MyPromise  The state of  */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
 Copy code 

Every Promise The instances are all... At the beginning pending state , Unless an external call resolve perhaps reject Function will change promise Status of the instance , Therefore, the constructor should first initialize the state to pending

constructor(executor) {
  this.status = PENDING
}
 Copy code 

secondly , according to Promise A+ standard , Every Promise Instances can call then Method , from onFulfilled Call back to deal with resolved State of value, And by onRejected Call back to deal with rejected State of reason, Give us an example to maintain value and reason So that it can be used by the corresponding callback later

constructor(executor) {
  this.value = undefined
  this.reason = undefined
}
 Copy code 

This is initialized to undefined Because if the external calls resolve perhaps reject If there is no parameter passed in the function , stay js When no argument is passed in, accessing the formal parameter is undefined, therefore onFulfilled/onRejected In callback value/reason It should also be undefined, This is for and js The function characteristics of are unified

function foo(text) {
  console.log(text)
}

foo() // => undefined
 Copy code 

And then there's the resolve/reject Process the change of state and set the corresponding value/reason

constructor(executor) {
  const resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED
      this.value = value
    }
  }
  
  const reject = (reason) => {
    if (this.status === PENDING) {
      this.status = REJECTED
      this.reason = reason
    }
  }
  
  executor(resolve, reject)
}
 Copy code 

Here are a few points worth noting :

  1. The change of state can only be from pending Change to fulfilled/rejected, It can't be other changes , Therefore, before changing the state, you must judge whether the current state is pending
  2. value/reason The modification of must take effect after the status changes , Therefore, it should also be put in the judgment statement
  3. The most important point !!!resolve/reject Functions can only be used with Arrow function To define , You must not use ordinary functions to define , This involves arrow functions and ordinary functions this Pointed question
    1. Because you need to resolve/reject Used in this, And make sure this It's pointing MyPromise Example of
    2. Of a normal function this The point is determined when calling externally , because this The default binding for 、 Implicit binding 、 Explicit binding and other features lead to this The direction of is not clear , There is no guarantee of pointing to the instance itself
    3. Not in arrow function this, according to js Characteristics of lexical scope , It will use the... In the parent function definition field when the function is defined this, That is to say constructor Function this
  4. The arrow function uses const Keyword declares a reference variable to point to it , Therefore, it is necessary to implement executor Previously defined , Otherwise, it will be unable to find... Due to temporary dead zone resolve, reject function

The next step is then Method is implemented ,then Method accepts two parameters ,onFulfilled and onRejected, Are two callback functions , according to Promise A+ standard , call then Methods in fulfilled State onFullfilled Callback , stay rejected State onRejected Callback , According to this characteristic , You can write the following code :

then(onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  }
  
  if (this.status === REJECTED) {
    onRejected(this.reason)
  }
}
 Copy code 

Now we have got a that can realize the basic functions Promise 了 , The current complete code is as follows :

/** * @description MyPromise  The state of  */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class MyPromise {
  constructor(executor) {
    this.status = PENDING //  The initial state is  PENDING
    this.value = undefined // resolve  The value of going out 
    this.reason = undefined // reject  The reason for going out 

    const resolve = (value) => {
      //  Only in  PENDING  State can be switched to  FULFILLED  state 
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
      }
    }

    const reject = (reason) => {
      //  Only in  PENDING  State can be switched to  REJECTED  state 
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
      }
    }

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    //  Determine which callback to execute according to the status 
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
}
 Copy code 

Test it

function foo() {
  return new MyPromise((resolve, reject) => {
    resolve('resolved value')
  })
}

foo().then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => resolved value
 Copy code 
function foo() {
  return new MyPromise((resolve, reject) => {
    reject('rejected reason')
  })
}

foo().then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => rejected reason
 Copy code 

2. exception handling

At present, our MyPromise What happens when you encounter an exception ? Can be like Promise Be like that onRejected Catch it ?

Obviously not , Because we didn't handle any exceptions . Then think about where exception handling should be added so that exceptions can be caught and passed to onRejected Callback to deal with ?

First of all, we should clarify where the exception comes from , The exception must be when calling the constructor from outside executor Function , Because the main business logic is executor Writing in the

So if you call executor An exception occurred in the process of , We should be in our MyPromise To capture it , And because I want to be onRejected Handle , Therefore, you need to change the status to rejected, And will reason Set to the exception caught , This part of the logic is not reject What functions do ? So we can reuse , call reject that will do

constructor(executor) {
  try {
    executor(resolve, reject)
  } catch (e) {
    reject(e)
  }
}
 Copy code 

Now the exception can be onRejected Processed

function foo() {
  return new MyPromise((resolve, reject) => {
    throw new Error('something wrong...')
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// => rejected: Error: something wrong...
 Copy code 

3. Use release - Subscription mode solves the problems of asynchrony and multiple calls

At present, our MyPromise What happens if you encounter asynchronous execution in ? For example, in executor in , Delay two seconds to execute resolve function , So it can be then Methodical onFulfilled Do you deal with it ?

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// =>
 Copy code 

You can see that there is no output , because executor The execution of is synchronous , encounter setTimeout The execution callback will be placed in the macro task queue of the event loop , Then immediately execute then The method , At this time, because there is no call resolve, So the status is still the same pending, that then Naturally, it can't be handled

Wait until two seconds later, the callback in the timer executes , Changed the State , But this time js The code in the thread has already been executed , That is to say then It's long over , So no other code can handle resolve After resolved state

Maybe we'll come up with the following solution

function foo() {
  return new MyPromise((resolve, reject) => {
    // resolve('resolved value')
    // reject('rejected reason')
    // throw new Error('something wrong...')

    setTimeout(() => resolve('resolved value'), 2000)
  })
}

const myPromise = foo()

// 2.5  We'll do it in seconds  then  Method treatment  resolved  state 
setTimeout(() => {
  myPromise.then(
    (value) => {
      console.log(`resolved: ${value}`)
    },
    (reason) => {
      console.log(`rejected: ${reason}`)
    }
  )
}, 2500)

// => resolved: resolved value
 Copy code 

Be careful : The two timers start counting almost at the same time , So finally print out **resolved: resolved value** The time taken is not **2 + 2.5 = 4.5 second **, It is **2.5 second **

This scheme seems to be able to solve the problem of asynchronous call , But has it really solved ?

Just imagine , If we don't use a timer now , It is a axios/fetch Of ajax What about the request ? We only call after the request has a result resolve function , You can still die like this 2.5 Second timer to call then Methods? ?

Obviously not , Because I don't know this ajax How long will the request take , If you are lucky , It's in 2.5 The result is returned in seconds , that then It can still handle the results normally , But if it goes beyond 2.5 Second is not enough , Do you want to set a super long timer , When it's due, do it again then Methods? ? It's obviously outrageous

It is necessary to consider improving our MyPromise 了 , We need to use ** Release - subscribe ** This design pattern to solve this problem

Think about it first , The reason why asynchronous calls cannot be triggered onResolved Callback , No, it's just because you execute... In synchronous code then Method time , Asynchronous code hasn't called yet resolve Well , So the whole MyPromise The status of the instance is still pending state

In that case , So we can do that then Method pending Status , Maintain a container , Used exclusively for storage onResolved Callback ( Because it may be called multiple times then Method , Every then Each method has its own onResolved Callback ), Then call... In asynchronous code resolve When , Take it out of this container in turn onResolved Just call back and execute !

in fact :

  • then In dealing with pending, take onResolved and onRejected The process of adding to the corresponding container is “ subscribe ” The process of , subscribe fulfilled and rejected event
  • resolve Put in the container onResolved The callback is taken out in turn, and the execution process is “ Release ” The process of , Release fulfilled news
  • reject It's the same thing , It's just that it's from storage onRejected It's just to get the callback from the callback container , Also a “ Release ” The process of , Released is rejected news

Just look at the code !

then(onFulfilled, onRejected) {
  //  Determine which callback to execute according to the status 
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  }

  if (this.status === REJECTED) {
    onRejected(this.reason)
  }

  if (this.status === PENDING) {
    // pending  In this state, you need  “ subscribe ” fulfilled  and  rejected  event 
    this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
    this.onRejectedCallbackList.push(() => onRejected(this.reason))
  }
}

const resolve = (value) => {
  //  Only in  PENDING  State can be switched to  FULFILLED  state 
  if (this.status === PENDING) {
    this.status = FULFILLED
    this.value = value

    //  Trigger  resolve  Behavior , Start  “ Release ” resolved  event 
    this.onFulfilledCallbackList.forEach((fn) => fn())
  }
}

const reject = (reason) => {
  //  Only in  PENDING  State can be switched to  REJECTED  state 
  if (this.status === PENDING) {
    this.status = REJECTED
    this.reason = reason

    //  Trigger  reject  Behavior , Start  “ Release ” rejected  event 
    this.onRejectedCallbackList.forEach((fn) => fn())
  }
}
 Copy code 

Now you can handle asynchronous code normally

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// => resolved: resolved value
 Copy code 

And this design has another advantage , For multiple calls then Method time , We must want to call then Methods to execute callbacks among them , Because the two containers we maintain are actually a queue , Callbacks are first in, first out , This ensures the order and accuracy of callback execution then Methods are called in the same order

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

const myPromise = foo()

myPromise.then(
  (value) => {
    console.log(`resolved1: ${value}`)
  },
  (reason) => {
    console.log(`rejected1: ${reason}`)
  }
)

myPromise.then(
  (value) => {
    console.log(`resolved2: ${value}`)
  },
  (reason) => {
    console.log(`rejected2: ${reason}`)
  }
)

myPromise.then(
  (value) => {
    console.log(`resolved3: ${value}`)
  },
  (reason) => {
    console.log(`rejected3: ${reason}`)
  }
)

/** => resolved1: resolved value resolved2: resolved value resolved3: resolved value */
 Copy code 

4. Solve the problem of chain call

Native Promise It supports chain call

const promise = new Promise((resolve, reject) => {
  resolve('plasticine')
})
promise
  .then((value) => {
    console.log(`then1 -- ${value}`)
    return value
  })
  .then((value) => {
    console.log(`then2 -- ${value}`)
    return new Promise((resolve, reject) => resolve(value))
  })
  .then((value) => {
    console.log(`then3 -- ${value}`)
    return Promise.resolve(value)
  })
  .then((value) => {
    console.log(`then4 -- ${value}`)
  })

/** => then1 -- plasticine then2 -- plasticine then3 -- plasticine then4 -- plasticine */
 Copy code 

At present, our MyPromise Such a function is not supported yet , Why is it possible to chain call ? That must be because then Method returns Promise Examples , in fact Promise A+ The specification clearly stipulates :

then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);

  1. If either onFulfilledor onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
  2. If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
  3. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  4. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

4.1 then Back in Promise Implement chain call

So according to this specification , We can modify it then Method implementation , Wrap a layer MyPromise object promise2, And return this object to

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    //  Determine which callback to execute according to the status 
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

    if (this.status === REJECTED) {
      onRejected(this.reason)
    }

    if (this.status === PENDING) {
      // pending  In this state, you need  “ subscribe ” fulfilled  and  rejected  event 
      this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
      this.onRejectedCallbackList.push(() => onRejected(this.reason))
    }
  })

  return promise2
}
 Copy code 

4.2 Capture then The return values of the two callbacks in x

The first article of the specification also mentions ,onFulFilled and onRejected The callback will return a x, Then add

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    //  Determine which callback to execute according to the status 
    if (this.status === FULFILLED) {
      let x = onFulfilled(this.value)
    }

    if (this.status === REJECTED) {
      let x = onRejected(this.reason)
    }

    if (this.status === PENDING) {
      // pending  In this state, you need  “ subscribe ” fulfilled  and  rejected  event 
      this.onFulfilledCallbackList.push(() => {
        let x = onFulfilled(this.value)
      })
      this.onRejectedCallbackList.push(() => {
        let x = onRejected(this.reason)
      })
    }
  })

  return promise2
}
 Copy code 

And then this x Yes, it will be resolve Out of the , In this way, it can be used by the next then Method received

Then you have to think about x What will it be , from TS From the perspective of ,x yes any Type of , It may be a basic data type or a reference data type , It doesn't matter , The trouble is x It could be MyPromise object , So this needs to be studied

Because if you just put MyPromise The object is to resolve get out , next then I got a MyPromise object , It can't get what it really wants value, So for x yes MyPromise About the object , It should carry out the resolve function


4.3 Handle then Exception thrown during callback execution

The second article of the specification refers to onFulfilled and onRejected The abnormal , Will act as promise2 Of reason By reject get out , Then we will try/catch Capture it , In case of exception, call promise2 Of reject that will do

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    //  Determine which callback to execute according to the status 
    if (this.status === FULFILLED) {
      try {
        let x = onFulfilled(this.value)
      } catch (e) {
        reject(e)
      }
    }

    if (this.status === REJECTED) {
      try {
        let x = onRejected(this.reason)
      } catch (e) {
        reject(e)
      }
    }

    if (this.status === PENDING) {
      // pending  In this state, you need  “ subscribe ” fulfilled  and  rejected  event 
      this.onFulfilledCallbackList.push(() => {
        try {
          let x = onFulfilled(this.value)
        } catch (e) {
          reject(e)
        }
      })
      this.onRejectedCallbackList.push(() => {
        try {
          let x = onRejected(this.reason)
        } catch (e) {
          reject(e)
        }
      })
    }
  })

  return promise2
}
 Copy code 

4.4 resolvePromise Handle x

Next, we should deal with x 了 , The first of the norms is [[Resolve]](promise2, x) In fact, it is to call [[Resolve]] Function to handle promise2 Instance and returned x

alike , The specification States , This [[Resolve]] The name of the function in the specification is resolvePromise, Then we should get promise2 and x Then call such a function

const promise2 = new MyPromise((resolve, reject) => {
  //  Determine which callback to execute according to the status 
  if (this.status === FULFILLED) {
    try {
      let x = onFulfilled(this.value)
      resolvePromise(promise2, x)
    } catch (e) {
      reject(e)
    }
  }
  ...
}
 Copy code 

4.4.1 Asynchronous execution resolvePromise Ensure that the parameters can be obtained

But there's a problem ,promise2 Can you get it ?promise2 Is in MyPromise After the constructor of is executed, you will get , Now we need to use... In advance inside the constructor promise2 example , There's obviously a problem

secondly , Then we need to call promise2 Of resolve/reject To deal with it x Of , But they are defined in MyPromise Inside the constructor , therefore resolvePromise Can't access , So we also need to put resolve/reject And to resolvePromise function

const promise2 = new MyPromise((resolve, reject) => {
  //  Determine which callback to execute according to the status 
  if (this.status === FULFILLED) {
    try {
      let x = onFulfilled(this.value)
      resolvePromise(promise2, x, resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  ...
}
 Copy code 

Okay , Then the next step is to solve how to obtain promise2 The problem. , The question is Promise A+ The specification also mentions

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

You can see , You can use macro tasks or micro tasks to achieve , thus , The entire constructor will be executed in js When the thread executes, execute , The macro task will be added to the macro task queue , Then it's time to execute macro tasks ,promise2 The instance has been created , therefore resolvePromise You can use it normally

Of course , You can also use micro tasks , Native Promise It is implemented in the form of micro tasks , For the sake of simplicity , Direct use setTimeout Realization , It is implemented in the form of macro tasks

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    //  Determine which callback to execute according to the status 
    if (this.status === FULFILLED) {
      //  Execute as a macro task  resolvePromise  To ensure that you get  promise2  example 
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value)
          resolvePromise(promise2, x)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
  })
}
 Copy code 

here **setTimeout** The default delay is 0, It's OK not to fill in , I filled it out just to emphasize the use of **setTimeout** Just to execute in the form of macro tasks **resolvePromise**, Let him execute in the next round of the event cycle , Not immediately , And pay attention to , Although it is **0ms** Delay of , But there will still be at least **4ms** Delay of , This point **MDN** There's an explanation on , Check for yourself


4.4.2 Realization resolvePromise

The next step is to realize resolvePromise 了 ,resolvePromise The implementation of also needs to follow Promise A+ To achieve image.png


4.4.2.1 An exception is thrown when referring to a loop

First of all, according to the 2.3.1, If promise and x It refers to the same object , Should be reject One TypeError As reason

Start with the original Promise Show me , Deepen the understanding

const promise1 = new Promise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return promise2
  },
  (reason) => {
    return reason
  }
)

promise2.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => TypeError: Chaining cycle detected for promise #<Promise>
 Copy code 

So we can do that resolvePromise To judge ,promise === x when reject One TypeError get out

/** * * @param {MyPromise} promise MyPromise  example  * @param {any} x  Previous  MyPromise resolve  The value of  * @param {Function} resolve promise  In the object constructor  resolve * @param {Function} reject promise  In the object constructor  reject */
function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }
}
 Copy code 

4.4.2.2 Judge x Whether it is MyPromise example

Let's take a look 2.3.32.3.2 You can skip , It's actually telling us to keep promise It's just a state of )

2.3.3 We need to judge x Is it a object/function

then 2.3.3.1 And make then = x.then, And then I will judge then Is it a function

The purpose of this is to judge x Is it a MyPromise object , Yes then The method regards it as MyPromise object

take **then = x.then** One thing to note in this process ,**x.then** May be **Object.defineProperty** Set up **getter** hijacked , If **getter** Exception thrown in , You need to treat this exception as **reason** Take it **reject** get out , This is what **2.3.3.2** Stipulated in

Object.defineProperty(x, 'then', {
  get() {
    throw new Error('something wrong...')
  }
})
 Copy code 

So we need to use try/catch Wrap it let then = x.then, And then according to 2.3.3.3.1 and 2.3.3.3.2, When x It's a MyPromise When the object , We need to call it then Method , And you need to explicitly bind this by x, Pass in two callbacks at the same time :resolvePromise and rejectPromise

this **resolvePromise** Not the whole outside **resolvePromise(promise, x, resolve, reject)**, It's another definition , Here we directly pass in the form of arrow function , Are two anonymous functions

And according to 2.3.3.3.1,resolvePromise The callback receives a parameter y, Then recursively call resolvePromise(promise, y, resolve, reject), Because if x It's a MyPromise Words , its then It is still possible to return MyPromise object , Therefore, recursive processing is required

According to the above points , What we have now resolvePromise(promise, x, resolve, reject) as follows :

/** *  according to  x  Make different treatment for different types of  * x  For the wrong  MyPromise  When the object  --  direct  resolve * x  by  MyPromise  When the object  --  Call its  resolve * *  The remaining details will be commented in the code  * * @param {MyPromise} promise MyPromise  example  * @param {any} x  Previous  MyPromise resolve  The value of  * @param {Function} resolve promise  In the object constructor  resolve * @param {Function} reject promise  In the object constructor  reject */
function resolvePromise(promise, x, resolve, reject) {
  //  according to  Promise A+  standard  2.3.1: promise  and  x  When pointing to the same reference, you should  reject  One  TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x  yes  object or function  The case when 
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object', Therefore, we need to make an additional judgment to exclude  x  yes  null  The situation of 

    try {
      // x.then  May be  Object.defineProperty  Set up  getter  hijacked , And throw an exception , So we have to  try/catch  Capture 
      let then = x.then

      if (typeof then === 'function') {
        // x  Yes  then  When the method is used , Think of it as  MyPromise  object 
        // 2.3.3.3  perform  x.then, And explicitly bind  this  Point to , And there are two callbacks  resolvePromise  and  rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            //  Need to call recursively  MyPromise  in  resolve  The value of going out , That's what we have here  y
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            //  about  reject, Directly  reason  to  reject  Just go out 
            reject(r)
          }
        )
      } else {
        // x  No,  then  Method  --  No special treatment is required , direct  resolve
        resolve(x)
      }
    } catch (e) {
      reject(e)
    }
  } else {
    //  Basic data type  --  direct  resolve
    resolve(x)
  }
}
 Copy code 

4.4.2.3 Prevent multiple executions then Callback in

according to 2.3.3.3.3, When both callbacks are executed , We should only carry out the first , What does that mean ? Use native Promise Show me

const promise1 = new Promise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return new Promise((resolve, reject) => {
      resolve(value)
      reject(new Error('something wrong...'))
    })
  },
  (reason) => {
    return reason
  }
)

promise2.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => plasticine
 Copy code 

That is to say, if you are then Of onFulfilled Calling back , Return to new Promise When an instance , Both... Are called in the constructor resolve Call again reject Words , Only the first called... Will be executed , The rest will be ignored , In this case reject It's ignored

So our MyPromise How to realize this function ? Can be marked with a Boolean value , Express x.then Whether any of the two callbacks in have been called , Some will not execute them again

function resolvePromise(promise, x, resolve, reject) {
  //  according to  Promise A+  standard  2.3.1: promise  and  x  When pointing to the same reference, you should  reject  One  TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x  yes  object or function  The case when 
  let isCalled = false //  When  x  yes  MyPromise  When an instance , Mark  x.then  Whether any of the callbacks in have been called 

  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object', Therefore, we need to make an additional judgment to exclude  x  yes  null  The situation of 

    try {
      // x.then  May be  Object.defineProperty  Set up  getter  hijacked , And throw an exception , So we have to  try/catch  Capture 
      let then = x.then

      if (typeof then === 'function') {
        // x  Yes  then  When the method is used , Think of it as  MyPromise  object 
        // 2.3.3.3  perform  x.then, And explicitly bind  this  Point to , And there are two callbacks  resolvePromise  and  rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            //  Need to call recursively  MyPromise  in  resolve  The value of going out , That's what we have here  y
            if (isCalled) return //  Any callback has been executed , Ignore the execution of the current callback 

            isCalled = true //  Execute callback for the first time , Set the flag variable to  true, Prevent repeated calls or calls  rejectPromise
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            //  about  reject, Directly  reason  to  reject  Just go out 
            if (isCalled) return //  Any callback has been executed , Ignore the execution of the current callback 

            isCalled = true //  Execute callback for the first time , Set the flag variable to  true, Prevent repeated calls or calls  resolvePromise
            reject(r)
          }
        )
      } else {
        // x  No,  then  Method  --  No special treatment is required , direct  resolve
        resolve(x)
      }
    } catch (e) {
      //  If something goes wrong , Should not be able to call  resolve, Therefore, we should also add  isCalled  Judge , Prevent calling  resolve
      if (isCalled) return

      isCalled = true
      reject(e)
    }
  } else {
    //  Basic data type  --  direct  resolve
    resolve(x)
  }
}
 Copy code 

4.5 A functional test

There is nothing in the later specifications , Now you can test whether the function is normal

const promise1 = new MyPromise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return new MyPromise((resolve, reject) => {
      resolve(value)
      reject(new Error('something wrong...'))
    })
  },
  (reason) => {
    return reason
  }
)

promise2.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => plasticine
 Copy code 
const promise2 = promise1.then(
  (value) => {
    return new MyPromise((resolve, reject) => {
      //  Asynchronous call  resolve
      setTimeout(() => {
        resolve(value)
      }, 2000)
    })
  },
  (reason) => {
    return reason
  }
)

promise2.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

// => plasticine
 Copy code 

You can see , And native Promise Same effect , stay onFulfilled One was returned in MyPromise Can still be in the back then Get in the resolve Out of the value

And both call resolve Call again reject,reject Neglected , Also with native Promise Act in unison , Call asynchronously at the same time resolve It can also work normally


5. then Callback is an optional parameter

Promise A+ The specification defines then Two callbacks for onFulfilled and onRejected It's optional , That means you can directly .then() call

Then when calling like this, we should resolve Of value Pass through , Until there is then The callback onFulfilled To deal with , So we can give onFulfilled and onRejected The default value is

then(onFulfilled, onRejected) {
  // onFulfilled  and  onRejected  Is an optional parameter , If it is not transmitted, the default value should be set 
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }
  // ...
}
 Copy code 

Test it then Whether the callback can be penetrated when it is not passed in

new MyPromise((resolve, reject) => {
  resolve('plasticine')
})
  .then()
  .then()
  .then()
  .then()
  .then((value) => {
    console.log(value)
  })

// => plasticine
 Copy code 

It can !


6. Realization catch

Is it really finished ? Native Promise Object has one more catch Methods? !

in fact ,catch The thing to do is then What the second callback does , Are used to deal with reject Coming out reason Of ,Promise A+ There is no provision in catch This method , In fact, you can put catch Method as then The syntax of the second callback , That is, only the second callback is passed in :this.then(null, () => {})

So let's reuse it directly then Can be realized catch

catch(onRejected) {
  return this.then(null, onRejected)
}
 Copy code 

7. Complete code

thus , Whole Promise Even if the function of is fully realized , The complete code is as follows

/** * @description MyPromise  The state of  */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

/** *  according to  x  Make different treatment for different types of  * x  For the wrong  MyPromise  When the object  --  direct  resolve * x  by  MyPromise  When the object  --  Call its  resolve * *  The remaining details will be commented in the code  * * @param {MyPromise} promise MyPromise  example  * @param {any} x  Previous  MyPromise resolve  The value of  * @param {Function} resolve promise  In the object constructor  resolve * @param {Function} reject promise  In the object constructor  reject */
function resolvePromise(promise, x, resolve, reject) {
  //  according to  Promise A+  standard  2.3.1: promise  and  x  When pointing to the same reference, you should  reject  One  TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x  yes  object or function  The case when 
  let isCalled = false //  When  x  yes  MyPromise  When an instance , Mark  x.then  Whether any of the callbacks in have been called 

  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object', Therefore, we need to make an additional judgment to exclude  x  yes  null  The situation of 

    try {
      // x.then  May be  Object.defineProperty  Set up  getter  hijacked , And throw an exception , So we have to  try/catch  Capture 
      let then = x.then

      if (typeof then === 'function') {
        // x  Yes  then  When the method is used , Think of it as  MyPromise  object 
        // 2.3.3.3  perform  x.then, And explicitly bind  this  Point to , And there are two callbacks  resolvePromise  and  rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            //  Need to call recursively  MyPromise  in  resolve  The value of going out , That's what we have here  y
            if (isCalled) return //  Any callback has been executed , Ignore the execution of the current callback 

            isCalled = true //  Execute callback for the first time , Set the flag variable to  true, Prevent repeated calls or calls  rejectPromise
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            //  about  reject, Directly  reason  to  reject  Just go out 
            if (isCalled) return //  Any callback has been executed , Ignore the execution of the current callback 

            isCalled = true //  Execute callback for the first time , Set the flag variable to  true, Prevent repeated calls or calls  resolvePromise
            reject(r)
          }
        )
      } else {
        // x  No,  then  Method  --  No special treatment is required , direct  resolve
        resolve(x)
      }
    } catch (e) {
      //  If something goes wrong , Should not be able to call  resolve, Therefore, we should also add  isCalled  Judge , Prevent calling  resolve
      if (isCalled) return

      isCalled = true
      reject(e)
    }
  } else {
    //  Basic data type  --  direct  resolve
    resolve(x)
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PENDING //  The initial state is  PENDING
    this.value = undefined // resolve  The value of going out 
    this.reason = undefined // reject  The reason for going out 

    //  Maintain two list containers   Store the corresponding callback 
    this.onFulfilledCallbackList = []
    this.onRejectedCallbackList = []

    /** * resolve, reject  It's two functions  --  Every  MyPromise  In the instance  resolve  The method is your own  *  If you will  resolve, reject  If defined outside the constructor , Method will be in the constructor  prototype  On  *  So you should define... Inside the constructor  resolve, reject  function  * *  And be sure to use the arrow function to define   It can't be an ordinary function  *  Of a normal function  this  The point is determined when calling externally , There is no guarantee of pointing to the instance itself  *  And there's nothing in the arrow function  this, The parent function of function definition will be used  constructor  Medium  this *  In other words, using the arrow function can ensure  this  Point to the instance itself  * *  The arrow function uses  const  Keyword declares a reference variable to point to it , Therefore, it is necessary to implement  executor  Previously defined  *  Otherwise, it will be unable to find... Due to temporary dead zone  resolve, reject  function  */

    const resolve = (value) => {
      //  Only in  PENDING  State can be switched to  FULFILLED  state 
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value

        //  Trigger  resolve  Behavior , Start  “ Release ” resolved  event 
        this.onFulfilledCallbackList.forEach((fn) => fn())
      }
    }

    const reject = (reason) => {
      //  Only in  PENDING  State can be switched to  REJECTED  state 
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason

        //  Trigger  reject  Behavior , Start  “ Release ” rejected  event 
        this.onRejectedCallbackList.forEach((fn) => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    // onFulfilled  and  onRejected  Is an optional parameter , If it is not transmitted, the default value should be set 
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }

    const promise2 = new MyPromise((resolve, reject) => {
      //  Determine which callback to execute according to the status 
      if (this.status === FULFILLED) {
        //  Execute as a macro task  resolvePromise  To ensure that you get  promise2  example 
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            //  Handle  x
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }

      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            //  Handle  x
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }

      if (this.status === PENDING) {
        // pending  In this state, you need  “ subscribe ” fulfilled  and  rejected  event 
        this.onFulfilledCallbackList.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value)
              //  Handle  x
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.onRejectedCallbackList.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              //  Handle  x
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })

    return promise2
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }
}
 Copy code 

copyright notice
author[Straw hat plastic],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/04/202204291313146671.html

Random recommended