preface
Ha ha, I haven't written a blog for a long time, mainly because I just joined Netease these days and have been adapting to work and all kinds of messy things in school, so I haven't had time to write. I just finished an iteration today. It's rare to have free time to write a blog (actually fishing ~).
Before, I also wrote a hand tearing promise, but it was only code without explanation, so it was not easy to understand. I'm going to start with the theory and then introduce it with the code.
If you haven't learned the basics of promise before reading this article, please visit Promise object of JS
Hahaha, just stop talking nonsense and get straight to the point.
1, Some concepts that Promise must know
1. Higher order function
Concept of higher order function:
(1) The parameter of a function is a function, and we can turn this function into a higher-order function.
(2) A function returns a function, which we can call a higher-order function.
(3) If we meet any of the above points, we can call this function a higher-order function.
2. Decorator mode
Concept (from rookie tutorial):
Decorator Pattern allows you to add new functionality to an existing object without changing its structure. This type of design pattern belongs to structural pattern, which is a wrapper for existing classes.
This pattern creates a decoration class to wrap the original class, and provides additional functions on the premise of maintaining the integrity of class method signature.
Combine higher-order functions with decorator mode, for example:
function say(who) { // Ordinary function console.log("say", who); } // Extend the original function without destroying the original function // @The decorator extends the class Function.prototype.before = function (beforeSay) { // Callback method received return (...args) => { // newSay beforeSay(...args); this(...args); }; }; let beforeSay = (args) => { // Pass in a callback method console.log("say before", args); }; let newSay = say.before(beforeSay); newSay("I"); // The new method should be called here
The decorator pattern is used here to add a new function beforeSay to the original say method
3. Coriolis function
I wrote a blog about Coriolis function before. In short, Coriolis function uses the pre storage function of closures. It converts a function with multiple parameters into a function passed in by parameters.
for instance:
// Originally, fn has four parameters, a, B, C and D function fn(a,b,c,d){...} // Through coritization, it can be transformed into fn(a)(b)(c)(d)
4. Publish subscribe mode
The publish subscribe pattern is a common design pattern in the front end, and the observer pattern widely used in vue is derived from the publish subscribe pattern.
The publish subscribe pattern has a publisher, a subscriber, and an event pool. Publishers publish messages to the event pool. Subscribers can subscribe to messages from the event pool. The event pool has an on method and an emit method. Messages are published through the on method and message events are executed through the emit method. In publish subscribe mode, there is no strong coupling between publisher and subscriber.
For example (files are executed in the node environment):
const fs = require("fs"); let events = { arr = [], on(){ this.arr.push(fn); } emit(){ this.arr.forEach(fn=>fn()); } } events.on(function(){ console.log('Execute after each reading') }) events.on(function(){ if(Object.keys(person).length === 2){ console.log('Read complete') } }) let person= {}; fs.readFile("./a.txt", "utf8", function (err, data) { person.name = data; events.emit(); }); fs.readFile("./b.txt", "utf8", function (err, data) { person.age = data; events.emit(); });
5. Observer mode
There is an observer and an observed in the observer mode. The observed has its own state. When its state changes, all observers are notified to execute the update method to trigger the event. In the observer mode, the observer and the observed are strongly coupled.
for instance:
class Subject { // The observed (need to have a state of its own, and notify all observers when the state changes) constructor(name){ this.name = name this.observers = [] this.state = 'I'm playing happily' } attach(o){ this.observers.push(o); } setState(newState){ this.state = newState; this.observers.forEach(o=>o.update(this)); } } class Observer{ // Observer constructor(name){ this.name = name; } update(s){ console.log(this.name+":" + s.name +'The current status is'+s.state) } } let s = new Subject('baby') let o1 = new Observer('dad'); let o2 = new Observer('mom'); // subscription model s.attach(o1) s.attach(o2) s.setState('Someone bit me, unhappy') s.setState('The teacher praised me and was happy')
6. Handwritten promise Basic Edition
With the above foundation, it will be easier for us to write promise
First, according to the definition of promise:
- promise is a class. You need the new class when using it
- In newPromise, you need to pass in an executor. By default, it will be called immediately, and there are two parameters: resolve and reject
- promise has three statuses: pending, default waiting, onfulfilled, successful, and onrejected
Our promise is pendding by default. When the user calls resolve, it will become successful, and when the user calls reject, it will become failed
Success can pass in the reason for success, and failure can pass in the reason for failure - new Promise will return a promise instance with a then method. The then method has two parameters, one is a successful callback and the other is a failed callback
- There are two cases of trend failure: reject() and the user actively throws an exception
- A promise can then multiple times (publish subscribe mode)
- The promise status cannot change from success to failure, nor can it change from failure to success. The status can only be changed when pending
Let's write a low profile promise
const PENDING = "PENDING"; const FULFILLED = "FULFILLED"; const REJECTED = "REJECTED"; class Promise { constructor(exector) { this.status = PENDING; this.value = undefined; // Reasons for success this.reason = undefined; // Reasons for failure this.onResolvedCallbacks = []; // Store successful callbacks this.onRejectedCallbacks = []; // Store failed callbacks const resolve = (value) => { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onResolvedCallbacks.forEach((fn) => fn()); } }; // Each time new generates two methods reoslve and reject const reject = (reason) => { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach((fn) => fn()); } }; try { exector(resolve, reject); // Two parameters are passed to the user } catch (e) { reject(e); } } then(onFulfilled, onRejected) { if (this.status == FULFILLED) { onFulfilled(this.value); } if (this.status == REJECTED) { onRejected(this.reason); } if (this.status == PENDING) { // It succeeds later. There is other logic besides callback this.onResolvedCallbacks.push(() => { // todo... onFulfilled(this.value); }); this.onRejectedCallbacks.push(() => { // todo... onRejected(this.reason); }); } } } module.exports = Promise;
7. Handwritten promise full version
Next, let's improve the promise we wrote. According to promise a + specification:
- The then method must return a promise
- If onFulfilled or onRejected returns an x, execute the Promise Resolution procedure and pass in the x
- If onFulfilled is not a function and our promise instance is already in the satisfied state, the promise instance returned by then must be in the satisfied state and have the same value as our promise instance
- If onRejected is not a function and our promise instance has failed, the promise instance returned by then must be in failed state and have the same failure reason as our promise instance
- In order to execute Promise Resolution procedure, we need to add a core method in promise specification: resolvePromise.
Upper Code:
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function resolvePromise(x, promise2, resolve, reject) { // x determines whether the state of promise 2 succeeds or fails // All promises should follow this specification, so that promises written by different people can be mixed // The core is in the resolvePromise method } class Promise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onFulfilledCallbacks.forEach(fn => fn()); } } const reject = (reason) => { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } } try { executor(resolve, reject); } catch (e) { resolve(e); } } then(onFulfilledCallback, onRejectedCallback) { let promise2 = new Promise((resolve, reject) => { if (this.status === FULFILLED) { setTimeout(()=>{ try{ let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === REJECTED) { setTimeout(()=>{ try{ let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === PENDING) { this.onFulfilledCallbacks.push(() => { setTimeout(()=>{ try { let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(()=>{ try{ let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); return promise2; } }); } } module.exports = Promise;
After the above content is completed, our own promise has begun to take shape. Next, let's write the core method of promise: resolvePromise method. Also according to Promise/A + specification:
Insert picture description here
- If promise and x refer to the same object, a promise in reject status is returned, and the result is a TypeError
- If x is a promise
- If the X state is pending, promise must keep the pending state until x becomes fully or rejected
- When x is satisfied (that is, when it becomes satisfied), promise is executed with the same value
- When x is rejected, promise is executed for the same reason
- In addition, if x is an object or a function (this place is used to judge whether x is a promise, which is used to regulate promises written by different people, and this rule can override three rules)
- Define a then variable and assign x.then to it
- If retrieving the attribute x.then causes an exception e to be thrown, promise is rejected on the grounds of E
- If then is a function, call it with x as its this. The first parameter is resolvePromise and the second parameter is rejectproject, where:
- If / when the resolvePromise value is y, execute [[Resolve]](promise, y)
- If / when the reason for rejectproject is r, reject project with R
- If resolvePromise and rejectPromise are called at the same time, or multiple calls are made to the same parameter, the first call takes precedence and the other calls are ignored
- If then is called, an exception is thrown e
- If resolvePromise or rejectPromise is called, it is ignored
- Otherwise, reject promise on the grounds of e
- If then is not a function, let x become a satisfied state
- If x is not an object or function, let x become satisfied
Upper Code:
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function resolvePromise(x, promise2, resolve, reject) { // x determines whether the state of promise 2 succeeds or fails if (promise2 === x) { return reject(new TypeError("Circular reference")); } // To judge whether x is a promise, first ensure that x is an object or function. If it is not an object or function, X must not be a promise if ((typeof x === "object" && x !== null) || typeof x === "function") { let called; // We use called to determine whether the following process has been executed. If it has been executed, it will not be executed again // We need to see if there is a then method on this x. if there is a then method, it means that it is a promise try { let then = x.then; //x may be a promise written by others, so taking then is risky if (typeof then === "function") { then.call(x, y => { if (called) return; called = true; resolvePromise(y, promise2, resolve, reject); // Recursively parse until our value of y is a normal value }, r => { if (called) return; called = true; reject(r); }) } else { // No then methods are executed here resolve(x); // Here x is just an ordinary object } } catch (e) { if (called) return; called = true; reject(e); } } else { // Here, X is just an ordinary value. Just pass x directly to promise2 resolve(x); } // All promises should follow this specification, so that promises written by different people can be mixed // The core is in the resolvePromise method } class Promise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onFulfilledCallbacks.forEach(fn => fn()); } } const reject = (reason) => { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } } try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilledCallback, onRejectedCallback) { // It's possible that the onfulfilled callback and onRejectedCallback are optional, so the user didn't fill them in, so we have to supplement them ourselves (penetration feature) onFulfilledCallback = typeof onFulfilledCallback === "function" ? onFulfilledCallback : function (data) { return data; }; onRejectedCallback = typeof onRejectedCallback === "function" ? onRejectedCallback : err =>{ throw err; }; let promise2 = new Promise((resolve, reject) => { if (this.status === FULFILLED) { setTimeout(() => { try { let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === REJECTED) { setTimeout(() => { try { let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === PENDING) { this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; } } // Install this module to test whether the promise you wrote conforms to the specification, and add Promise.deferred // npm install promises-aplus-tests -g // promises-aplus-tests 3.promise // catch Promise.resolve Promise.reject // During the test, it will test whether your promise object complies with the specification Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }) return dfd } module.exports = Promise;
Attach a screenshot of passing the test
8. Improve other functions of promise
Add catch, all and finally methods
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function resolvePromise(x, promise2, resolve, reject) { // x determines whether the state of promise 2 succeeds or fails if (promise2 === x) { return reject(new TypeError("Circular reference")); } // To judge whether x is a promise, first ensure that x is an object or function. If it is not an object or function, X must not be a promise if ((typeof x === "object" && x !== null) || typeof x === "function") { let called; // We use called to determine whether the following process has been executed. If it has been executed, it will not be executed again // We need to see if there is a then method on this x. if there is a then method, it means that it is a promise try { let then = x.then; //x may be a promise written by others, so taking then is risky if (typeof then === "function") { then.call(x, y => { if (called) return; called = true; resolvePromise(y, promise2, resolve, reject); // Recursively parse until our value of y is a normal value }, r => { if (called) return; called = true; reject(r); }) } else { // No then methods are executed here resolve(x); // Here x is just an ordinary object } } catch (e) { if (called) return; called = true; reject(e); } } else { // Here, X is just an ordinary value. Just pass x directly to promise2 resolve(x); } // All promises should follow this specification, so that promises written by different people can be mixed // The core is in the resolvePromise method } class Promise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onFulfilledCallbacks.forEach(fn => fn()); } } const reject = (reason) => { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } } try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilledCallback, onRejectedCallback) { // It's possible that the onfulfilled callback and onRejectedCallback are optional, so the user didn't fill them in, so we have to supplement them ourselves (penetration feature) onFulfilledCallback = typeof onFulfilledCallback === "function" ? onFulfilledCallback : function (data) { return data; }; onRejectedCallback = typeof onRejectedCallback === "function" ? onRejectedCallback : err => { throw err; }; let promise2 = new Promise((resolve, reject) => { if (this.status === FULFILLED) { setTimeout(() => { try { let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === REJECTED) { setTimeout(() => { try { let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === PENDING) { this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFulfilledCallback(this.value); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejectedCallback(this.reason); resolvePromise(x, promise2, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; } catch(errCallback) { return this.then(null, errCallback); } static reject(reason) { return new Promise((resolve, reject) => { reject(reason); }); } static resolve(value) { return new Promise((resolve, reject) => { resolve(value); }); } static all(values) { return new Promise((resolve, reject) => { let times = 0; const arr = []; function processMap(key, value) { arr[key] = value; if (++times === values.length) { resolve(arr); } } for (let i = 0; i < values.length; i++) { let val = values[i]; // It may be promise or normal value let then = val && val.then; if (typeof then === "function") { then.call( val, data => { // Get successful results processMap(i, data); }, reject ); } else { processMap(i, val); } } }); } static race(values) { return new Promise((resolve, reject) => { for (let i = 0; i < values.length; i++) { let p = values[i]; // p may be promise or normal // Whoever succeeds first will succeed, and whoever fails first will fail if (p instanceof Promise) { p.then(resolve, reject); } else { Promise.resolve(p).then(resolve, reject); } } }); }; finally(cb) { return this.then( y => { return Promise.resolve(cb()).then(() => y); }, r => { return Promise.resolve(cb()).then(() => { throw r; }); } ); } } module.exports = Promise;