学习笔记21—期约与异步函数

1 同步与异步

同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照它们出现的顺序来执行。相对地,异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作的例子可以是在定时回调中执行一次简单的数学计算:

let x = 3;
setTimeout(()=>x=x+4,1000);

为了让后续代码能够使用x,异步执行的函数需要在更新x的值以后通知其他代码。如果程序不需要这个值,那么就只管继续执行,不必等待这个结果了。设计一个能够知道x什么时候可以读取的系统是非常难的,JavaScript在实现这样一个系统的过程中也经历了几次迭代。

2 以往的异步编程模式

异步行为是JavaScript的基础,但以前的实现不理想,在早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见问题,通常需要深度嵌套的回调函数(俗称回调地狱)来解决。
(1)异步返回值
假设setTimeout操作会返回一个有用的值,有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数):

function double(value,callback){
	setTimeout(()=>callback(value*2),1000);
}
double(3,(x)=>console.log(`I was given:${x}`));

这里setTimeout调用告诉JavaScript运行时在1000毫秒之后把一个函数推到消息队列上,这个函数会由运行时负责异步调度执行。而位于函数闭包中的回调及其参数在异步执行时仍然是可用的。
(2)失败处理
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调:

function double(value,success,failure){
	setTimeout(()=>{
		try{
			if(typeof value !== 'number'){
				throw 'Must provide number as first argument';
			}
			success(2*value);
		}catch(e){
			failure(e);
		}
	},1000);
}

const successCallback = (x) =>console.log(`Success:$(x)`);
const failureCallback = (e) =>console.log(`Failure:$(e)`);

double(3,successCallback,failureCallback);
double(3,successCallback,failureCallback);

这种模式已经不可取了,因为必须在初始化异步操作时定义回调,异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。
(3)嵌套异步回调
如果异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步复杂,在实际的代码中,这就要求嵌套回调:

function double(value,success,failure){
	setTimeout(()=>{
		try{
			if(typeof value !== 'number'){
				throw 'Must provide number as first argument';
			}
			success(2*value);
		}catch(e){
			failure(e);
		}
	},1000);
}
const successCallback = (x) =>{
	double(x,(y)=>console.log(`Success:${y}`));
};

const failureCallback = (e)=>console.log(`Failure:${e}`);
double(3,successCallback,failureCallback);

3 期约

期约是对尚不存在结果的一个替身,ECMAScript6新增的引用类型Promise,可以通过new操作符来实例化,创建新期约时需要传入执行器(executor)函数作为参数:

let p = new Promise(()=>{});
setTimeout(console.log,0,p);//Promise <pending>

(1)期约状态机
在把一个期约实例传给console.log()时,控制台输出表明该实例处于待定(pending)状态。期约可能处于如下3种状态之一:

  • 待定(pending)
  • 兑现(fulfilled,有时候也成为“解决,resolved)
  • 拒绝(rejected)

待定(pending)是期约的最初始状态,在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态,无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约状态就不再改变,而且也不能保证期约必然会脱离待定状态。因此组织合理的代码无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。
重要的是期约的状态是私有的,不能直接通过JavaScript检测到,这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象,另外期约的状态也不能被外部JavaScript代码修改,这与不能读取该状态的原因是一样的,期约故意将异步行为封装起来,从而隔离外部的同步代码。

(2)解决值、拒绝理由及期约用例
期约主要有两大用途,首先是抽象地表示一个异步操作,期约的状态代表期约是否完成。在另一些情况,期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。相应的如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。
为了支持这两种用例,每个期约只要状态切换为兑现,就会有一个私有的内部值,类似地每个期约只要状态切换为拒绝,就会有一个私有的内部值,类似地每个期约只要状态切换为拒绝就会有一个私有的内部理由。无论是值还是理由,都是包含原始值或对象的不可修改的引用。二者都是可选的,而且默认值为undefined。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。

(3)通过执行函数控制期约状态
由于期约状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。**执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。**其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为resolve()和reject()。调用resolve()会把状态切换为兑现,调用reject()会把状态切换为拒绝。另外调用reject()也会抛出错误:

let p1 = new Promise((resolve,reject)=>resolve());
setTimeout(console.log,0,p1);//Promise <resolved>

let p2 = new Promise((resolve,reject)=>reject());
setTimeout(console.log,0,p2);//Promise <rejected>
//Uncaught error (in promise)

在前面的例子中,并没有什么异步操作,因为在初始化期约时,执行器函数已经改变了每个期约的状态,这里的关键在于执行器函数是同步执行的,这是因为执行器函数是期约的初始化程序,添加setTimeout可以推迟切换状态:

