Hand tear source code series - functor + observer mode + status = Promise

Keywords: Javascript Programming TypeScript Attribute github

Preface

Some time ago, I was too busy to write a blog for nearly a month, but Promise had summed up a wave of how to realize it long ago, but at that time, it was purely for the purpose of realization, without elaborating some of the clever designs. Until recently, when I was learning the knowledge related to functional programming, I found that Promise and Functor were inadvertently looking up the data Actually, there are countless relationships, which makes me decide to reexamine my understanding of Promise, so I have this "old wine new clothes" blog.

Pre knowledge

To fully read and understand this blog, I roughly estimated that the following pre knowledge is needed. Students who don't know can learn it first by themselves:

  • The idea of recursion
  • Basic cognition of ES6 and Typescript
  • Functor and the thought of "function is first class citizen" in functional programming
  • Observer pattern in design pattern

Implement Promise step by step

We can compare the realization of a Promise to the construction of a building. By dividing each step and solving, understanding and remembering, we can quickly understand its realization.

I. lay Promise Foundation

Since we often use Promise with async/await to implement asynchronous programming, let's recall how to use Promise most basically

new Promise(resolve => {...})
    .then(onFulfilled)
    .then(onFulfilled)

According to the most basic Promise we recall above, we can write the following implementation:

class Promise {
    constructor(executor) {
        const resolve = () => {}
        
        executor(resolve)
    }
    
    then = onFulfilled => new Promise(...)
}

OK, then our first step is over. Isn't it very simple? It doesn't need any cost to understand and remember.

II. Adding raw material functor and observer model

In this step, we begin to add things to it. We also need to understand what is added, so let's look at the basic concepts of raw materials.

Functor

As mentioned in the previous knowledge, I believe you have known about it. A basic functor can be written as follows:

class Functor {
    static of = value => new Functor(this.value)
    
    constructor(value) {
        this.value = value
    }
    
    map = fn => Functor.of(fn(this.value))
}

In order to facilitate the mapping to the implementation of Promise, it is written as follows:

class Functor {
    constructor(value) {
        this.value = value
    }
    
    map = fn => new Functor(fn(this.value))
}

Then it combines some properties of functors:

const plus = x + y => x + y
const plusOne = x => plus(x, 1)

new Functor(100)
    .map(plusOne)
    .map(plusOne) // { value: 102 }

This is the time to give full play to our ability of finding rules that we have been trained since childhood

  1. The structure of the two is similar, with a method to operate on the internal data
  2. Both can be linked

Through the above two points, we can get a conclusion that the introduction of functor can solve the problem of chain call, but one functor is not enough. Functor can only realize synchronous chain call, and then another raw material observer mode will appear.

Observer mode

Let's start with a simple observer pattern implementation:

class Observer {
  constructor() {
    this.callback = []

    this.notify = value => {
      this.callback.forEach(observe => observe(value))
    }
  }

  subscribe = observer => {
    this.callback.push(observer)
  }
}

At this time, smart people will find out that the notify and subscribe are resolve and then!

I don't know! No matter what! Magic moves!

ターンエンド!

class Promise {
    constructor(executor) {
        this.value = undefined
        this.callback = []
        
        // Equivalent to notify
        const resolve = value => {
            this.value = value
            this.callback.forEach(callback => callback())
        }
        
        executor(resolve)
    }
    
    // Equivalent to subscribe or map
    then = onFulfilled => new Promise(resolve => {
        this.callback.push(() => resolve(onFulfilled(this.value)))
    })
}

The integrated primary Promise has the ability of asynchronous chain call, such as:

const promise = new Promise(resolve => {
    setTimeout(() => {
        resolve(100)
    }, 500)
})
    .map(plusOne)
    .map(plusOne)
    // { value: 102 }

But when we do some operations, we still have problems:

const promise = new Promise(resolve => {
    setTimeout(() => {
        resolve(100)
        resolve(1000)
    }, 500)
})
    .map(plusOne)
    .map(plusOne)
    // { value: 1002 }

In order to solve this problem, we need a state of raw materials.

