0%

动手实现Promise

前言

之前已经学习过了Promise的相关知识,Promise其实就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。一般的Promise长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
if(/* 一般是异步操作 */) {
resolve('回调');
} else {
reject('error');
}
});

promise.then((data) => {
console.log(data);
}).catch((error) => {
console.log(error);
});

下面尝试一下自己去实现Promise的功能。

基本功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定一个构造函数
function MyPromise(callBack: Function) {
let thenFunc: ?Function = null;

this.then = (call) => {
thenFunc = call;
};

const thenCall = (value) => {
thenFunc && thenFunc(value);
thenFunc = null;
};

callBack(thenCall);
}

// 使用
const promise = new MyPromise((resolve) => {
setTimeout(() => {
resolve('ssss');
}, 1000);
});

promise.then((data) => {
console.log(data);
});

定义一个构造函数,将初始化的异步方法传入其中。在函数内部定一个变量来存储promise实例传递进来的then方法,当异步方法开始执行的时候就会在函数内部调用这个变量存储的函数,从将参数回调至promise实例中。

初始化的异步方法中,resolve参数其实是一个函数,对应到自定义的函数中就是thenCall方法,然后当resolve调用的时候,就是函数内部的用来存储then的变量执行的时候。

初始化的方法不是异步的

如果在初始化的时候,传入的不是一个异步的函数,比如下面的

1
2
3
const promise = new MyPromise((resolve) => {
resolve('ssss');
});

由于执行顺序的问题,并不会回调到then方法中。可以想办法让then方法先执行,之后再执行resolve,利用Js的循环机制以及setTimeout,将resolve放入栈底执行

1
2
3
4
5
6
const thenCall = (value) => {
setTimeout(() => {
thenFunc && thenFunc(value);
thenFunc = null;
}, 0);
};

如果对于Js的循环机制和setTimeout不是很了解的话可以参考这篇

这样就可以确保在有then的情况下,resolve在后面执行。

promise调用时间很晚

如果初始化Promise实例之后,并没有立即使用它,而是间隔了一段时间再去调用promise的then方法,那么由于resolve已经执行完毕,所以即使是调用过了then方法也不会正确的回调。

1
2
3
4
5
6
7
setTimeout(() => {
promise.then((data) => {
console.log(data);
}).catch((err) => {
console.log('error' + err);
});
}, 1000);

之前就已经介绍过了,Promise是有三种状态的:pendingfulfilledrejected。而且只能从pending转换为另外两种状态,而且是不可逆转的。可以借用Promise的这三种状态,让自己的Promise记住自己当前的状态。

添加一个记录Promise状态的变量,添加一个存储参数的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function MyPromise(callBack: Function) {
let thenFunc: ?Function = null;
let status: string = 'pending';
let callValue: any = null;

this.then = (call) => {
if (status === 'pending') {
thenFunc = call;
}
call(callValue);
};

const thenCall = (value) => {
setTimeout(() => {
thenFunc && thenFunc(value);
callValue = value;
status = 'fulfilled';
thenFunc = null;
}, 0);
};

callBack(thenCall);
}

在Promise初始化的时候,Promise的初始状态为pending,执行过thenCall方法后,改变自身状态为fulfilled,并且将传递的参数暂时保存到callValue中。过一段时间后,then执行的时候,由于状态已经改变,则会直接执行then中传递的回调函数,并将callValue作为回调参数。

添加reject状态

上述的方法都是只有resolve一个状态,其实reject与resolve相同,仅仅是执行了不同的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function MyPromise(callBack: Function) {
let thenFunc: ?Function = null;
let catchFunc: ?Function = null;
let status: string = 'pending';
let callValue: any = null;

this.then = (call) => {
if (status === 'pending') {
thenFunc = call;
} else if (status === 'fulfilled') {
call(callValue);
}
return this;
};

this.catch = (call) => {
if (status === 'pending') {
catchFunc = call;
} else if (status === 'rejected') {
call(callValue);
}
return this;
};

const thenCall = (value) => {
setTimeout(() => {
thenFunc && thenFunc(value);
callValue = value;
status = 'fulfilled';
thenFunc = null;
}, 0);
};

const catchCall = (value) => {
setTimeout(() => {
catchFunc && catchFunc(value);
callValue = value;
status = 'rejected';
catchFunc = null;
}, 0);
};

callBack(thenCall, catchCall);
}


const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
reject('ssss');
}, 1000);
});

promise.then((data) => {
console.log(data);
}).catch((err) => {
console.log('error' + err);
});

关于return this,其实是将对象本身返回以供后续的catch或者其他操作,如果没有那么会报错说没有then方法或者没有catch方法。

链式调用

自身调用

Promise可以通过不停地返回Promise对象从而实现链式调用

1
promise.then().then().then().....

由于MyPromise内部是由一个变量来保存的回调函数,只能保存最新的then方法,要是想实现自身的链式调用,那么就需要将其修改为数组,把每一个then传入的回调函数保存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function MyPromise(callBack: Function) {
let thenFunc: Array<Function> = [];
let catchFunc: ?Function = null;
let status: string = 'pending';
let callValue: any = null;

this.then = (call) => {
if (status === 'pending') {
thenFunc.push(call);
} else if (status === 'fulfilled') {
call(callValue);
}
return this;
};

const thenCall = (value) => {
setTimeout(() => {
callValue = value;
status = 'fulfilled';
thenFunc.forEach((fun) => {
fun && fun(value);
});
}, 0);
};

....
....

callBack(thenCall, catchCall);
}

