深入理解 Promise(上)

发布于 2022-05-06 14:33:13 字数 16255 浏览 1049 评论 0

为了解决回调函数带来的问题,Promise 作为一种更优雅的异步解决方案被提出,最初只是一种实现接口规范,而到了 es6,则是在语言层面就原生支持了 Promise 对象。

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

Promise 也有一些缺点。

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
  • 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

如果某些事件不断地反复发生,一般来说,使用 stream 模式是比部署 Promise 更好的选择。

一、基本用法

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。

这两个函数都接受Promise对象传出的值作为参数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

二、相关 API

1. Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获

p.then((val) => console.log("fulfilled:", val))
  .catch((err) => console.log("rejected:", err));

// 等同于

p.then((val) => console.log("fulfilled:", val))
  .then(null, (err) => console.log("rejected:", err));

一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

2. Promise.all()

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON("/post/" + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

上面代码中,promises是包含6个Promise实例的数组,只有这6个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

3. Promise.race()

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.race([p1,p2,p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将Promise的状态变为reject,否则变为resolve。

相当于,多个promise值进行比赛,看谁的状态最先改变,就将谁的值传递给回调函数。

下面基于promise.race([p1,p2,p3])实现超时处理

function delayPromise(wait) {
    return new Promise((resolve) => {
        setTimeout(resolve, wait);
    });
}

function timeoutPromise(promise, wait) {
    let timeout = delayPromise(wait).then(() => {
        throw new Errow(`超过${wait}ms未完成`);
    });
    return Promise.race([promise, timeout]);
}
// 测试
var promise = new Promise((resolve, reject) => {
    let random = Math.random()*1000
    //setTimeout(resolve(random),random)  ???????立即执行了
    setTimeout(() => {
        resolve(random)
    }, random)
})

timeoutPromise(promise, 300)
.then((value)=> {
    console.log(`规定${value}ms时间内完成`)
})
.catch((err) => {
    console.log(`发生超时:${err}`)
})

4. Promise.resolve()

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将jQuery生成的deferred对象,转为一个新的Promise对象。

Promise.resolve等价于下面的写法。

Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));

Promise.resolve方法的参数分成四种情况。

  1. 参数是一个Promise实例

如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  1. 参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。

上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出42。

  1. 参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。

var p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。

  1. 不带有任何参数

Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。

所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。

var p = Promise.resolve();

p.then(function () {
  // ...
});

上面代码的变量p就是一个Promise对象。

5. Promise.reject()

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。

var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s){
  console.log(s)
});
// 出错了

上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。

参考文章

es6学习笔记5--promise

通过深入理解 Promise 内部原理,下面动手实现一个 Promise() 方法

function myPromise(fn) {
    this.value = null;
    this.status = 'pending';
    this.resolveFun = function(){}; // 成功回调的方法就得变成数组才能存储 最好写成 this.resolveFun = []
    this.rejectedFun = function(){};
    fn(this.resolve.bind(this));
}
myPromise.prototype.resolve = function(val) {
    //let _this = this;
    if(this.status == 'pending') {
        this.status = 'fullfilled';
        this.value = val;
        setTimeout(() => {
            this.resolveFun(this.value);
        }, 0);
    }
};
// .then() 每次返回一个新的 Promise 对象
myPromise.prototype.then = function(fullfilled) {
    fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value}; // 实现穿透
    //let _this = this;
    return new myPromise((resolve, reject) => {
        this.resolveFun = () => {
            let res = fullfilled(this.value);
            if(res && typeof res.then == 'function') {
                res.then(resolve, reject);
            } else {
                resolve(res);
            }
        }
    });
};

完整版:

class myPromise {
    constructor(fn) {
        this.value = null;
        this.status = 'pending';
        this.resolveFunc = function(){}; // 异步操作成功后的回调
        this.rejectedFunc = function(){};
        fn(this.resolve.bind(this), this.reject.bind(this)); // fn() 中执行异步操作
    }
    /*
        调用 then() 注册回调函数, 类似观察者模式
    */
    then(onFulfilled, onRejected) {
        fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value};
        onRejected = typeof onRejected === 'function' ? onRejected : (value) => {return value};
        // this.resolveFunc = onFulfilled;
        // this.rejectedFunc = onRejected;
        return new myPromise((resolve, reject) => {
            // if(this.status === 'pending') { // 不能加这个判断 无论什么时候都会执行 下面的 this.resolveFunc 不起作用
            //     this.resolveFunc = onFulfilled;
            //     this.rejectedFunc = onRejected;
            //     return;
            // }
            // 如果 then() 中没有传递任何参数
            if(!onFulfilled && !onRejected) {
                resolve(this.value); // 要把value resolve 出去,因为可能后续还有.then()操作,不能断链,能继续形成链式调用
            }
            this.resolveFunc = () => { // 改变外部 Promise 的回调
                let res = onFulfilled(this.value); // 执行传进 .then() 的参数,判断是否返回一个Promise对象
                if(res && typeof res.then === 'function') {
                    res.then(resolve, reject); // ???
                } else {
                    resolve(res);
                }
            };
            this.rejectedFunc = () => {
                let res = onRejected(this.value);
                if(res && typeof res.then === 'function') {
                    res.then(resolve, reject);
                } else {
                    reject(res);
                }
                
            }
        });
    }
    /*
        val 代表一步操作返回的结果
        当异步操作执行成功后,使用 resolve() ,开始一一执行 resolveFunc 中的回调
        并通过 setTimeout 机制,保证在执行 resolve 之前,then 方法已经完成回调函数的注册
    */
    resolve(val) {
        if(this.status === 'pending') {
            this.status = 'fullfilled';
            this.value = val;
            setTimeout(() => {
                this.resolveFunc(this.value);
            }, 0);
        }
    }
    reject(val) {
        if(this.status === 'pending') {
            this.status = 'rejected';
            this.value = val;
            setTimeout(() => {
                this.rejectedFunc(this.value);
            }, 0);
        }
    }
}

