Promise学习笔记(一)

190 阅读5分钟

基本概念

抽象上看,Promise是JavaScript中进行异步编程的解决方案。

具体来看,Promise是一个对象,它通常用于描述 现在开始执行,一段时间后才能获得结果的异步行为,内部保存了该异步行为的结果。

Promise对象有且仅有以下3种状态:

  • pending:待定(进行中)
  • fulfilled:成功
  • rejected:失败

一个Promise的状态转换仅有以下2种,Promise一旦决议,就会一直保持其决议结果(fulfilled或rejected)不变

  • pending 到 fulfilled
  • pending 到 rejected

基础用法

Promise对象构造器接收一个executor执行器

let p = new Promise((resolve,reject) => {
	//这个代码块是executor
})

执行器通常承担2个任务:

  • 初始化一个异步行为
  • 控制状态的最终转换

执行器接收两个函数作为参数,其中:

  • resolve:用于将状态 pending 转换成 fulfilled
  • reject:用于将状态 pending 转换成 rejected
let p = new Promise((resolve,reject) => {
	setTimeout(() => {
		resolve()
	},1000)
})

在setTimeout真正被执行的1000ms后,对象p的状态从pending转换成fulfilled,并将resolve对应的回调函数放入异步队列(等待执行)。

实例方法.then( )

then方法可接收2个函数作为参数

  • 第一个为onResolved:当executor中执行resolve( )的时候,就会进入onResolved这个函数,传递成功的value
  • 第二个为onRejected:当executor中执行reject( )的时候,就会进入onRejected这个函数,传递失败的reason

我们可以把这2个函数单独写在外部

let p = new Promise((resolve, reject) => {
    console.log('promise执行成功')
    resolve(3)
})

p.then(onResolved,onRejected)

function onResolved(value){
    console.log('resolved:'+ value)
}

function onRejected(reason){
    console.log('rejected:'+ reason)
}
//输出如下:
//promise执行成功
//resolved:3

也可以将函数直接写在then括号内(省略函数名的定义)

let p = new Promise((resolve, reject) => {
    console.log('promise执行成功')
    resolve(3)
})

p.then((value)=>{
    console.log('rejected:'+ value)
}, (reason)=>{
    console.log('rejected:'+ reason)
})
//与上面的写法完全等价

then方法的参数是可选的

当参数只有onResolved的时候,可以这样写:

let p = new Promise((resolve, reject) => {
    resolve(3)
})

p.then((value)=>{
    console.log('resolved:'+ value)
})

当参数只有onRejected的时候,需要把第一个参数设置为null

let p = new Promise((resolve, reject) => {
    reject('error')
})

p.then(null,(reason)=>{
    console.log('rejected:'+reason)
})

实例方法 .catch( )

catch专门用于处理失败的promise对象,它只接收一个 onRejected函数作为参数

let p = new Promise((resolve, reject) => {
    reject('error')
})

p.catch((reason) => {
    console.log(reason)
})

如何判断Promise或者类似于Promise的值

虽然Promise是通过new Promise(...)语法创建,但如果用p instanceof Promise来检查某个值是否为promise,是不全面的,因为:

  • Promise的值可能是从其他浏览器窗口(iframe)中接收到的,这个窗口的Promise可能和当前窗口的不一样,所以无法识别Promise实例
  • 某一些库或框架会实现自己的Promise,而不是使用原生的ES6 Promise

所以,识别Promise就是定义一个thenable的东西,任何具有then(...)方法的对象和函数,都被称为Promise一致的thenable。

const thenable = {
	then(res) {
		setTimeout(res, 3000)
	}
}
// 1
Promise.resolve()
	.then(()=>thenable)
	.then(()=>console.log('3秒过去'));

// 2
!async function() {
	const sleep = () => thenable

	await sleep();
	console.log('3秒过去');
}();

如上面一段代码,无论是哪一种写法,都会经过3秒然后打印。证明判断一个对象是不是Promise或行为方式类似于Promise,仅仅判断它是否有 then 函数即可。

if (
    p !== null && 
   	(
		typeof p === "object" ||
    	typeof p === "function"
   	) &&
    typeof p.then === "function"
  ) {
    // 这是一个thenable对象
} else {
    // 这不是一个thenable对象
}

如果我们无意给某个对象加上then函数,却不希望它被当作Promise或者thenable,那恐怕会事与愿违,他会被自动识别为thenable,按照特定的规则处理。所以这可能是有害的,可能导致难以追踪的bug。

这种类型检测叫鸭子类型(duck typing): When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

为什么要使用Promise

如果用普通的回调来提供异步方案,会有一些信任的问题,如下面这段代码:

// A
ajax('..', function(..) {
    // C
});
// B

A和B发生于现在,C可能会延迟到将来发生,并且是在第三方的控制下。这种控制反转会出现五个问题:

  • 回调调用次数太少或太多(第三方可能会不如我们所期待地多次调用回调函数)
  • 调用回调过早(在追踪之前)
  • 调用回调过晚(甚至没有调用)
  • 没有把所需要的环境/参数传给回调函数
  • 吞掉可能出现的错误或异常

Promise的特性就是用来解决这些问题的:

  • 解决回调调用过早或过晚:即使是立即完成的promise,其回调then函数里的内容也总是会被放到微任务队列里,异步执行,即使有多个回调函数,它们的执行是独立的,不会受到影响。比如:

    p.then(function() {
        p.then(function() {
            console.log('C');
        });
        console.log('A');
    })
    p.then(function() {
        console.log('B');
    })
    // A B C
    
  • 解决回调次数过少(未调用):我们可以设定一个超时函数,并且用promise.race来解决超时未调用的问题

    function timeout(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Timeout!');
            });
        });
    }
    Promise.race([p, timeout(3000)]).then(resCb, rejCb);
    // 当超时或者p抛出错误,都会调用rejCb
    
  • 解决回调次数过多:若代码试图多次调用resolve或reject,Promise只会接受第一次决议,并忽略任何后续的调用。

    var p = new Promise((resolve, reject) => {
        // ...
        resolve('1');
        // 之后的决议全部忽略
        resolve('2');
        reject('3');
    });
    p.then((value) => { console.log(value); }, (reason) => { console.error(reason); }); // 1
    
  • 解决未能传递参数/环境值:Promise的resolve和reject都只能传一个参数,第二个参数及之后的都会被忽略,如果未显式定义,则这个值为undefined,详情见后文的链式调用流。

  • 解决吞掉异常或错误的问题:每个then函数都会返回另一个promise,所以任何地方抛出错误,都会导致相应的promise被拒绝,可以在catch或者then的第二个参数中定义异常处理,详情见后文的错误处理。

参考文章:

再一次深入理解Promise

《你不知道的JavaScript》(中卷)