Promise

Rounding MDN – Promise polyfill

Initialize the Promise class

class Promise { constructor(resolver) { this[PROMISE_ID] = nextId() this._result = this._state = undefined # 1 this._subscribers = [] # 2 if (noop ! == resolver) { typeof resolver ! == 'function' && needResolver() #3 this instanceof Promise ? initializePromise(this, resolver) : needNew() #4 } } finally() {} catch() {} } Promise.prototype.then = then export defatult PromiseCopy the code
  • #1 Initialize Promise contains attributes result result, state state (pending, Fullifilled, Reject) that cannot be changed
  • #2 subscibers(used to initiate subscriptions during asynchronous execution), which will be mentioned later
  • #3 Calling Promise must require passing in arguments of type function
  • #4 Calling Promise must use the constructor to create an instance of new

initializePromise

When you initialize, you basically rely on an initializePromise to do something

function initializePromise(promise, resolver) { try { resolver(function resolvePromise(value){ resolve(promise, value); }, function rejectPromise(reason) { reject(promise, reason); }); } catch(e) { reject(promise, e); }}Copy the code

It is a higher-order function that executes the resolver and passes the two functions as arguments to the resolver for the caller to call manually.

Resolve (1) calls resovlePromise(1), and inside resovlePromise calls resolve.

resolve

function resolve(promise, value) { if (promise === value) { #1 reject(promise, selfFulfillment()); } else if (objectOrFunction(value)) { #2 let then; try { then = value.then; } catch (error) { reject(promise, error); return; } handleMaybeThenable(promise, value, then); } else {// If resovle is not an object or function directly fulfill(promise, value); # 3}}Copy the code
  • The #1 use of the resolve call cannot be passed in to the parent Promise
  • #2 Resovle calls that pass in an object or function will check for the then attribute. If then does not exist, the fulfill handler continues. I’m not going to go into this case.
  • If the value passed to resolve does not have a THEN attribute and is not a promise, the fulfill function will be executed

fulfill

const PENDING = void 0 #1 const FULFILLED = 1 const REJECTED = 2 function fulfill(promise, value) { if (promise._state ! == PENDING) { return; } #2 promise._result = value; promise._state = FULFILLED; if (promise._subscribers.length ! == 0) { #3 asap(publish, promise); }}Copy the code
  • Void 0 === undefined; void 0 === undefined
  • #2 If the current PROMISE state is not the initial state, result will be changed to resolve and state will be changed from the initial state
  • If subscribers of the current promise have subscribed messages, ASAP will be executed.

The ASAP function will perform the subscription callback, which is essentially for asynchronous cases. The execution timing of resolve determines the call timing of FULFILL. Assume that code executes asynchronously, and then resolve executes after some time. Resolve calls FULFILL to change the state and result of the Promise. In the meantime, new Promise().then() is actually executed before that. Condition #3 is true only if.then is executed first.

then

Function then(onFulfillment, onRejection) {// parent promise const parent = this; Const child = new this.constructor(noop); const child = new this.constructor(noop); if (child[PROMISE_ID] === undefined) { makePromise(child); } const { _state } = parent; If (_state) {#1 return const callback = arguments[_state - 1]; asap(() => invokeCallback(_state, child, callback, parent._result)); [// Add child ondepressing onRejection SUBSCRIBE (parent, child, subscribed) onFulfillment, onRejection); } return child; # 3}Copy the code

When we execute the then function, we create an internal parent that refers to the currently invoked instance object promise, and child that refers to the Promise class. The initial state of the current promise will only change if the fulfill mentioned above executes.

  • #1 The current state of a promise is not PENDING. If the status has changed, synchronization is performed. And then CALL ASAP
  • The current state of the promise is PENDING. Now the state is not changed and SUBSCRIBERS are added to _SUBSCRIBERS. The #3 condition will be established when FULFILL is executed.
  • #3 That’s why promises can be called chained, returning a new Promise instance object
  • #4 ASAP passes in four values. _state is the state of the current PROMISE, child is the newly created instance, callback is the then callback, and finally the _result value of the current Promise

asap

let len = 0 const queue = new Array(1000); If (isNode) {scheduleFlush = useNextTick(); } else if (BrowserMutationObserver) {// scheduleFlush = useMutationObserver(); } else if (isWorker) { scheduleFlush = useMessageChannel(); } else if (browserWindow === undefined && typeof require === 'function') { scheduleFlush = attemptVertx(); } else { scheduleFlush = useSetTimeout(); } export var asap = function asap(callback, arg) { console.log('implement asap... ') queue[len] = callback; queue[len + 1] = arg; len += 2; if (len === 2) { // If len is 2, that means that we need to schedule an async flush. // If additional callbacks are queued before the queue is flushed, they // will be processed by this flush that we are scheduling. if (customSchedulerFn) { customSchedulerFn(flush); } else { scheduleFlush(); // execute}}}Copy the code

The ASAP function adds a callback function to the [len] subscript of the queue to detect the execution environment. For now, we only consider the browser environment and call the useMutationObserver function

function useMutationObserver() {
  let iterations = 0;
  const observer = new BrowserMutationObserver(flush);
  const node = document.createTextNode('');
  observer.observe(node, { characterData: true });

  return () => {
    node.data = (iterations = ++iterations % 2);
  };
}

function flush() {
  for (let i = 0; i < len; i+=2) {
    let callback = queue[i];
    let arg = queue[i+1];

    callback(arg);

    queue[i] = undefined;
    queue[i+1] = undefined;
  }

  len = 0;
}
Copy the code

The useMutationObserver uses the browser-provided MutaionObserver method to create a microtask that performs flush on behalf of the call. This reminds me of the implementation of nextTick in the Vue source code. Vue nextTick uses promise first, then MutationObservere, then Immediate, and finally setTimeout. Flush will reset the queue and len, and callback will be executed. This callback is wrapped in invokeCallback.

invokeCallback

function invokeCallback(settled, promise, callback, detail) { let hasCallback = isFunction(callback), value, error, succeeded = true; if (hasCallback) { try { value = callback(detail); #1 } catch (e) { succeeded = false; error = e; } if (promise === value) { reject(promise, cannotReturnOwn()); return; } } else { value = detail; } // The promise is passed to the child defined in the then function and the new instance if (promise._state! == PENDING) { // noop } else if (hasCallback && succeeded) { resolve(promise, value); Resolve} else if (Succeeded === false) {reject(promise, error); } else if (settled === FULFILLED) { fulfill(promise, value); } else if (settled === REJECTED) { reject(promise, value); }}Copy the code
  • #1 Caches the value of value to get the onFulfillment value as the _result property of the return child. Scene: the Promise (). Then (() = > 1). Then (res = > console. The log (res)) / / 1

    Value = callback(detail) This step is where the callback function in promise.then (function callback() {}) is actually called.

    At this point, the first synchronous execution in the THEN function ends. In the second asynchronous case where the state has not changed, subscribe is executed

subscribe

function subscribe(parent, child, onFulfillment, onRejection) {
  let { _subscribers } = parent;           
  let { length } = _subscribers;

  parent._onerror = null;

  _subscribers[length] = child;                     #1
  _subscribers[length + FULFILLED] = onFulfillment;
  _subscribers[length + REJECTED]  = onRejection;

  if (length === 0 && parent._state) {           
    asap(publish, parent);
  }
}

function publish(promise) {
  let subscribers = promise._subscribers;
  let settled = promise._state;

  if (subscribers.length === 0) { return; }

  let child, callback, detail = promise._result;

  for (let i = 0; i < subscribers.length; i += 3) {
    child = subscribers[i];
    callback = subscribers[i + settled];

    if (child) {
      invokeCallback(settled, child, callback, detail);
    } else {
      callback(detail);
    }
  }

  promise._subscribers.length = 0;
}
Copy the code
  • [subscribers] Add child to parent’s _subscribers. L_subscribers length is checked in DESCENDANT and ASAP (publish, promise) is implemented if subscribers exist. Because I mentioned earlier that the timing of the FULFILL call may be after then.

As FULFILL is invoked, publish execution invokes ASAP, WHICH invokes PUBLISH, which invokes invokeCallback. There are just more publish subscriptions than synchronous execution.

Asynchronous execution:

first
-> subscribe
async...
-> fulfill -> asap -> publish -> invokeCallback
Copy the code

Synchronous execution:

-> asap -> invokeCallback
Copy the code