测试:

  1. 回到过去: resolve, reject和then
// 测试
var fn=function(resolve, reject){
    console.log('begin to execute!');
    var number=Math.random();
    if(number<=0.5){
        resolve('less than 0.5');
    }else{
        reject('greater than 0.5');
    }
}

var p = new myPromise(fn);
p.then(function(data) {
    console.log('resolve: ', data);
}, function(data) {
    console.log('reject: ', data);
});
  1. 加入状态: pending, fullfilled, rejected
// 测试
var fn=function(resolve, reject){
    resolve('hello');
    reject('hello again');
}

var p1=new Promise(fn);
p1.then(function(data){
    console.log('resolve: ',data)
}, function(data){
    console.log('reject: ',data)
});
//'resolve: hello'

var p2=new myPromise(fn);
p2.then(function(data){
    console.log('resolve: ',data)
}, function(data){
    console.log('reject: ',data)
});

p1是原生Promise,p2是我们自己写的,可以看出,当调用resolve之后再调用reject,p1只会执行resolve,如果不加入状态我们的则是两个都执行。

事实上在Promise规范当中,规定Promise只能从初始pending状态变到resolved或者rejected状态,是单向变化的,也就是说_执行了resolve就不会再执行reject_,反之亦然。

为此,我们需要在MyPromise中加入状态,并在必要的地方进行判断,防止重复执行:

  1. 链式调用

在Promise的使用中,我们一定注意到,是可以链式调用的:

要实现链式调用,then方法的返回值也必须是一个Promise对象,这样才能再次在后面调用then。

观察Promise规范,会发现链式调用的情况也分两种。一种情况下,前一个Promise的resolve或者reject的返回值是普通的对象;但还有一种情况,就是前一个Promise的resolve或者reject执行后,返回的值本身又是一个Promise对象。

因此我们需要对回调函数的执行结果做一次判断,看其是否是一个 Promise 对象。通过调用 .then 看它有没有 then() 方法来判断。 typeof res.then === 'function'

var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
});
//'hello'
//'hello again'
var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
  return new Promise(function(resolve){
    var innerData='hello third time!';
    resolve(innerData);
  })
}).then(function(data){
  console.log(data);
});

这里一定要注意的点是:promise里面的 then 函数仅仅是注册了后续需要执行的代码(只是注册了异步操作结束后的回调函数),回调函数的真正的执行是在 resolve 方法里面执行的。

  • 链式调用为什么要返回新的 promise

如我们理解,为保证 then 函数链式调用,then 需要返回 promise 实例。但为什么返回新的 promise,而不直接返回 this 当前对象呢?看下面示例代码:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)
})

假如 then 函数执行返回 this 调用对象本身,那么 promise2 === promise1,promise2 状态也应该等于 promise1 同为 resolved。而 onResolved 回调中返回状态为 rejected 对象。考虑到 Promise 状态一旦 resolved 或 rejected就不能再迁移,所以这里 promise2 也没办法转为回调函数返回的 rejected 状态,产生矛盾。

  1. Promise值的穿透
new Promise(resolve=>resolve(8))
  .then()
  .catch()
  .then(function(value) {
    alert(value)
  })

即相当于:

new Promise(resolve=>resolve(8))
  .then(function(value){
    return value
  })
  .catch(function(reason){
    throw reason
  })
  .then(function(value) {
    alert(value)
  })

所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为function(value) {return value},function(reason) {throw reason}。
所以我们只需要把then里判断onResolved和onRejected的部分改成如下即可:
fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value}; onRejected = typeof onRejected === 'function' ? onRejected : (value) => {throw value};

一旦promise resolve一个值,他就应该一直保留那个值,并且永远也不会再resolve

最后,看一道2019年阿里校招的一道在线编程题

const timeout = ms => new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve();
 }, ms);
});

const ajax1 = () => timeout(2000).then(() => {
 console.log('1');
 return 1;
});

const ajax2 = () => timeout(1000).then(() => {
 console.log('2');
 return 2;
});
const ajax3 = () => timeout(2000).then(() => {
 console.log('3');
 return 3;
});

const mergePromise = ajaxArray => {
	// your code
};

console.time('sequence')
mergePromise([ajax1, ajax2, ajax3]).then(data => {
    console.log('done');
    console.log(data); // data 为 [1, 2, 3]
    console.timeEnd('sequence')
});
// 分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

答案:

const mergePromise = ajaxArray => {
    let result=[];
    return ajaxArray.reduce((res, cur) => {
        return res
            .then(cur)
            .then(item => {
                result.push(item);
                return result;
            })
    }, Promise.resolve())
};

看一下,使用 Promise.all()的结果: 虽然结果是顺序输出,但是执行顺序不是顺序执行的

console.time('serial');
Promise.all([ajax1(), ajax2(), ajax3()]).then(data=>{
  console.log('done');
  console.log(data);
  console.timeEnd('serial')
})
// 分别输出
// 2
// 1
// 3
// done
// [1, 2, 3]

References

30分钟,让你彻底明白Promise原理
彻底理解Promise对象——用es5语法实现一个自己的Promise(上篇)
解读Promise内部实现原理
剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84961 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文