Limited space, this part of the more detailed conversion process, my repo There are records.

III. status of raw materials added

As we all know, < span style = "text decoration line: line through" > "blue eye research dragon needs three blue eye white dragons" < / span >. In order to solve the problems left by the previous part, this part needs to add the raw material of state to Promise.

class Promise {
  static PENDING = 'PENDING'
  static FULFILLED = 'FULFILLED'

  constructor(executor) {
    this.value = undefined
    this.callbacks = []
    this.status = Promise.PENDING

    // A series of operations (change of state, execution of successful callback)
    const resolve = value => {
      // Only promise in the pending state can call resolve
      if (this.status === Promise.PENDING) {
        // After the resolve call, the status changes to completed
        this.status = Promise.FULFILLED
        // Store the final value of fulfilled
        this.value = value
        // Once resolve is executed, call the callback stored in the callback array
        this.callbacks.forEach(callback => callback())
      }
    }

    executor(resolve)
  }

  then = onFulfilled =>
    new Promise(resolve => {
      // When status is Fulfilled
      if (this.status === Promise.FULFILLED) {
        resolve(onFulfilled(this.value))
      }
      // When status is Pending
      if (this.status === Promise.PENDING) {
        // Save onFulfilled to callback array
        this.callbacks.push(() => resolve(onFulfilled(this.value)))
      }
    })
}

So far, the Promise built by three major raw materials has been completed, of course, there are many functions that have not been realized, < span style = "text decoration line: line through" > Lu Xun once said: < / span > "stand on the shoulders of giants to see problems." , next, we need Promise/A + specification to help us implement a Promise with complete functions.

IV. open design drawing Promise/A + specification

Sword! Promise/A + specification , the next operation needs to follow it step by step.

1. reject and onRejected

In fact, we don't need to standardize this step. We also know that project has two end states: fully and rejected, so we need to supplement the rest of rejected and some related operations.

class Promise {
  ......
  static REJECTED = 'REJECTED'
  
  constructor(executor) {
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []
    this.status = PromiseFunctorWithTwoStatus.PENDING

    // A series of operations after success (change of state, execution of successful callback)
    const resolve = value => {
        ......
    }
    
    // A series of operations after a failure (change of state, execution of a failed callback)
    const reject = reason => {
      // Only promise in the pending state can call resolve
      if (this.status === Promise.PENDING) {
        // When reject is called, status changes to rejected
        this.status = Promise.REJECTED
        // Store rejected
        this.reason = reason
        // Once reject is executed, call the callback stored in the failed callback array
        this.onRejectedCallbacks.forEach(onRejected => onRejected())
      }
    }

    executor(resolve, reject)
  }

  then = (onFulfilled, onRejected) =>
    new Promise(resolve => {
      // When status is Fulfilled
      ......
      
      // When status is Rejected
      if (this.status === PromiseFunctorWithTwoStatus.REJECTED) {
        reject(onRejected(this.reason))
      }
      
      // When status is Pending
      if (this.status === Promise.PENDING) {
        // Save onFulfilled to callback array
        this.onFulfilledCallbacks.push(() => resolve(onFulfilled(this.value)))
        // Store onRejected into the failed callback array
        this.onRejectedCallbacks.push(() => reject(onRejected(this.reason)))
      }
    })
}

2. Add the core resolvePromise method to realize the solution process

The Promise solving process is an abstract operation. It needs to enter a Promise and a value, which we express as [[Resolve]](promise, x). If x has the then method and looks like a Promise, the solving program tries to make Promise accept the state of X; otherwise, it uses the value of X to execute Promise.

This kind of tenable feature makes the implementation of Promise more general: as long as it exposes a then method that follows Promise/A + protocol; this also enables the implementation that follows Promise/A + specification to coexist well with those that are less standardized but available.

According to the specification description, we write the code implementation according to the implementation steps given by him:

