Welcome to the front-end chat here, talk about the front-end
The code is on github
"Handwritten Promise" is a classic question. Basically, everyone can write a promise according to their own understanding. One day, a friend asked me, "How far does a handwritten Promise need to be?", which is also It aroused my interest and thought, "What kind of Promise is perfect?"
Perfect Promise
The first question is what constitutes a perfect Promise. In fact, this problem is not difficult. To achieve a Promsie that is "same" as the native Promise is not perfect. Then the second question comes, the native Promise. What standard is the Promise implemented according to? After consulting the information, I know that it is implemented according to the [Promises/A+] ( https://promisesaplus.com/ ) standard. The specific implementation is on ECMA - sec-promise-objects For the record, now that the standard is in place, we can implement a "perfect Promise"
Promises/A+
Next, let's take a look at Promises/A+
what the standard says, mainly in two parts, one is the noun definition and the other is the standard description. The standard description consists of three parts. Next, we briefly introduce:
Terminology
This part is the definition of nouns, mainly describing the definition of each noun in the standard
-
promise
: 是then
object
function
, 这里需要注意的是不是function
--Yesfunction
then
,Yesfunction
withthen
method -
thenable
: 是定义then
方法的object
函数
,这个和上面---0c313592e580eaf08ab77ce5514587b9promise
then
is a function that does not necessarily need to conform to canonical behavior -
value
: 是任何合法的javascript 值,undefined
、thenable
、promise
,这里的value
thenable
andpromise
, combined with the following specifications, it will be found that it is a nestable relationship -
exception
: is a value thrown by thethrow
keyword -
reason
: Indicates the reason for apromise
status isrejected
Requirements
This part is the definition of the standard, divided into the following three parts
Promise States
A promise
must be in one of the following three states
pending
- Can be converted to
fulfilled
orrejected
status
- Can be converted to
fulfilled
- There needs to be a
value
- There needs to be a
rejected
- There needs to be a
reason
- There needs to be a
When the state is fulfilled
or rejected
, the state can no longer be changed to another state, and value
and reason
can no longer be changed
The then
Method
promise
中then
方法的行为, then
方法是promise
fulfilled
rejected
value
fulfilled
reason
then
with two parameters, ---7e5074e1ffc756e52aa6f21fe76adff
promise.then(onFulfilled,onRejected)
onFulfilled
/onRejected
- Both are optional parameters, if these two parameters are not function types, then ignore
-
promise
046fbec5919c67229ac2cb0e404d0537---状态变成fulfilled
/rejected
,会value
/reason
函数the parameters - will only be called once
- This needs to be done in the
宏任务
or微任务
event loop. Note: The description of execution timing is interesting here, you can see document 2.2.4 - Both functions need to be bound to run on
global this
- 同
then
Promise可以被多次---89d18e0187aff63dcd8582e9279ba0c9---调用,then
中的onFulfilled
onRejected
then
的call sequence call then
After the function call, it needs to return apromise
, which is also the basis ofpromise
can be chained callthen
promise2 = promise1.then(onFulfilled,onRejected)
- If the
onFulfilled
oronRejected
function returns the valuex
, then run the Promise Resolution Procedure -
onFulfilled
onRejected
e
,promise2
rejected
,并且reason
Yese
-
onFulfilled
onRejected
函数,promise1
的fulfilled/rejected
,promise2
- If the
The Promise Resolution Procedure
其实Promise States
The then Method
完了,这部分主要规定了一个抽象的操作promise resolution procedure
, 用来描述当then
的onFulfilled
onRejected
返回值x
时,需要怎么样去进行操作,把表达式记[[Resolve]](promise,x)
, This part is also the most complex part of the entire Promise implementation, let's take a look at what he stipulates
[[Resolve]](promise,x)
当
promise
x
是同一个对象时,promise
7decbbf3adef1f41da9a2e8cf2243461rejected
,reason
是TypeError
const promise = Promise.resolve().then(()=>promise); // TypeError
- If
x
is a Promise, then the state of ---b8c51e8ff6519fa395799022faef6690promise
should be synchronized withx
If
x
is aobject
or afunction
, this part is the most complicated- First of all, store
x.then
in an intermediate variablethen
, why to do this can see document 3.5 , and then process it according to different conditions - 如果获取
x.then
的时候就抛出错误---e2db07b1f63217fc1cfc508e3b9782ffe
,则---076a50e185dafeba03535f63fb211fd8promise
状态变成rejected
,reason
是e
If
then
is a function, then this is what we defined inthenable
, then bindx
for this and callthen
promise
inresolvePromise
andrejectPromise
as two parametersthen.call(x, resolvePromise, rejectPromise)
Next, judge the result of the call
- If
resolvePromise
is called,value
isy
, then call[[Resolve]](promise,y)
-
rejectPromise
9d72b529ea4cf9cbf90c04c1339fb239---被调用,reason
是e
,promise
c44535cd53feb7ccde228f5718885c16---状态变成rejected
,reason
Yese
- If
resolvePromise
andrejectPromise
are called, the first call will prevail and subsequent calls will be ignored If an error is thrown during the call
e
- If
resolvePromise
orrejectPromise
has been called before throwing, then ignore the error - In the latter case, the status of
promise
becomesrejected
,reason
ise
- If
- If
-
then
不是一个函数,那么promise
fulfilled
,value
是x
- First of all, store
-
x
不是object
8d2906acb4f13608ffbfd8d1fd0e241d---或者function
,promise
fulfilled
,value
--Yesvalue
x
The most complicated thing here is that when resolvePromise
is called, value
is y
this part implements the recursive function thenable
The above is the specification of how to implement a "perfect" Promise. In general, the more complicated ones are The Promise Resolution Procedure
and for the case of errors and call boundaries, we will start to implement a "perfect" Promises
How to test your promises
The Promise/A+
specification was introduced earlier, so how to test that your implementation fully implements the specification? Here Promise/A+
provides [promises-tests
]( https://github.com/promises-aplus/promises-tests ), which currently contains 872 test cases to test whether Promises are standard
text begins
First of all, here is the introduction of the implementation of promise according to the completed code. The code is here , the final version is used here, and the comments in it roughly indicate the rule number of the implementation. In fact, it has undergone many modifications as a whole. Portable process, you can commit history , pay attention to the two files promise_2.js
and promise.js
Writing key points
The overall implementation idea is mainly the above specifications. Of course, we do not mean to implement one by one, but to classify the specifications and implement them uniformly:
- Promise state definition and transition rules and basic operation
- implementation of then
- Execution and execution timing of onFulfilled and onRejected
- thenable handling
- Error handling in promise and then and thenable
-
resolve
andreject
the number of function calls
Promise state definition and transition rules and basic operation
const Promise_State = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
class MyPromise {
constructor(executerFn) {
this.state = Promise_State.PENDING;
this.thenSet = [];
try {
executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
} catch (e) {
this._rejectedFn.call(this, e);
}
}
}
In the constructor, the initialization state is pending
, and run the incoming constructor executerFn
, pass in resovlePromise
rejectePromise
two parameters , ---450caddfddf45
Then we will implement resolvePromise
, rejectPromise
these two functions
_resolveFn(result) {
// 2.1.2
if (this._checkStateCanChange()) {
this.state = Promise_State.FULFILLED;
this.result = result;
this._tryRunThen();
}
}
_rejectedFn(rejectedReason) {
//2.1.3
if (this._checkStateCanChange()) {
this.state = Promise_State.REJECTED;
this.rejectedReason = rejectedReason;
this._tryRunThen();
}
}
_checkStateCanChange() {
//2.1.1
return this.state === Promise_State.PENDING;
}
Here is mainly through _checkStateCanChange
to determine whether the executable state, and then change the state, value
, reason
assignment, and then try to run then
the function registered by the method
At this time, our promise can already be called like this
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
implementation of then
Next, we implement the then
function. First, there is a simple question: "When is the then method executed?", someone will answer, it is when the promise state becomes resolve
rejected
, this seems to be fine at first glance, but it is actually wrong. The correct statement should be
『then方法是立即执行的,then方法onFulfilled
、onRejected
参数会在promiseresolve
rejected
post execution
Let's code first
then(onFulfilled, onRejected) {
const nextThen = [];
const nextPromise = new MyPromise((resolve, reject) => {
nextThen[1] = resolve;
nextThen[2] = reject;
});
nextThen[0] = nextPromise;
//2.2.6
this.thenSet.push([onFulfilled, onRejected, nextThen]);
this._runMicroTask(() => this._tryRunThen());
return nextThen[0];
}
The code looks very simple, the main logic is to construct a new promise, and then put onFulfilled
, onRejected
and the newly constructed promise resolve
reject
stored in thenSet
set, and then returned this newly constructed promise, at this time our code can already be called like this
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
p.then((value)=>{
console.log(`resolve p1 ${value}`);
},(reason)=>{
console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));
p.then((value)=>{
console.log(`resolve p2 ${value}`);
},(reason)=>{
console.log(`reject p2 ${reason}`);
});
Execution and execution timing of onFulfilled and onRejected
onFulFilled
onRejected
promise fulfilled
rejected
,结合then
The timing of the call, when judging that the state can be called, need to be done in two places
- When
resolvePromise
,resolvePromise
are called (judging whether there is a call to then registeredonFulfilled
andonRejected
) When the
then
function is called (to determine whether the promise state has becomefulfilled
orrejected
)These two timings will call the following functions
_tryRunThen() { if (this.state !== Promise_State.PENDING) { //2.2.6 while (this.thenSet.length) { const thenFn = this.thenSet.shift(); if (this.state === Promise_State.FULFILLED) { this._runThenFulfilled(thenFn); } else if (this.state === Promise_State.REJECTED) { this._runThenRejected(thenFn); } } } }
Here it will be judged that the function registered by
then
needs to be called, and then the function inthenSet
will be called correspondingly according to the state of the promise
_runThenFulfilled(thenFn) {
const onFulfilledFn = thenFn[0];
const [resolve, reject] = this._runBothOneTimeFunction(
thenFn[2][1],
thenFn[2][2]
);
if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
// 2.2.73
resolve(this.result);
} else {
this._runThenWrap(
onFulfilledFn,
this.result,
thenFn[2][0],
resolve,
reject
);
}
}
_runThenFulfilled
and _runThenRejected
are similar, here is an explanation,
First we judge the legitimacy of onFulfilled
or onRejected
- 如果不合法则不执行,直接将promise 的
value
reason
透传给之前返回给then
promise,then
The state of the promise is the same as the state of the original promise - If legal, execute
onFulfilled
oronRejected
_runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {
this._runMicroTask(() => {
try {
const thenResult = onFn(fnVal);
if (thenResult instanceof MyPromise) {
if (prevPromise === thenResult) {
//2.3.1
reject(new TypeError());
} else {
//2.3.2
thenResult.then(resolve, reject);
}
} else {
// ... thenable handler code
// 2.3.3.4
// 2.3.4
resolve(thenResult);
}
} catch (e) {
reject(e);
}
});
}
Here is a short section of _runThenWrap
, mainly to illustrate the operation of onFulfilled
or onRejected
, which is described in the specification.
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
To put it simply, onFulfilled
and onRejected
can only be executed after there is nothing in the execution context except platform code
. It is often said in 微任务
, 宏任务
So we wrap the _runMicroTask
method here to encapsulate the logic executed in this part
_runMicroTask(fn) {
// 2.2.4
queueMicrotask(fn);
}
Here we use queueMicrotask
as the implementation of micro-tasks, of course, this has compatibility issues, you can see caniuse for details
There are many ways to achieve, such as setTimeout
, setImmediate
, MutationObserver
, process.nextTick
value
reason
onFulfilled
onRejected
,然后获取返回值thenResult
,接下来There will be several branches of judgment
if
thenResult
is a promise- Determine if the promise returned by
then
is the same, if it is thrownTypeError
- 传递
then
返回的promise 的resolve
reject
,thenResult.then
的onFulFilled
onRejected
- Determine if the promise returned by
if
thenResult
is not a promise- To determine whether it is
thenable
, we will explain this part below - If none of the above judgments are true, then use
thenResult
as a parameter and callresolvePromise
- To determine whether it is
thenable handling
thenable
should be said to be the most complicated part of the implementation. First of all, we have to judge the upper part according to the definition thenResult
is thenable
if (
typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function"
) {
//2.3.3.1
const thenFunction = thenResult.then;
if (typeOf(thenFunction) === "Function") {
// is thenable
}
}
Object
d14abe064d49d8764b17175fbcdfbdc9---或者Function
,然后thenResult.then
是不是个Function
,那么有人会问, Can it be written like this
if (
(typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
) {
// is thenable
}
At the beginning, I also wrote it like this, and then I found that the test case could not run, and finally I read the specification, there is such a paragraph 3.5
Simply put, in order to ensure the consistency of testing and calling, it is necessary to store thenResult.then
before judging and running it. Multiple accesses to attributes may return different values.
Next is the processing logic of ---9b9df011938e1626dbaa289c99419075 thenable
In short, the processing logic of thenable
has two cases
- In the case of
then
orresolve
in promises that handlethenable
- In the
thenable
ofthen
callback ofvalue
orthenable
This is described here with the thenable
call in promise then
:
_thenableResolve(result, resolve, reject) {
try {
if (result instanceof MyPromise) {
// 2.3.2
result.then(resolve, reject);
return true;
}
if (typeOf(result) === "Object" || typeOf(result) === "Function") {
const thenFn = result.then;
if (typeOf(thenFn) === "Function") {
// 2.3.3.3
thenFn(resolve, reject);
return true;
}
}
} catch (e) {
//2.3.3.3.4
reject(e);
return true;
}
}
const [resolvePromise, rejectPromise] =
this._runBothOneTimeFunction(
(result) => {
if (!this._thenableResolve(result, resolve, reject)) {
resolve(result);
}
},
(errorReason) => {
reject(errorReason);
}
);
try {
thenFunction.call(thenResult, resolvePromise, rejectPromise);
} catch (e) {
//2.3.3.2
rejectPromise(e);
}
Here we construct resolvePromise
and rejectPromise
, and then call thenFunction
, which will be called after processing in the function logic resolvePromise
rejectPromise
f216e7 rejectPromise
, result
thenable
,那么就会继续传递下去, thenable
,调用resolve
reject
What we should pay attention to is that the then
method of promise is different from the --- ---dccf78fd7533655f6529a7b8afa62fb2--- method of thenable
then
- The promise
then
has two parameters, one isfulfilled
, and the other isrejected
, the corresponding function will be called back after the previous promise state changes thenable
then
also has two parameters, these two parameters are provided tothenable
call completion for callbackresolve
reject
call backreject
方法,thenable
thenable
,那么会按照这个逻辑调用下去,直到是一个非thenable
,就会调用Fromthenable
back to the nearest promiseresolve
orreject
At this point, our promise can already support the operation of
thenable
new MyPromise((resolve)=>{ resolve({ then:(onFulfilled,onRejected)=>{ console.log('do something'); onFulfilled('hello'); } }) }).then((result)=>{ return { then:(onFulfilled,onRejected)=>{ onRejected('world'); } } });
Error handling in promise and then and thenable
Error handling refers to the error that occurs during the running process to be captured and processed, basically using try/catch
call after the error is caught reject
callback, this part is relatively simple, you can see it directly code
The number of calls of resolve and reject functions
The resolve
and reject
calls in a promise can be said to be mutually exclusive and unique, that is, only one of these two functions can be called, and it is called once, which is relatively simple to say , but with error scenarios there is a certain level of complexity that could have been
if(something true){
resolve();
}else {
reject();
}
After adding the error scene
try{
if(something true){
resolve();
throw "some error";
}else {
reject();
}
}catch(e){
reject(e);
}
At this time, the judgment will be invalid, so we can achieve the goal by wrapping two mutually exclusive functions through a tool class.
_runBothOneTimeFunction(resolveFn, rejectFn) {
let isRun = false;
function getMutuallyExclusiveFn(fn) {
return function (val) {
if (!isRun) {
isRun = true;
fn(val);
}
};
}
return [
getMutuallyExclusiveFn(resolveFn),
getMutuallyExclusiveFn(rejectFn),
];
}
At this point, we have a Promise that fully complies with the Promise/A+
standard, and it is completed. The complete code is here
Wait, is there something missing?
Some people will see this and say, is this the end?
我catch
、 finally
,还有静态方法Promise.resolve
、 Promise.reject
、 Promise.all/race/any/allSettled
方法呢?
In fact, from the standard, the standard of Promise/A+
is the part described above, only defines the then
method, and the other methods we use every day are actually in the then
The method is derived from the above, such as catch
method
MyPromise.prototype.catch = function (catchFn) {
return this.then(null, catchFn);
};
The specific method is actually implemented, you can see promise_api for details.
at last
Finally, I want to share the process of writing this promise. From the above description, it seems to be very smooth, but in fact, when writing, I basically simply passed the following standards, and then combined it according to my own understanding promises-tests
Written by unit test cases, this development mode is actually TDD (Test-driven development) , this development mode will greatly reduce the mental burden of developers who do not cover boundary scenarios when programming, But on the other hand, the portable quality requirements for test cases are very high. Overall, this portable promise is a more interesting process. If there is any problem with the above, please leave a message for more exchanges.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。