我们需要了解Promise


2019-11-12 前端基础

定义

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

说在前面

这是一个简版非规范的Promise实现,其中前置知识点比较重要,理解了前置知识点的内容,也就了解了Promise最基本的原理。

前置知识

这小节的核心是:把函数名当变量用。

以下三点理解之后,就能够实现简单的Promise了。

类型为Function的变量,可以像函数一样调用。

举个栗子

let myFn = function () {};
myFn(); 
1
2

和下面的代码效果是一样的

function myFn() {
    // code here...
}
myFn();
1
2
3
4

函数数组,即函数里的每一个元素都是函数,可以用遍历调用。

let fnArray = [];
let fn1 = () => { console.log(1) };
let fn2 = () => { console.log(2) };
fnArray.push(fn1,fn2);
fnArray.forEach(cb => cb());
1
2
3
4
5

运行结果是

1
2

函数可以当作参数传递

function showYouFn(fn1, fn2) {
    // 调用
    fn1();
    fn2();
}

let myFn1 = () => { console.log(3); };
let myFn2 = () => { console.log(4); };

showYouFn(myFn1, myFn2);
1
2
3
4
5
6
7
8
9
10

回想一下:把函数名当变量用。

实现简单模型

下面是一个简陋得不像Promise的Promise实现

function MyPromise(fn) {
    function resolve(value) {
        console.log(value);
    }
    function reject(value) {
        console.log(value);
    }
    
    fn(resolve, reject);
}
1
2
3
4
5
6
7
8
9
10

现在你就可以用上自定义的Promise了

new MyPromise((resolve, reject) => {
    setTimeout(()=>{
        resolve('你将看到两秒后的我');
    }, 2000);
});

1
2
3
4
5
6

将会在2秒后输出

你将看到两秒后的我
1

解释一下整体代码:

MyPromise 中的参数 fn是需要用户传入的自定义函数(该函数需要接收两个参数)。

MyPromise 中的内部还有两个函数resolvereject,其中 resolve 表示用户的异步任务成功时应该调用的函数,reject 表示用户的异步任务失败时该调用的函数。

那用户如何调用 resolvereject 呢?

很简单,把两个函数当作 fn的参数传递出去即可。

所以 MyPromise 内部在调用 fn 时会把 resolvereject当作参数传递给 fn。

然后用户在自定义函数内调用 resolvereject 来通知 MyPromise 异步任务已经执行完了。

通过上面的代码可以发现一个问题,我们不知道Promise的异步任务进行到哪一步了、是成功还是失败了。

所以增加三个状态用来标识一个Promise的异步任务进行到何种程度了。

pending、resolved、rejected 分别表示 执行中、已完成、已失败

然后通过观察用户调用的是 resolve 还是 reject 可以判断当前Promise的状态。 那么会有三种情况:

  • 在用户调用 resolve 或 reject 之前状态是 pending
  • 用户调用 resolve 时,状态将变为 resolved
  • 用户调用 reject 时,状态将变为 rejected

