1.什么是Promise

Promise是异步编程的一种解决方案,他是一个对象,可以获取异步操作的消息,避免了回调地狱

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

Promise的实例有三个状态 :

  1. Pending(进行中)
  2. Resolved(已完成)
  3. Rejected(已拒绝)

把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected

具有then方法的鸭子类型:
在Promise领域,一个重要的细节是如何确定某个值是不是真正的Promise。首先我们要知道,不能通过 p instanceof Promise来检查。因为Promise的值可能是从其他浏览器窗口接收到的,这个浏览器窗口自己的Promise可能和当前窗口的不同,并且有些库或框架可能会选择实现自己的Promise,而不是使用原生的ES6 Promise实现。
因此,识别Promise就是定义某种称为thenable的东西,将其定义为任何具有then(…)方法的对象和函数。我们认为,任何这样的值就是Promise一致的thenable。
根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查一般用术语鸭子类型(duck typing)来表示 ——“如果它看起来像只鸭子,叫起来像只鸭子,那他就一定是一只鸭子”
于是对thenable值的鸭子类型大致检测就类似于:
(p !== null && (typeof p === “object” || typeof p === “function” ) && typeof p.then === “function”)

2.Promise信任问题

2.1 调用过早
这个问题只要就是担心代码是否会引入类似Zalgo这样的副作用,在这类问题中,一个任务有时同步完成,有时异步完成,这就可能导致竞态条件。
根据定义,Promise就不必担心这种问题,因为即使是立即完成的Promise

new Promise(function(resolve){resolve(42)})

类似这样的代码,也不会被同步观察到。也就是说,对一个Promise调用then(…)的时候,即使这个Promise已经决议,提供给then(…)的回调也总会被异步调用。

2.2 调用过晚
Promise创建对象调用resolve(…)或reject(…)时,这个Promise的then(…)注册的观察回调就会被自动调用,可以确信,这些调度的回调在下一个异步事件点上一定会被触发,并且这个Promise上所有的通过then(…)注册的回调都会在下一个异步时机上依次被立即调用

p.then(function(){
	p.then(function(){
		console.log("C")
	})
	console.log("A")
})
p.then(function(){
	console.log("B")
})
//A B C
//这里的C无法抢断B

2.3 回调未调用
首先没有任何东西(甚至JavaScript错误)都不能阻止Promise向你通知它的决议。如果你对Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总会调用其中一个。
当然如果你的回调函数本身包含了JavaScript的错我,那可能你看不到期望的结果。
但是如果Promise本身永远不被决议呢?即使这样Promise也提供了解决方法,其使用了一种称为竞态的高级抽象机制:

//用于超时一个Promise的工具
function timeoutPromise(delay){
	return new Promise(function(resolve,reject){
		setTimeout(function(){
			reject("Timeout")
		},delay);
	});
}

//设置foo超时
Promise.race([
	foo(), //尝试开始foo()
	timeoutPromise(3000) //给他3秒钟
]).then(
	function(){
		//foo(..)及时完成
	},
	function(err){
		//或者foo()被拒绝,或者是没能按时完成。查看err属于那种情况
	}
)

2.4 调用次数过少或过多
Promise的定义方式使得他只能被决议一次,如果出于某种原因,Promise创建代码试图调用resolve(…)或reject(…)多次,或者试图两者都调用,那这个Promise也只会接受一次决议,并默默忽略任何后续的调用。
当然,如果你把同一个回调注册了不止一次,那么他的调用次数就会和注册次数相同。响应函数只会被调用一次,但这个保证并不能预防你搬起石头砸自己的脚。

2.5 未能传递参数/环境值
Promise至多只有一个决议值。
如果你没有用任何值显式决议,那么这个值就是undefined。但不管这个值是什么,无论当前或未来,她都会被传送给所有注册的回调。
如果使用多个参数调用resolve(…)或者reject(…)第一个参数之后的所有参数都会被默默忽略。
如果要传递多个值,你就必须要把他们封装在单个值中传递,比如通过一个数组或者对象。

