Recently, I revisited the design explanation of Q/Promise , combined with my own understanding and some small optimizations, I decided to write an article about handwritten Promise. The content of this article is suitable for children's shoes who have a certain understanding of the use of Promise, because the basic operation of Promise will not be explained too much in the process. We start with a basic version and work through the Promise incrementally, sharing my understanding and perspective along the way. The content may be a bit long, so without further ado, let's get started.
base version
We first use the observer pattern as the cornerstone to build a basic version, which implements the following functions:
- The constructor accepts a function
exector
as a parameter, the first parameter of the function isresolve
, the function is to change the state of the Promise object to "success". - The prototype method
then
is used to register a callback function when the status becomes successful. When the callback is triggered, the parameter is the resolution value ofresolve
.
function Promise(exector) {
this.pending = [];
this.value = undefined;
const resolve = value => {
if (this.pending) {
this.value = value;
for (const onFulfilled of this.pending) {
// 通知观察者。
onFulfilled(this.value);
}
this.pending = undefined;
}
};
exector(resolve);
}
Promise.prototype.then = function (onFulfilled) {
if (this.pending) {
// 还没决议,先注册观察者。
this.pending.push(onFulfilled);
} else {
// 已决议,直接通知。
onFulfilled(this.value);
}
};
// 测试一下。
const p = new Promise(resolve => {
setTimeout(() => resolve(666), 100);
})
p.then(res => console.log('res: %s', res));
// 输出:
// res: 666
The code is very simple and should not need to be explained too much. The complete code above is here: p0.js .
There is an obvious problem with this basic version: then
can't make chained calls, so let's optimize it.
then
chain call
Chaining calls to then returns a new Promise, and the return value of the callback in then
causes this new Promise to resolve to the "success" state.
Promise.prototype.then = function (onFulfilled) {
// “当前”Promise,对于返回的新 Promise 而言,也是“前一个”Promise。
const prev = this;
const promise = new Promise(resolve => {
// 包装 onFulfilled,使其可以“传播”决议;
// “前一个” Promise 决议后,决议返回的这个新 Promise。
const onSpreadFulfilled = function (value) {
resolve(onFulfilled(value));
};
if (prev.pending) {
prev.pending.push(onSpreadFulfilled);
} else {
onSpreadFulfilled(prev.value);
}
});
return promise;
};
// 测试一下。
const p = new Promise(resolve => {
setTimeout(() => resolve(666), 100);
});
p.then(res => {
console.log('res1: %s', res);
return res + 1;
).then(res => {
console.log('res2: %s', res);
);
// 输出:
// res1: 666
// res2: 667
The key to implementing chaining is how to resolve the new Promise that is returned? Here I made some meaningful names for the variables for easy understanding:
-
prev
then
called. For the new Promise returned, it can be regarded as the "previous" Promise. - Wrapping onFulfilled - After executing the currently registered onFulfilled, use its return value to determine the new Promise returned. This is a key step, named
onSpreadFulfilled
to reflect the action of propagation. - Register
onSpreadFulfilled
as a successful callback toprev
.
The full code for the above is here: p1.js .
Now there is a new problem, if the resolve
of value
is a Promise, or onfulfilled
the value returned by the function should not be a Promise, then the chain propagation resolution should not be It is the Promise itself, but the resolution value of the Promise, that is, to support the state transfer of the Promise .
state transfer
Before implementing state transfer, let's first see how Kangkang determines whether a value is a Promise. We can use prototypal inheritance to determine:
return value instanceof Promise;
The disadvantage of this is that the compatibility is poor, you can't force the user's runtime context to use only one Promise library, or pass Promise instances in different runtime contexts. So here we use duck type to judge Promise, focus on the behavior of the object , treat Promise as a thenable
object.
function isPromise(value) {
// 如果这个对象上可以调用 then 方法,就认为它是一个“Promise”了。
return value && typeof value.then === 'function';
}
The next step is to implement state transfer. The idea of implementation is based on duck typing and "notification transfer". Let's define a function first:
function wrapToThenable(value) {
if (isPromise(value)) {
return value;
} else {
return {
then: function (onFulfilled) {
return wrapToThenable(onFulfilled(value));
}
};
}
}
As the name implies, the function of this function is to wrap a value as a thenable
object: if the value is a Promise, return it directly; if not, wrap it and return an object with a then
method, That is thenable
object. What is the function of this thenable
object? Then look here:
function Promise(exector) {
this.pending = [];
this.value = undefined;
const resolve = value => {
if (this.pending) {
// 包装为 thenable。
this.value = wrapToThenable(value);
for (const onFulfilled of this.pending) {
// 通知时改为调用 thenable 上的 then。
this.value.then(onFulfilled);
}
this.pending = undefined;
}
};
exector(resolve);
}
resolve
When the resolution is made, there are two processing situations according to the type of value
:
- If
value
is a common value, afterwrapToThenable
it will be packaged asthenable
1051509b7c55b383941d34f61538ddfc--- method call directly, which is equivalent to callingonFulfilled
then
onFulfilled
. - If
value
is a Promise, putonFulfilled
registered tovalue
on; waitvalue
when the resolution will callonFulfilled
. RememberonSpreadFulfilled
when chaining calls? This is the "notification transfer", which transfers the responsibility of notifying the next Promise tovalue
.
Of course then
also make a little modification:
Promise.prototype.then = function (onFulfilled) {
const prev = this;
const promise = new Promise(resolve => {
const onSpreadFulfilled = function (value) {
resolve(onFulfilled(value));
};
if (prev.pending) {
prev.pending.push(onSpreadFulfilled);
} else {
// 这里也要改为调用 then。
prev.value.then(onSpreadFulfilled);
}
});
return promise;
};
// 测试一下。
const p = new Promise(resolve => {
setTimeout(() => resolve(666), 100);
});
p.then(res => {
console.log('res1: %s', res);
return new Promise(resolve => {
setTimeout(() => resolve(777), 100);
});
}).then(res => {
console.log('res2: %s', res);
});
// 输出:
// res1: 666
// res2: 777
Here is a summary of the design ideas of state transfer. The package is thenable
the object is very critical, and its function is to maintain the behavior consistent with Promise, that is, the interface is consistent. In this way, when resolve
, we do not need to specifically determine whether this value is a Promise, but can use a unified processing method to notify the observer; and also complete the "notification transfer" by the way, if value
If there is no resolution yet, then then
will be registered as a callback, and if it has been resolved, then
will be executed immediately.
The full code for the above is here: p2.js . Next, let's improve reject
.
failure status
When the Promise resolution fails, the then
method will only execute the callback corresponding to the second parameter onRejected
. First we need another wrapper function:
function wrapToRejected(value) {
return {
then: function (_, onRejected) {
return wrapToThenable(onRejected(value));
}
};
}
The function of this function is that once it happens reject(value)
, we change the value to another thenable
object, which will only call--- when executing then
onRejected
.
Then change the constructor a bit:
function Promise(exector) {
// pending 变为一个二维数组,里面存放的元素是 [onFulfilled, onRejected]。
this.pending = [];
this.value = undefined;
const resolve = value => {
if (this.pending) {
this.value = wrapToThenable(value);
for (const handlers of this.pending) {
this.value.then.apply(this.value, handlers);
}
this.pending = undefined;
}
};
const reject = value => {
resolve(wrapToRejected(value));
};
exector(resolve, reject);
}
Now there is a big change: this.pending
becomes a two-dimensional array. In this way this.value.then.apply
there will be three situations during execution:
- this.value is the
thenable
object converted from a successful resolution, rememberwrapToThenable
?then
will only callonFulfilled
when executed. - this.value is the
thenable
object converted from the failed resolution,then
will only be called when it is executedonRejected
. - this.value is a Promise to which the resolution will be transferred.
The same then
method also needs to be modified:
Promise.prototype.then = function (onFulfilled, onRejected) {
const prev = this;
// 注意这里给了 onFulfilled、onRejected 默认值。
onFulfilled =
onFulfilled ||
function (value) {
return value;
};
onRejected =
onRejected ||
function (value) {
return wrapToRejected(value);
};
const promise = new Promise(resolve => {
const onSpreadFulfilled = function (value) {
resolve(onFulfilled(value));
};
const onSpreadRejected = function (value) {
resolve(onRejected(value));
};
if (prev.pending) {
prev.pending.push([onSpreadFulfilled, onSpreadRejected]);
} else {
prev.value.then(onSpreadFulfilled, onSpreadRejected);
}
});
return promise;
};
// 测试一下。
const p = new Promise((resolve, reject) => {
setTimeout(() => reject(666), 100);
});
p.then(undefined, err => {
console.log('err1: %s', err);
return 1;
}).then(res => {
console.log('res1: %s', res);
});
// 输出:
// err1: 666
// res1: 1
We should pay special attention to the addition of the default values of onFulfilled
and onRejected
. In actual use then
, it may only focus on the callback of success or failure, but we need another state to continue to propagate. It may be a bit difficult to understand here, you can simulate it by substituting data. The full code for the above is here: p3.js .
It's time to think and summarize again, thenable
this interface is the key. By using two wrapper objects to handle the status of success and failure respectively, a unified logic can be maintained when notifying the observer. Does this design feel wonderful?
Next we have to deal with the problem of exceptions when called.
exception handling
Let's first think about where exceptions will occur? The first is in the constructor exector
when it is executed:
function Promise(exector) {
this.pending = [];
this.value = undefined;
const resolve = value => {
// ...
};
const reject = value => {
resolve(wrapToRejected(value));
};
try {
exector(resolve, reject);
} catch (e) {
// 如果有异常产生,状态变为“失败”。
reject(e);
}
}
Then onFulfilled
and onRejected
are executed. When an exception is generated in the above two methods, the state must be changed to fail, and the exception needs to be propagated. then
changes as follows:
Promise.prototype.then = function (onFulfilled, onRejected) {
// ...
// 产生异常的时候包装一下。
const errHandler = returnWhenError(err => wrapToRejected(err));
onFulfilled = errHandler(onFulfilled);
onRejected = errHandler(onRejected);
const promise = new Promise(resolve => {
const onSpreadFulfilled = function (value) {
resolve(onFulfilled(value));
};
const onSpreadRejected = function (value) {
resolve(onRejected(value));
};
if (prev.pending) {
prev.pending.push([onSpreadFulfilled, onSpreadRejected]);
} else {
prev.value.then(onSpreadFulfilled, onSpreadRejected);
}
});
return promise;
};
// 封装为一个可重用的高阶函数。
// 如果 fun 执行失败了,则返回 onError 的结果。
function returnWhenError(onError) {
return fun =>
(...args) => {
let result;
try {
result = fun(...args);
} catch (e) {
result = onError(e);
}
return result;
};
}
Then we can add the catch
method:
Promise.prototype.catch = function (onRejected) {
// 在 then 中忽略掉“成功”状态的回调。
return Promise.prototype.then.call(this, undefined, onRejected);
};
// 测试一下。
const p = new Promise(resolve => {
setTimeout(() => resolve(666), 100);
});
p.then(res => {
console.log('res1: %s', res);
throw new Error('test error1');
}).then(undefined, err => {
console.log('err1: %s', err.message);
throw new Error('test error2');
}).catch(err => {
console.log('err2: %s', err.message);
});
// 输出:
// res1: 666
// err1: test error1
// err2: test error2
The full code for the above is here: p4.js .
At this point, basically the basic functions of Promise are almost completed. However, there are still some imperfections, let's continue to do some optimization.
some optimizations
encapsulate private variables
this.pending
and this.value
are readable and writable from the outside, not secure and robust enough. And I still want to use constructors and prototype methods, and don't want to use closures to encapsulate. I use WeakMap here to achieve the purpose, and the key modifications are as follows:
const refMap = new WeakMap();
// ...
function Promise(exector) {
// 用当前的实例引用作为 key,把想隐藏的数据放进一个对象里。
refMap.set(this, {
pending: [],
value: undefined
});
const resolve = value => {
// 取出封装的数据。
const data = refMap.get(this);
if (data.pending) {
data.value = wrapToThenable(value);
for (const handlers of data.pending) {
data.value.then.apply(data.value, handlers);
}
data.pending = undefined;
}
};
// ...
}
Similarly then
also modify it:
Promise.prototype.then = function (onFulfilled, onRejected) {
// ...
const promise = new Promise(resolve => {
const onSpreadFulfilled = function (value) {
resolve(onFulfilled(value));
};
const onSpreadRejected = function (value) {
resolve(onRejected(value));
};
// 取出封装的数据。
const data = refMap.get(prev);
if (data.pending) {
data.pending.push([onSpreadFulfilled, onSpreadRejected]);
} else {
data.value.then(onSpreadFulfilled, onSpreadRejected);
}
});
return promise;
};
The full code for the above is here: p5.js .
When the Promise instance is garbage collected, the reference to the private data object corresponding to the WeakMap will also be eliminated, and there is no memory leak problem. This solution is very suitable for encapsulating private variables.
call sequence
The current Promise has a call order problem when it is executed, such as:
const p = new Promise(resolve => resolve(1));
p.then(res => {
console.log('res1:', res);
return res + 1;
}).then(res => {
console.log('res2:', res);
});
p.then(res => {
console.log('res3:', res);
});
console.log('Hi!');
// 目前的输出是:
// res1: 1
// res2: 2
// res3: 1
// Hi!
// 正确的输出应该是:
// Hi!
// res1: 1
// res3: 1
// res2: 2
A simple way is to use setTimeout
to improve:
function Promise(exector) {
// ...
const resolve = value => {
const data = refMap.get(this);
if (data.pending) {
data.value = wrapToThenable(value);
for (const handlers of data.pending) {
// 延迟执行。
enqueue(() => {
data.value.then.apply(data.value, handlers);
});
}
data.pending = undefined;
}
};
// ...
}
Promise.prototype.then = function (onFulfilled, onRejected) {
// ...
const promise = new Promise(resolve => {
// ...
if (data.pending) {
data.pending.push([onSpreadFulfilled, onSpreadRejected]);
} else {
// 延迟执行。
enqueue(() => {
data.value.then(onSpreadFulfilled, onSpreadRejected);
});
}
});
return promise;
};
function enqueue(callback) {
setTimeout(callback, 1);
}
The function of enqueue
is to simulate the delayed execution of functions in the order of enqueuing. By deferred execution of all then
calls are guaranteed to be executed in the correct registration order and resolution order, the complete code above is here: p6.js .
what's next?
Ahem, I think it's almost the same when I get here. After all, the purpose of this article is to share and exchange a Promise design idea and experience, not to create a perfect Promise. The result of writing a Promise by hand should not be our purpose, but observing the ideas and solutions in the evolution process is what we need to absorb. When I have time, I will add some missing interfaces, such as Promise.resolve
, Promise.prototype.finally
and so on.
Finally, I hope you can also gain something from this article, welcome to star and follow my JavaScript blog: Whisper Bibi JavaScript
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。