下面进行代码的改造,定义了三个常量表示状态以及一个变量 state 用来存储当前状态。 并且当 resolve 被调用时将 state 修改为 resolved 。

    const PEDNING="pending";//执行状态
    const RESOLVED='resolved';//以完成;
    const REJECTED='rejected';//以失败

    function MyPromise(fn){
        const that=this
        //初始状态为执行中,pending
        this.state=PEDNING;

        function resolve(value){
            console.log(value)
            that.state=RESOLVED;
        }
        function reject(err){
            console.log(err)
            that.state=REJECTED;
        }
        fn(resolve,reject);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

OK,现在已经能知道Promise当前所处的状态了,但是任务完了得拿到结果吧,MyPromiseresolve 被调用,那也只是MyPromise知道任务完成了,用户还不知道呢。 所以我们需要回调函数告诉用户,是的,其实就是回调函数。 这时候就轮到 then 方法出场了,用户通过then方法传入回调函数, MyPromise 将在成功调用 resolve 时调用用户传入的回调函数。 开始改造代码,MyPromise 内部需要变量存储回调函数,then 方法的作用就是将用户传入的回调函数赋予 MyPromise 内的变量。 所以 then 方法长这样,接收两个参数,一个是成功时的回调函数,一个是失败时的回调函数

        const PEDNING="pending";//执行状态
        const RESOLVED='resolved';//以完成;
        const REJECTED='rejected';//以失败

        function MyPromise(fn){
            const that=this
            //初始状态为执行中,pending
            this.state=PEDNING;
            //两个储存回调函数的变量
            this.resolvedCallback;
            this.rejectedCallback;

            function resolve(value){
                that.state=RESOLVED;
                that.resolvedCallback && that.resolvedCallback(value); 
            }
            function reject(err){
                that.state=REJECTED;
                that.rejectedCallback && that.rejectedCallback(err);
            }
            fn(resolve,reject);
        }

        MyPromise.prototype.then=function(onFulfilled,onRejected){
            this.resolvedCallback = onFulfilled;
            this.rejectedCallback = onRejected;
        }
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

是的,一个简版Promise几乎大功告成,让我们再试试在浏览器执行如下代码(注意我删除了 resolve 和 reject 里的console语句);咱们来使用一下:

    (function(){
        
        const PEDNING="pending";//执行状态
        const RESOLVED='resolved';//以完成;
        const REJECTED='rejected';//以失败

        function MyPromise(fn){
            const that=this
            //初始状态为执行中,pending
            this.state=PEDNING;
            //两个储存回调函数的变量
            this.resolvedCallback;
            this.rejectedCallback;

            function resolve(value){
                that.state=RESOLVED;
                that.resolvedCallback && that.resolvedCallback(value); 
            }
            function reject(err){
                that.state=REJECTED;
                that.rejectedCallback && that.rejectedCallback(err);
            }
            fn(resolve,reject);
        }

        MyPromise.prototype.then=function(onFulfilled,onRejected){
            this.resolvedCallback = onFulfilled;
            this.rejectedCallback = onRejected;
        }

        new MyPromise((resolve,reject)=>{
            setTimeout(()=>{
                resolve('我是结果')
            },4000);
        }).then((value)=>{
            console.log(value)
        })
    })()
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

通过匿名函数和函数自执行,形成局部作用域,保护里面的变量;

上面的代码,用法上已经和Promise长得差不多了,但是如果我们多次调用 then 方法呢? 是的,只有最后一个 then 方法里的回调函数能执行,这当然没法满足我们的需要。 于是,将两个回调函数改成函数数组(请回想一下前置知识),并在状态更改时遍历调用回调函数。 改造后的代码如下:

    (function(){

    const PEDNING="pending";//执行状态
        const RESOLVED='resolved';//以完成;
        const REJECTED='rejected';//以失败

        function MyPromise(fn){
            const that=this
            //初始状态为执行中,pending
            this.state=PEDNING;
            //两个储存回调函数的变量,注意,变成了数组
            this.resolvedCallbackList=[];
            this.rejectedCallbackList=[];

            function resolve(value){
                that.state=RESOLVED;
                that.resolvedCallbackList && that.resolvedCallbackList.forEach(cbFn =>cbFn(value)); 
            }
            function reject(err){
                that.state=REJECTED;
                that.rejectedCallbackList && that.rejectedCallbackList.forEach(cbFn =>cbFn(err)); 
            }
            fn(resolve,reject);
        }

        MyPromise.prototype.then=function(onFulfilled,onRejected){
            this.resolvedCallbackList.push(onFulfilled);
            this.rejectedCallbackList.push(onRejected);
             // 这里是为了链式调用,所以要返回this,即myPromise.then().then().then()...
            return this
        }

        new MyPromise((resolve,reject)=>{
            setTimeout(()=>{
                resolve('经过5秒出现')
            },5000)
        }).then((val)=>{
            console.log(val+'第一次出现的value')
        }).then((val)=>{
            console.log(val+'第二次出现的value')
        })
    })()
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

上面已经是简版Promise的实现了。 但是我们还可以更完善一点,增强 MyPromise 的健壮性。 例如,若用户自定义函数在执行过程中发生了错误,会中断程序的执行,于是我们增加try...catch...语句,并在发生错误时主动执行reject函数告知用户。

try {
    fn(resolve, reject);
} catch (e) {
    reject(e);
}
1
2
3
4
5

又或者,对参数进行校验,状态进行判断等,以 then为例,若用户传入的参数不是函数呢? 或者Promise的状态已经时rejected或resolved,此时调用then呢?

改造 then 后代码如下:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if(typeof onRejected !== 'function') {
        onRejected = v => v;
    }
    if(typeof onFulfilled !== 'function') {
        onFulfilled = v => { throw r };
    }
    const that = this;
    if (that.state === PENDING) {
        that.resolvedCallbacks.push(onFulfilled)
        that.rejectedCallbacks.push(onRejected)
    }
    if (that.state === RESOLVED) {
        onFulfilled(that.value)
    }
    if (that.state === REJECTED) {
        onRejected(that.value)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

致谢旺仔牛奶真甜

Thomas: 11/12/2019, 5:16:10 PM