class Promise {
    ......
    static resolvePromise = (anotherPromise, x, resolve, reject) => {
      // If onFulfilled or onRejected returns a value of X, run the following project solution: [[resolve]] (project2, x)
      // To run [[Resolve]](promise, x), follow these steps:
        
      // If promise and x point to the same object, reject promise with TypeError to prevent circular references
      if (anotherPromise === x) {
        return reject(new TypeError('Chaining cycle detected for promise'))
      }
      
      // If x is promise, make promise accept the state of X
      if (x instanceof Promise) {
          x.then(
          // If x is in the execution state, perform promise with the same value
          value => {
            return Promise.resolvePromise(anotherPromise, value, resolve, reject)
          },
          // If x is in reject state, reject promise with the same reject reason
          reason => {
            return reject(reason)
          }
        )
        // If x is an object or function
      } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
          let called = false
          try {
              // Assign x.then to then (in this step, we first store a reference to x.then, and then test and call the reference to avoid multiple access to the x.then attribute. This precaution ensures the consistency of the attribute because its value may be changed when retrieving the call.)
              const then = x.then
              // If then is a function, x is called as the scope of this function. Pass two callback functions as parameters,
              if (typeof then === 'function') {
                then.call(
                    x,
                    // The first parameter is called resolvePromise,
                    value => {
                        // If resolveproject and rejectproject are both called, or are called more than once by the same parameter, the first call is preferred and the remaining calls are ignored
                        if (called) {
                            return 
                        }
                        called = true
                        // If resolvePromise is called with the value y as the parameter, run [[Resolve]](promise, y)
                        return Promise.resolvePromise(
                          anotherPromise,
                          value,
                          resolve,
                          reject
                        )
                    },
                    // The second parameter is called rejectproject
                    reason => {
                        // If resolveproject and rejectproject are both called, or are called more than once by the same parameter, the first call is preferred and the remaining calls are ignored
                        if (called) {
                          return
                        }
                        called = true
                        // If rejectproject is called with reject r as the parameter, reject project with reject R
                        return reject(reason)
                    }
                )
              } else {
                  //If then is not a function, perform promise with an x parameter
                  return resolve(x)
              }
          } catch (error) {
              // If the call to the then method throws an exception e, if resolveproject or rejectproject has already been called, it is ignored
              if (called) {
                  return
              }
              called = true
              // If an error E is thrown when taking the value of x.then e is taken as the rejection of promise
              return reject(error)
          }
      } else {
          // If x is not an object or function, perform promise with X as an argument
          return resolve(x)
      }
    }
    ......
}

At the same time, we need to transform the resolve method in Promise

class Promise {
    ......
    constructor(executor) {
        ......
        // A series of operations after success (change of state, execution of successful callback)
        const resolve = x => {
          const __resolve = value => {
            // Only promise in the pending state can call resolve
            if (this.status === Promise.PENDING) {
              // After the resolve call, the status changes to completed
              this.status = Promise.FULFILLED
              // Store the final value of fulfilled
              this.value = value
              // Once resolve is executed, the callback stored in the success callback array is called
              this.onFulfilledCallbacks.forEach(onFulfilled => onFulfilled())
            }
          }
          return Promise.resolvePromise.call(this, this, x, __resolve, reject)
        }
        ......
    }
    ......
}
class Promise {
    ......
    then = (onFulfilled, onRejected) => {
        // The then method must return a promise object
        const anotherPromise = new Promise((resolve, reject) => {
          // Encapsulate the method of processing chain call
          const handle = (fn, argv) => {
            // Ensure that the onFulfilled and onRejected methods execute asynchronously
            setTimeout(() => {
              try {
                const x = fn(argv)
                return Promise.resolvePromise(anotherPromise, x, resolve, reject)
              } catch (error) {
                return reject(error)
              }
            })
          }
          // When status is Fulfilled
          if (this.status === Promise.FULFILLED) {
            // Then execute onFulfilled, value as the first parameter
            handle(onFulfilled, this.value)
          }
          // When status is Rejected
          if (this.status === Promise.REJECTED) {
            // Then execute onRejected, reason as the first parameter
            handle(onRejected, this.reason)
          }
    
          // When status is Pending
          if (this.status === Promise.PENDING) {
            // Save onFulfilled to success callback array
            this.onFulfilledCallbacks.push(() => {
              handle(onFulfilled, this.value)
            })
            // Store onRejected into the failed callback array
            this.onRejectedCallbacks.push(() => {
              handle(onRejected, this.reason)
            })
          }
        })
    
        return anotherPromise
    }
    ......
}