2.6 吞掉错误或者异常
如果拒绝一个Promise并给出一个理由,这个值就会被传递给拒绝回调。但是如果在Promise的创建过程中或在查看决议结果过程中的任何时间点出现了一个JavaScript异常错误,比如一个TypeError或者ReferenceError,那么这个异常就会被捕获,并且使这个Promise被拒绝。

var p = new Promise(function(resolve,reject){
	foo.bar(); //这里会报错,foo未定义
	resolve(42);
})
p.then(
	function fulfilled(){
		//永远不会执行到这里
	},
	function rejected(err){
		//err将会是一个来自foo.bar()这一行的TypeError异常对象
	}
)

这是一个重要的细节,因为其有效的解决了另一个潜在的Zalgo风险,即出错可能会引起同步响应,而不出错则会使异步的。Promise甚至把JavaScript异常也变成了异步行为,进而极大地降低了竞态条件出现的可能。

但是如果Promise完成后再查看结果是出现JavaScript异常错误会怎样那?

var p = new Promise(function(resolve,reject){
	resolve(42);
})
p.then(
	function fulfilled(msg){
		foo.bar(); 
		console.log(msg) //永远不会执行到这里
	},
	function rejected(err){
		//永远不会执行到这里
	}
)

这里看起来像是foo.bar()产生的异常被吞掉了。但实际上是p.then(…)调用本身返回了一个promise,就是我们没有监听他,正是这个promise将会因TypeError异常而被拒绝。
为什么不是简单调用我们定义的错误处理函数那?因为这样的话就会违背Promise的一条基本原则,即Promise一旦决议就不能在被改变。

2.7 是可信任的Promise吗
你肯定已经注意到了Promise并没有完全摆脱回调,他只是改变了回调的位置。我们并不是把回调传递给foo(),而是从foo(…)得到某个东西,然后把回调传给这个东西。为什么这就比单纯的回调更值得信任呢?
包含在原生ES6Promise实现中的解决方案就是Promise.resolve(…)
如果向Promise.resolve(…)传递一个非Promise,非thenable的立即值,就会得到一个用这个值填充的promise。下面这种情况,promise p1和promise p2的行为完全一样。

var p1 = new Promise(function(resolve,reject){
	resolve(42);
})
var p2 = Promise.resolve(42);

而向Promise.resolve(…)传递一个真正的Promise,就只会返回同一个Promise

var p1 = Promise.resolve(42);
var p2 = Promise.resolve(p1);
p1 === p2 //true

Promise,resolve(…)可以接受任何thenable,并将其解封为他的非thenable值,从而得到一个真正的Promise,是一个可信任的值。

2.8 建立信任
Promise这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可靠合理。通过把回调的控制反转反转回来,我们把控制权放在一个可信任的系统(Promise)中,这种系统的设计目的就是为了异步编程更清晰。

3.链式流

我们可以把多个Promise链接到一起表示一系列异步操作。
这种方式可以实现的关键在于以下两个Promise固有行为特性:

  • 每次你对Promise调用then(…),他都会创建并返回一个新的Promise,我们可以将其链接起来。
  • 不管从then(…)调用的完成回调返回的值是什么,他都会被自动设置为被链接Promise的完成。
var p = Promise.resolve(21);
p
.then(function(v){
	console.log(v); // 21
	return v*2;
})
.then(function(v){
	console.log(v); //42
})

在这个例子中,一步步传递的值是可选的。如果不显式返回一个值,就会隐式返回undefined,并且这些promise仍然会以同样的方式链接在一起。这样,每个promise的决议就成了继续下一个步骤的信号。

4.错误处理

对于多数开发者来说,错误处理最自然地形式就是同步的try…catch结构。遗憾的是,他只能是同步的,无法用于异步代码模式。

function foo(){
	setTimeout(function(){
		baz.bar();
	},100)
}
try{
	foo() //后面的baz.bar()会抛出全局错误
}
catch{
	//永远不会到达这里
}

在回调中,一些模式化的错误处理方式已经出现,最值得一提的是error-first回调风格:

function foo(cb){
	setTimeout(function(){
		try{
			var x = baz.bar();
			cb(null,x);//成功
		}
		catch(err){
			cb(err)
		}
	},100)
}

foo(function(err,val){
	if(err){
		console.error(err);
	}else{
		console.log(val);
	}
})