串行promise

更加常见的需求是不同的Promise类型需要在同一个方法中链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('ssss');
}, 100);
});

promise.then((data) => {
console.log('11' + data);
return new MyPromise((resolve) => {
setTimeout(() => {
resolve('dddd');
}, 100);
});
}).then((data) => {
console.log('22' + data);
}).catch((err) => {
console.log('error' + err);
});

// 控制台输入的结果为
// 11ssss
// 22ssss
// 并不是预期的
// 11ssss
// 22dddd

那么如何让后续的then方法,返回的是最新初始化的promise呢?首先考虑的位置就是MyPromise中的then方法。因为返回了一个新的promise,而不是像之前的返回this,所以就没有办法处理新的promise里面的内容。那么就应该区分出来,then方法里是否返回了一个新的promise。

1
2
3
4
5
6
7
8
this.then = (call) => {
const ret = typeof call === 'function' && call(callValue);
if (ret && ret.then && typeof ret.then === 'function') {
// 返回了一个新的promise
} else {
// 正常处理
}
};

在then方法中,首先让传入的回调方法执行,观察它的返回值是否是一个对象并且里面包含着then方法。如果有的话,该回调函数就返回了一个新的pormise。

上述的方法确实可以判断出来是否传入了新的promise,但是要明确的一点是,then方法中并不是回调函数执行的时候,回调函数是否执行是由thenCall控制。以上的逻辑判断,需要传入thenFunc数组中存储起来,在thenCall中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const handlePromise = (call: Function, value) => {
const ret = typeof call === 'function' && call(value);
if (ret && ret.then && typeof ret.then === 'function') {
ret.then((data) => {
call(data);
})
}
};

this.then = (call) => {
if (status === 'pending') {
thenFunc.push((value) => { handlePromise(call, value); });
} else if (status === 'fulfilled') {
call(callValue);
}
return this;
};
`将handlePromise传入thenFunc数组中,当其执行的时候,会判断then传入回调函数的返回值。如果是一个promise对象,那么就执行该promise的then函数`
// 控制台输出结果为
// 11ssss
// 22ssss
// 11dddd

控制台输出并不是预期的结果,但是好消息是出现第二个promise的数据。那么问题在哪里呢,为什么会输出三个结果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 当第一个then执行的时候,执行到
const ret = typeof call === 'function' && call(value);
// 因为调用了call(value);
// 在控制台打印了11ssss
// 之后判断出返回了一个promise对象,执行到
ret.then((data) => {
call(data);
})
// 这里会执行第二个promise的异步函数
setTimeout(() => {
resolve('dddd');
}, 100);
// 即在100ms后,回调dddd,注意这里面仍然是在第一个then中回调!
// 执行到这里的时候控制台并没有输出11dddd,而是先输出了22ssss
// 因为是一个100ms延迟的异步函数,在调用的时候会直接执行第二个then
// 此时MyPromise中的value值仍然是ssss,所以会直接输出22ssss
// 100ms后输出11dddd

到了第二次then方法的时候,仍然是第一个值,原因就是return this。仍然返回了本身的promise,而不是第二个promise中的内容。那么可以考虑在then中每次都返回一个新的promise,用来接收新的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const handlePromise = (call: Function, resolve: Function) => {
const ret = typeof call === 'function' && call(callValue);
if (ret && ret.then && typeof ret.then === 'function') {
ret.then((value) => {
resolve(value);
});
} else {
resolve(ret);
}
};

this.then = (call) => {
return new MyPromise((resolve) => {
if (status === 'pending') {
thenFunc.push(() => { handlePromise(call, resolve); });
} else if (status === 'fulfilled')
call(callValue);
}
});
};

由于在then中构建了一个新的promise,在每次then中都会调用resolve来改变MyPromise中value的值,确保在下一次的then中获取到新的值。

以上即可在promise中实现链式调用。

相关API

promise.race()

Promise的race方法其实是将几个promise一起执行,首先回调的promise会做为race方法的回调值。

1
2
3
4
5
6
7
8
9
10
11
12
13
MyPromise.race = (raceList: Array<MyPromise>) => {
return new MyPromise((resolve) => {
let count = 0;
for (let i = 0; i < raceList.length; i++) {
raceList[i].then((value) => {
if (count === 0) {
count++;
resolve(value);
}
});
}
});
};

promise.all()

Promise的all方法是将几个promise一起执行,当每个promise返回成功的时候,才会将所有的结果组合成一个数组返回到结果中。

1
2
3
4
5
6
7
8
9
10
11
12
13
MyPromise.all = (allList: Array<MyPromise>) => {
return new MyPromise((resolve) => {
let dataList = [];
for (let i = 0; i < allList.length; i++) {
allList[i].then((value) => {
dataList.push(value);
if (dataList.length == allList.length) {
resolve(dataList);
}
});
}
});
};

小结

以上是全部关于自己实现promise的内容,仅仅实现了promise的一部分内容。实现起来还是有一点吃力的,尤其是关于链式调用的实现,更加能理解关于函数编程的思想,总体来说实现的比较乱。网上其他大神都是用es5写的,和es6写法还是有一点出入的。使用es6实现还是比较简洁的,如果有任何问题请不吝赐教。

参考链接