let p = new Promise((resolve,reject)=>setTimeout(resolve,1000));

//在console.log打印期约实例的时候,还不会执行超时回调(即resolve())
setTimeout(console.log,0,p);

无论resolve()和reject()中的哪个被调用,状态转换都不可撤销了,于是继续修改状态都会静默失败:

let p = new Promise((resolve,reject)=>{
	resolve();
	reject();//没有效果
});

setTimeout(console.log,0,p);//Promise <resolved>

为避免期约卡在待定状态,可以添加一个定时退出功能,比如可以通过setTimeout设置一个10秒后无论如何都会拒绝期约的回调:

let p = new Promise((resolve,reject)=>{
	setTimeout(reject,10000);
});

setTimeout(console.log,0,p);//Promise <pending>
setTimeout(console.log,11000,p);//11秒后再检查状态

因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间,如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败。
(4)Promise.resolve()
期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态,通过调用Promise.resolve()静态方法,可以实例化一个解决的期约,下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve,reject)=>resolve());
let p2 = Promise.resolve();

这个解决的期约的值对应传给Promise.resolve()的第一个参数,使用这个静态方法,实际上可以把任何值都转换为一个期约:

setTimeout(console.log,0,Promise.resolve());
//Promise <resolved>:undefined

setTimeout(console.log,0,Promise.resolve(3));
//Promise <resolved>:3

//多余的参数会忽略
setTimeout(console.log,0,Promise.resolve(4,5,6));
//Promise <resolved>:4

对这个静态方法而言如果传入的参数本身是一个期约,那么它的行为就类似于一个空包装。因此Promise.resolve()可以说是一个幂等方法:

let p = Promise.resolve(7);

setTimeout(console.log,0,p===Promise.resolve(p));
//true

setTimeout(console.log,0,p===Promise.resolve(Promise.resolve(p)));
//true

这个幂等性会保留传入期约的状态:

let p = new Promise(()=>());

setTimeout(console.log,0,p);//Promise <pending>
setTimeout(console.log,0,p===Promise.resolve(p));//true

注意这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约,因此也可能导致不符合预期的行为:

let p = Promise.resolve(new Error('foo'));
setTimeout(console.log,0,p);
//Promise <resolved>:Error:foo

(5) Promise.reject()
与Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)。下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve,reject)=>reject());
let p2 = Promise.reject();

这个拒绝的期约的理由就是传给Promise.reject()的第一个参数,这个参数也会传给后续的拒绝处理程序:

let p = Promise.reject(3);
setTimeout(console.log,0,p);//Promise <rejected>:3

p.then(null,(e)=>setTimeout(console.log,0,e));//3

关键在于,Promise.reject()并没有照搬Promise.resolve()的幂等逻辑,如果给它传一个期约对象,则这个期约会成为它返回拒绝期约的理由:

setTimeout(console.log,0,Promise.reject(Promise.resolve()));
//Promise <rejected>:Promise <resolved>

(6)同步/异步执行的二元性
Promise的设计很大程度上会导致一种完全不同于JavaScript的计算模式,下面的例子完美地展示了这一点,其中包含了两种模式下抛出错误的情形:

try{
	throw new Error('foo');
}catch(e){
	console.log(e);//Error:foo
}

try{
	Promise.reject(new Error('bar'));
}catch(e){
	console.log(e);
}
//Uncaught (in promise) Error:bar

在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的,因此try/catch块并不能捕获该错误,代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体的说就是期约的方法。
在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。
要获得对象上的所有可枚举的实例属性可以使用Object.keys()方法,这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组:

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 31;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let keys = Object.keys(Person.prototype);
console.log(keys);//"name,age,job,sayName"
let p1 = new Person();
p1.name = 'Rob';
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys);//[name,age]

这里keys变量保存的数组中包含"name",“age”,“job"和”sayName”,这是正常情况下通过for-in循环的顺序,而在Person的实例上调用时,Object.keys()返回的数组中只包含"name"和"age"两个属性。如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyName():

let keys = Object.getOwnPropertNames(Person.prototype);
console.log(keys);//[constructor,name,age,job,sayName]

在ECMAScript6新增符号类型之后,相应的出现了增加一个Object.getOwnPropertyNames()的兄弟方法的需求,因为以符号为键的属性没有名称概念,因此Object.getOwnPropertySymbols()方法就出现了,这个方法与Object.getOwnPropertyNames()类似,只是针对符号而已:

let k1 = Symbol('k1'),
	k2 = Symbol('k2');
let p = {
	[k1]:'k1',
	[k2]:'k2'
};
console.log(Object.getOwnPropertySymbols(o));
//[Symbol(k1),Symbol(k2)]

4 期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁,这些方法可以访问异步草做返回的数据,处理成功或失败的结果。
(1)Promise.prototype.then()
Promise.prototype.then()是为期约实例添加处理程序的主要方法,这个then()方法接收最多的两个参数:onResolved处理程序onRejected处理程序。这两个参数都是可选的,如果提供的话分别会在期约进入兑现或拒绝状态时执行:

function onResolved(id){
	setTimeout(console.log,id,'resolved');
}

function onRejected(id){
	setTimeout(console.log,0,id,'rejected');
}

let p1 = new Promise((resolve,reject)=>setTimeout(resolve,3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));

p1.then(()=>onResolved('p1'),
	    ()=>onRejected('p1'));
p2.then(()=>onResolved('p2'),
		()=>onRejected('p2'));

//3秒后
//p1 resolved
//p2 rejected

如果想只提供onRejected参数那就要在onResolved参数的位置上传入undefined,有助于避免在内存中创建多余的对象。

let p1 = Promise.resolve('foo');

//若调用then()时不传处理程序,则原样向后传
let p2 = p1.then();
setTimeout(console.log,0,p2);//Promise <resolved>:foo

//这些都一样
let p3 = p1.then(()=>undefined);
let p4 = p1.then(()=>{});
let p5 = p1.then(()=>Promise.resolve());

setTimeout(console.log,0,p3);//Promise <resolved>:undefined
setTimeout(console.log,0,p4);//Promise <resolved>:undefined
setTimeout(console.log,0,p5);//Promise <resolved>:undefined

如果有显式的返回值,则Promise.resolve()会包装这个值

let p6 = p1.then(()=>'bar');
let p7 = p1.then(()=>Promise.resolve('bar'));

setTimeout(console.log,0,p6);//Promise <resolved>:bar
setTimeout(console.log,0,p7);//Promise <resolved>:bar

//Promise.resolve()保留返回期约
let p8 = p1.then(()=>new Promise(()=>{}));
let p9 = p1.then(()=>Promise.reject());
//Uncaught (in promise):undefined

setTimeout(console.log,0,p8);//Promise <pending>
setTimeout(console.log,0,p9);//Promise <rejected>:undefined

抛出异常会返回拒绝的期约:

let p10 = p1.then(()=>{throw 'baz';});
//Uncaught (in promise)baz
setTimeout(console.log,0,p10);//Promise <rejected> baz

注意返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:

let p11 = p1.then(()=>Error('qux'));

setTimeout(console.log,0,p11);//Promise <resolved>:Error:qux

下面代码片段展示了用Promise.reject()代替之前例子中的Promise.resolve()之后的结果:

let p1 = Promise.reject('foo');

//调用then()时不传处理程序则原样后传
let p2 = p1.then();
//Uncaught (in promise) foo

setTimeout(console.log,0,p2);//Promise <rejected>:foo

//这些都一样
let p3 = p1.then(null,()=>undefined);
let p4 = p1.then(null,()=>{});
let p5 = p1.then(null,()=>Promise.resolve());

setTimeout(console.log,0,p3);//Promise <resolved>:undefined
setTimeout(console.log,0,p4);//Promise <resolved>:undefined
setTimeout(console.log,0,p5);//Promise <resolved>:undefined


//这些都一样
let p6 = p1.then(null,()=>'bar');
let p7 = p1.then(null,()=>Promise.resolve('bar'));

setTimeout(console.log,0,p6);//Promise <resolved>:bar
setTimeout(console.log,0,p7);//Promise <resolved>:bar

//Promise.resolve()保留返回的期约
let p8 = p1.then(null,()=>new Promise(()=>{}));
let p9 = p1.then(null,()=>Promise.reject());
//Uncaught (in promise):undefined

setTimeout(console.log,0,p8);//Promise <pending>
setTimeout(console.log,0,p9);//Promise <rejected>:undefined

let p10 = p1.then(null,()=>{throw 'baz';});
//Uncaught (in promise)baz

setTimeout(console.log,0,p10);//Promise <rejected>:baz

let p11 = p1.then(null,()=>Error('qux'));

setTimeout(console.log,0,p11);//Promise <resolved>:Error:qux

(2)Promise.prototype.catch()
用于给期约添加处理程序,只接收一个参数:onRejected处理程序:

let p = Promise.reject();
let onRejected = function(e){
	setTimeout(console.log,0,'rejected');
};

//这两种添加拒绝处理程序的方式是一样的:
p.then(null,onRejected);//rejected
p.catch(onRejected);//rejected

Promise.prototype.catch()返回一个新的期约实例:

let p1 = new Promise(()=>{});
let p2 = p1.catch();
setTimeout(console.log,0,p1);//Promise <pending>
setTimeout(console.log,0,p2);//Promise <pending>
setTimeout(console.log,0,p1 === p2);//false

在返回期约实例方面,Promise.prototype.catch()行为与Promise.prototype.then()的onRejected处理程序是一样的。
(3)Promise.prototype.finally()
Promise.prototype.finally()方法用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免onResolved和onRejected处理程序中出现冗余代码,但onFinally处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码:

let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function(){
	setTimeout(console.log,0,'Finally!')
}
p1.finally(onFinally);//Finally
p2.finally(onFinally);//Finally

Promise.prototype.finally()方法返回一个新的期约实例:

let p1 = new Promise(()=>{});
let p2 = p1.finally();
setTimeout(console.log,0,p1);//Promise <pending>
setTimeout(console.log,0,p2);//Promise <pending>
setTimeout(console.log,0,p1===p2);//false

在大多数情况下他将表现为父期约的传递,对于已解决状态和被拒绝状态都是如此:

let p1 = Promise.resolve('foo');

let p2 = p1.finally();
let p3 = p1.finally(()=>undefined);
let p4 = p1.finally(()=>{});

setTimeout(console.log,0,p2);//Promise <resolved>:foo
setTimeout(console.log,0,p3);//Promise <resolved>:foo
setTimeout(console.log,0,p4);//Promise <resolved>:foo

如果返回是一个待定的期约,或onFinally处理程序抛出了错误,则会返回相应的期约(待定或者拒绝):

let p9 = p1.finally(()=>new Promise(()=>{}));
let p10 = p1.finally(()=>Promise.reject());
//Uncaught(in promise):undefined

setTimeout(console.log,0,p9);//Promise <pending>
setTimeout(console.log,0,p10);//Promise <rejected>:undefined

let p11 = p1.finally(()=>{throw 'baz'});
//Uncaught (in promise) baz

返回待定期约的情形并不常见,这是因为只要期约一解决,新期约仍然会原样后传初始的期约:

let p1 = Promise.resolve('foo');
let p2 = p1.finally(()=>new Promise((resolve,reject)=>setTimeout(()=>resolve('bar'),100)));

setTimeout(console.log,0,p2);//Promise <pending>
setTimeout(()=>setTimeout(console.loh,0,p2),200);

//200毫秒后:
//Promise <resolved>:foo

3 非重入期约的方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行,跟在添加这个处理程序的代码之后的同步代码一定会在处理之前先执行:

//创建解决的期约
let p = Promise.resolve();

//添加解决处理程序
p.then(()=>console.log('onResolved hanlder'));

console.log('then() returns');

//then() returns
//onResolved handler

先添加处理程序后解决期约也是一样的,如果添加处理程序后,同步代码才改变期约状态,那么处理程序仍然会基于该状态变化表现出非重入特性:

let synchronousResolve;

let p = new Promise((resolve)=>{
	synchronousResolve = function(){
		console.log('1:invoking resolve()');
		resolve();
		console.log('2:resolve() returns');
	};
});

p.then(()=>console.log('4:then() handler executes'));

synchronousResolve();
console.log('3:synchronousReolve() returns');

//1:invoking resolve()
//2:resolve() returns
//3:synchronousResolve() returns
//4:then() handler executes

非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally()处理程序,下面例子演示了这些处理程序都只能异步执行:

let p1 = Promise.resolve():
p1.then(()=>console.log('p1.then() onResolved'));
console.log('p1.then() returns');

let p2 = Promise.reject();
p2.then(null,()=>console.log('p2.then() onRejected'));
console.log('p2.then() returns');

let p3 = Promise.reject();
p3.catch(()=>console.log('p3.catch() onRejected'));
console.log('p3.catch() returns');

let p4 = Promise.resolve();
p4.finally(()=>console.log('p4.finally() onFinally'));

console.log('p4.finally() returns');

//p1.then() returns
//p2.then() returns
//p3.catch() returns
//p4.finall() returns
//p1.then() onResolved
//p2.then() onRejected
//p3.catch() pnRejected
//p4.finally() onFinally
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值