只有在baz.bar()调用同步地立即执行的情况下才有用,否则如果baz.bar()本身有自己的异步完成函数,其中的任何错误都不能被捕捉。

4.1 绝望的陷阱
Jeff Atwood多年前曾提出:通常编程语言构建的方式是,默认情况下,开发者陷入“绝望的陷阱(pit of despair)”,需要为错误付出代价,只有努力才能做到,他呼吁我们转而构架一个“成功的坑(pit of success)”,其中默认情况下你能够得到想要的结果,想出错很难。
毫无疑问,Promise错误处理就是一个绝望的陷阱设计,默认情况下,他假定你想要Promise状态吞掉所有的错误。如果你忘了查看这个状态,这个错误就会绝望的在暗处凋零死掉。
为了避免丢失被忽略和抛弃的Promise错误,一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(…)结束

var p = Promise.resolve(42);
p.then(
	function fulfilled(msg){
		//数字没有string函数所以会抛出错误
		console.log(msg.toLowerCase());
	}
)
.catch(handleErrors);

因为我们没有为then(…)传入拒绝处理函数,所以默认的处理函数被替换掉了,而这仅仅是把错误传递给了链中的下一个promise。因此,进入p的错误以及p之后进入其决议的错误都会传给最后的handleErrors中。
看似问题解决了?
可是如果handleErrors中也有错误该怎么办呢?!

4.2 处理未捕获的情况
浏览器有一个特有的功能是我们代码所没有的:他们可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机。所以浏览器可以追踪Promise对象。如果在他被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正未被捕获的错误,进而确定应该将其报告到开发者终端。

但是如果一个Promise未被垃圾回收该怎么办?!

4.3 成功的坑
接下来的内容只是理论上的:

  • 默认情况下,Promise在下一个任务或时间循环tick上(向开发者终端)报告所有拒绝,如果在这个时间点上该Promise上还没有注册错误处理函数。
  • 如果想要一个被拒绝的Promise在查看之前的某个时间段内保持被拒绝状态,可以调用defer(),这个函数优先级高于该Promise的自动错误报告。

如果一个Promise被拒绝的话,默认情况下会向开发者终端报告这个事实(而不是默认的沉默)。可以选择隐式(再拒绝前注册一个错误处理函数)或者显式(通过defer())禁止这种报告。在这两种情况下,都是你来控制误报的情况。

5.Promise模式

5.1 Promise.all([…])
在异步序列中(Promise链),任意时刻都只能有一个异步任务正在执行——步骤2只能在步骤1之后,步骤3也只能在步骤2之后。但是,如果要同时执行两个或更多步骤(并行执行)要怎么实现呢?
在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行/并发任务都完成才能继续。他们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。
在PromiseAPI中,这种模式被称为all([…])。

var p1 = request("http://some.url.1/")
var p2 = request("http://some.url.2/")
Promise.all([p1,p2])
.then(function(msgs){
	//这里.p1和.p2完成并把他们的消息传入
	return request(
		"http://some.url.3/?v="+msgs.join(",")
	);
});
.then(function(msg){
	console.log(msg)
})

Promise.all([…])需要一个参数,是一个数组,通常由Promise实例组成,从Promise.all([…])调用返回的promise会收到一个完成信息。这是一个由传入的promise的完成消息组成的数组,与指定的顺序一致,与完成顺序无关。如果这些promise中有一个被拒绝,主Promise.all([…])就会立即被拒绝,并丢弃来自其他所有promise的全部结果。

5.2 Promise.race([…])
尽管Promise.all([…])协调多个并发Promise的运行,并假定所有Promise都需要完成,但有时候你会想只响应“第一个跨过终点线的Promise”,而抛弃其他Promise。这种模式传统上称为门闩,在Promise中称为竞态。
Promise.race([…])也接受单个数组参数。这个数组由一个或多个Prmose,thenable或立即值组成。但是立即值的竞争在实践中没有太大意义。因为显然列表中的第一个会获得胜利,就像赛跑中有一个选手从终点开始比赛一样
与Promise.all([…])类似,一旦有任何一个Promise决议完成,Promise.race([…])就会完成,一旦有任意一个Promise决议为拒绝,他就会拒绝。
(1)超时竞赛

