This article is also published in my github blog Welcome to star
Previously, I wrote a simple promise by hand. This time, in order to pass the official Proise A + test set, I borrowed some promises Polyfill with more downloads and changed it several times. Finally, I passed 872 test cases of A + specification.
How to test?
The test library address is here: promises-tests After you have written your promises, you may also want to test whether your promises conform to the Promise A + specification. This library is very convenient to use, as follows:
const tests = require("promises-aplus-tests"); const Promise = require("./index"); const deferred = function() { let resolve, reject; const promise = new Promise(function(_resolve, _reject) { resolve = _resolve; reject = _reject; }); return { promise: promise, resolve: resolve, reject: reject }; }; const adapter = { deferred }; tests.mocha(adapter);
Among them, the Promise you wrote in index.js
Realization
First, we define some global attributes:
const IS_ERROR = {}; let ERROR = null;
IS_ERROR is used as the identification when an error occurs, and ERROR is used to save the error.
Prepare and define the _Promise class, where fn is the function accepted by Promise and called immediately when the constructor executes; _status is the state of Promise, initially 0 (pending), resolved is 1, rejected is 2; _value is used to save the return value of Promise resolved and the failure information of rejected; _handler S is used to save the processing method invoked when Promise succeeds or fails
function _Promise(fn) { this._status = 0; this._value = null; this._handlers = []; doFn(this, fn); }
Finally, the doFn method is executed, passing in this value and fn:
function doFn(self, fn) { const ret = safeCallTwo( fn, function(value) { self.resolve(value); }, function(reason) { self.reject(reason); } ); if (ret === IS_ERROR) { self.reject(ERROR); } }
safeCallTwo is a function used to safely execute the two-parameter method. When an error occurs, it captures the error, saves it in ERROR, and returns IS_ERROR identifier:
function safeCallTwo(fn, arg1, arg2) { try { return fn(arg1, arg2); } catch (error) { ERROR = error; return IS_ERROR; } }
In doFn, call safeCallTwo, fn passes in two parameters for us to call, that is, our commonly used resolve method and reject method, and gets the return value. If ret identifies IS_ERROR as an error, reject is called.
_ The Promise prototype contains resolve and reject methods, as follows:
_Promise.prototype.resolve = function(value) { if (this._status !== 0) { return; } this._status = 1; this._value = value; doThen(this); }; _Promise.prototype.reject = function(reason) { if (this._status !== 0) { return; } this._status = 2; this._value = reason; doThen(this); };
Because the status of Promise can only be changed from pending to resolved and rejected, when executing the resolve and reject methods, it is necessary to determine whether status is zero or not, and return directly; after modifying status and value, the doThen method is executed:
function doThen(self) { const handlers = self._handlers; handlers.forEach(handler => { doHandler(self, handler); }); }
The doThen function takes handlers from self and executes them in turn.
Let's look again at the then method mounted on the prototype:
_Promise.prototype.then = function(onResolve, onReject) { const res = new _Promise(function() {}); preThen(this, onResolve, onReject, res); return res; };
We know that Promise supports chain invocation, so our then method also returns a Promise for subsequent invocation.
The following is the preThen method:
function preThen(self, onResolve, onReject, res) { onResolve = typeof onResolve === "function" ? onResolve : null; onReject = typeof onReject === "function" ? onReject : null; const handler = { onResolve, onReject, promise: res }; if (self._status === 0) { self._handlers.push(handler); return; } doHandler(self, handler); }
The preThen method accepts four values: the current Promise-self, the callback function onResolve after resolve, the reject ed callback function onReject, and the promise-res returned by the then function. First, determine whether onResolve and onReject are functions, if not, set them directly to null. Then put onResolve, onReject, and res into handler objects
Next, it should be noted that the functions that Promise accepts (that is, fn above) are not necessarily asynchronous calls to resolve and reject, but also synchronous. That is to say, when the preThen function is executed, the status of self may be no longer 0, so we do not need to save handler for call, but straight. Receive callbacks
The doHandler function code is shown below:
function doHandler(self, handler) { setTimeout(() => { const { onReject, onResolve, promise } = handler; const { _status, _value } = self; const handlerFun = _status === 1 ? onResolve : onReject; if (handlerFun === null) { _status === 1 ? promise.resolve(_value) : promise.reject(_value); return; } const ret = safeCallOne(handlerFun, _value); if (ret === IS_ERROR) { promise.reject(ERROR); return; } promise.resolve(ret); }); }
We know that even if relove or reject is executed synchronously, the callback function accepted by the then function will not execute synchronously immediately. The following code will output 1, 3, 2, instead of 1, 2, 3 in turn.
const p = new Promise(resolve => { console.log(1); resolve(); }); p.then(() => { console.log(2); }); console.log(3);
Here, I use setTimeout to simulate this pattern. Of course, it's just a rough simulation. A better way is to introduce or implement libraries like asap (I might implement this next week, haha), but setTimeout is also good enough to pass the test.
In the doHandler function, we call the corresponding callback function. It should be noted that if the corresponding callback function is null (null is the unified assignment when the previous judgement callback function is not function), then we call the promise resolve or reject method returned by the then function directly.
Likewise, we used safeCallOne to catch errors, which is not discussed here.
At this point, we executed the test and found no unexpected failure, because we only implemented the basic Proise, but not the nable function in resolve. Here is the description of the nable by mdn:
Returns a Promise object whose state is determined by a given value. If the value is thenable (that is, the object with the then method), the final state of the Promise object returned is determined by the then method; otherwise (the value is empty, the basic type or the object without the then method), the Promise object returned is fulfilled, and the value is passed to the corresponding then method. Usually, if you don't know if a value is a Promise object, use Promise.resolve(value) to return a Promise object, so that you can use the value as a Promise object.
Let's modify the resolve method:
_Promise.prototype.resolve = function(value) { if (this._status !== 0) { return; } if (this === value) { return this.reject(new TypeError("cant's resolve itself")); } if (value && (typeof value === "function" || typeof value === "object")) { const then = getThen(value); if (then === IS_ERROR) { this.reject(ERROR); return; } if (value instanceof _Promise) { value.then( value => { this.resolve(value); }, reason => { this.reject(reason); } ); return; } if (typeof then === "function") { doFn(this, then.bind(value)); return; } } this._status = 1; this._value = value; doThen(this); };
First judge whether this and value are a Promise, and if so, throw an error.
Then determine whether the value type is function or object, and if so, implement getThen method for error capture:
function getThen(self) { try { return self.then; } catch (error) { ERROR = error; return IS_ERROR; } }
If the value instance of _Promise is detected, and if it is true, the state and value or reason of value are used directly.
If the function is the function, then the value of the function is the value of this, which is executed as fn, that is to say, the effect of the following code is achieved:
const p = new Promise(resolve => { resolve({ then: _resolve => { _resolve(1); } }); }); p.then(value => console.log(value)); //Print 1
We ran the test again and found that there were still errors because of the following circumstances:
const p = new _Promise(resolve => { resolve({ then: _resolve => { setTimeout(() => _resolve(1)), 500; } }); resolve(2); }); p.then(value => console.log(value));
At this point, using our Promise, the output is 2, and in the specification, it should be 1.
The reason is that we resolve asynchronously in the then method of the object. At this time, the following resolution (2) is executed, status has not changed, so we can naturally modify status and value.
The solution is also simple. It is only used in the doFn method to determine whether the first execution is performed:
function doFn(self, fn) { let done = false; const ret = safeCallTwo( fn, function(value) { if (done) { return; } done = true; self.resolve(value); }, function(reason) { if (done) { return; } done = true; self.reject(reason); } ); if (ret === IS_ERROR) { if (done) { return; } done = true; self.reject(ERROR); } }
When the test is executed again, it is found that all the test cases have passed to ~
Code
The complete code has been placed on my github at the address of https://github.com/Bowen7/playground/tree/master/promise-polyfill clone my playground project, go to the promise-polyfill directory npm install, and then execute npm test to run the test