3. Add other methods to improve the surrounding areas

The main body of Promise has been written. Next, we need to implement some other auxiliary methods to improve it.

  • catch
catch = onRejected => {
    return this.then(null, onRejected)
}
  • finally
finally = fn => {
    return this.then(
        value => {
            setTimeout(fn)
            return value
        },
        reason => {
            setTimeout(fn)
            throw reason
        }
    )
}
  • resolve
static resolve = value => new Promise((resolve, reject) => resolve(value))
  • reject
static reject = reason => new Promise((resolve, reject) => reject(reason))
  • all
static all = promises => {
    if (!isArrayLikeObject(promises)) {
      throw new TypeError(
        `${
          typeof promises === 'undefined' ? '' : typeof promises
        } ${promises} is not iterable (cannot read property Symbol(Symbol.iterator))`
      )
    }
    
    // The implementation of promise is based on the setTimeout implementation of macroTask, which requires async/await to adjust the execution order
    // The native promise is based on the implementation of microTask, the execution order is correct, and async/await is not required
    return new Promise(async (resolve, reject) => {
      const result = []

      for (const promise of promises) {
        await Promise.resolve(promise).then(resolvePromise, rejectPromise)
      }

      return resolve(result)

      function resolvePromise(value) {
        if (value instanceof Promise) {
          value.then(resolvePromise, rejectPromise)
        } else {
          result.push(value)
        }
      }

      function rejectPromise(reason) {
        return reject(reason)
      }
    })
}
  • race
static race = promises => {
    if (!isArrayLikeObject(promises)) {
      throw new TypeError(
        `${
          typeof promises === 'undefined' ? '' : typeof promises
        } ${promises} is not iterable (cannot read property Symbol(Symbol.iterator))`
      )
    }
    
    return new Promise((resolve, reject) => {
      for (const promise of promises) {
        Promise.resolve(promise).then(
          value => resolve(value),
          reason => reject(reason)
        )
      }
    })
}

4. Add some robust code

This part is basically mended to enhance the robustness of Promise

  • Verify executor
constructor(executor) {
    // Parameter checking
    if (typeof executor !== 'function') {
      throw new TypeError(`Promise resolver ${executor} is not a function`)
    }
}
  • Checking on fulfilled and on rejected by using the idea of May functor
then = (onFulfilled, onRejected) => {
    // If onFulfilled is not a function, it must be "ignored"
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value => value

    // If onFulfilled is not a function, it must be "ignored"
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : error => {
            throw error
          }
}

V. decoration in Typescript style

This part will not be written, repo There are records in it.

Vi. whether the test conforms to Promise/A + specification

We use a library to check the written Promise:

Add the required glue code:

class Promise {
    ......
    static defer = () => {
        let dfd: any = {}
        dfd.promise = new Promise((resolve, reject) => {
          dfd.resolve = resolve
          dfd.reject = reject
        })
        return dfd
    }

    static deferred = Promise.defer
    ......
}
npm i promises-aplus-tests -D

npx promises-aplus-tests promise.js

summary

Recently, in the process of reading the materials, I really realized what is "learning from the past" and "front-end knowledge, though miscellaneous, is related". In the beginning, the implementation of promise was poorly written, but it came back when learning functional programming. This feeling is so wonderful that people can't help admiring the first person who generated promise idea. Promise will functor and observer mode
Combined with state and Promise, an asynchronous solution can be realized.

The space is limited, and some mistakes are inevitable. Welcome to discuss and teach~
Attach a full repo address of GitHub: https://github.com/LazyDuke/ts-promise-from-functor-observer

Epilogue

This is a series of articles:

Posted by Nightseer on Sun, 15 Dec 2019 12:17:47 -0800