//用于超时一个Promise的工具
function timeoutPromise(delay){
	return new Promise(function(resolve,reject){
		setTimeout(function(){
			reject("Timeout")
		},delay);
	});
}

//设置foo超时
Promise.race([
	foo(), //尝试开始foo()
	timeoutPromise(3000) //给他3秒钟
]).then(
	function(){
		//foo(..)及时完成
	},
	function(err){
		//或者foo()被拒绝,或者是没能按时完成。查看err属于那种情况
	}
)

(2)finally
一个关键的问题:那些被丢弃或忽略的promise会发生什么呢?我们不是从性能的角度提出这个问题,因为他们通常最终会被垃圾回收。我们是从行为的角度,Promise不能被取消,也不该被取消。因为这样会摧毁Promise的外部不变性原则,所以他们只能被默默忽略。
于是就有些开发者提出,Promise需要一个finally(…)回调注册,这个回调在Promise决议后总是会被调用,并且允许你执行任何必要的清理工作。但目前规范还没支持,或许会在ES7+中吧。

var p = Promise.resolve(42);
p.then(something)
.finally(cleanup)
.then(something)
.finally(cleanup)

(3)all([…])和race([…])的变体

  • none([…]) 所有的Promise都要被拒绝,即拒绝转换成完成值,反之亦然。
  • any([…]) 只需完成一个即可
  • first([…]) 等第一个完成后,他就会忽略后续的任何拒绝和完成
  • last([…])只有最后一个完成胜出

(4)并发迭代
有时候需要在一列Promise中迭代,并对所有的Promise都执行某个任务,非常类似与对同步数组可以做的那样(比如some,foreach,map)。如果要对每个Promise执行的任务是同步的,那这些工具就是可以工作。
但如果这些任务从根本上是异步的,或者可以并发执行,那你可以使用这些工具的异步版本。

if(!Promise.map){
	Promise.map = function(vals,cb){
		//一个等待所有map的promise的新promise
		return Promise.all(
			//一般数组map(..)把值数组转换成promise数组
			vals.map(function(val){
				//用val异步map之后决议的新promise代替val
				return new Promise(function(resolve){
					cb(val,resolve);
				})
			})
		)
	}
}

在这个map(…)实现中,不能发送异步拒绝信号,但如果在映射的回调(cb(…))内出现同步的异常或错误,主Promise.map(…)返回的promise就会拒绝。

6.Promise局限性

  1. 顺序错误处理:Promise链中的错误很容易被无意中默默忽略掉。
  2. 单一值:Promise只能有一个完成值或一个拒绝理由
  3. 单决议:Promise只能被决议一次
  4. 惯性:运动状态(使用回调)的代码库会一直保持运动状态(使用回调),直到受到一位聪明的,理解Promise开发者的作用。
  5. 无法取消的Promise:一旦创建,如果出现了某一种情况使得这个任务悬而未决的话,你也没办法从外部停止他的进程。
  6. Promise性能:稍慢一点,但是作为交换,你得到的是大量内建的可信任性,对Zalgo的避免以及可组合性。

7.手搓Promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function myPromise(fn){
	this.state = PENDING;
	this.value = null;
	this.resolveCallbacks = [];
	this.rejectedCallbacks = [];
	
	function resolve(value){
		this.state = RESOLVED;
		this.value = value;
		this.resolveCallbacks.map(cb => cb(this.value))
	}

	function reject(value){
		this.state = REJECTED;
		this.value = value;
		this.rejectedCallbacks.map(cb => cb(this.value))
	}
	
	try{
		fn(resolve,reject)
	}catch(e){
		reject(e)
	}

	myPromise.prototype.then = function(onFulfilled,onRejected){
		const that = this;
		onFulfilled = typeof onFulfilled === 'function' ? onFulfilled:v =>{}
		onRejected = typeof onRejected === 'function' ? onRejected:v =>{}
		if(that.state === PENDING){
			this.resolveCallbacks.push(onFulfilled)
			this.rejectedCallbacks.push(onRejected)
		}
		if(that.state === RESOLVED){
			onFulfilled(that.value)
		}
		if(that.state === REJECTED){
			onRejected(that.value)
		}
		return that;
	}
	
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