JS异步编程:Promise、Async/Await、setTimeout与事件循环机制

异步编程

在 JavaScript 这种单线程事件循环模型中,同步操作与异步操作是代码所要依赖的核心机制。

异步行为是为了优化因计算量大而时间长的操作。在等待其他操作完成时,即使运行其他指令,系统也能保持稳定。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

以往的异步编程(setTimeout)

function double(value) {
    
 setTimeout(() => setTimeout(console.log, 0, value * 2), 1000); 
} 
double(3); 
// 6(大约 1000 毫秒之后)

对这个例子而言,1000 毫秒之后,JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后,回调什么时候出列被执行对 JavaScript 代码就完全不可见了。double()函数在 setTimeout 成功调度异步操作之后会立即退出。

1.处理异步返回值

假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?

=》给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。

function double(value, callback) {
    
 setTimeout(() => callback(value * 2), 1000); 
} 
double(3, (x) => console.log(`I was given: ${
     x}`)); 
// I was given: 6(大约 1000 毫秒之后)

这里的 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('b', successCallback, failureCallback); 
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)

这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

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); 
// Success: 12(大约 1000 毫秒之后)

显然,随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。

Promise

成为了主导性的异步编程机制。

Promise 基础

ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。

当通过new创建Promise实例时,需要传入一个回调函数,我们称之为executor

  • 这个回调函数会被立刻执行,并传入两个回调参数resolvereject
  • 当调用resolve回调函数时,会执行 Promise 对象的then方法传入的回调
  • 当调用reject回调函数时,会执行 Promise 对象的catch方法传入的回调
  1. Promise状态机

promise 是一个有状态的对象,有以下3种:

 待定(pending):最初始状态代表尚未开始或者正在执行中
 兑现(fulfilled,有时候也称为“解决”,resolved):代表成功
 拒绝(rejected):代表失败

无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,promise的状态就不再改变。

重要的是,promise的状态是私有的,不能直接通过 JavaScript 检测到或修改。promise故意将异步行为封装起来,从而隔离外部的同步代码。

2.解决值、拒绝理由及用例

promise主要有两大用途。

(1)抽象地表示一个异步操作。

(2)promise封装的异步操作会实际生成某个值,而程序期待promise状态改变时可以访问这个值。

为了支持这两种用例,每个promise只要状态切换为resolved,就会有一个私有的内部值(value)。
每个promise只要状态切换为rejected,就会有一个私有的内部理由(reason)。

3.通过执行函数控制promise状态

由于promise的状态是私有的,所以只能在内部进行操作。内部操作在promise的执行器函数中完成。

执行器函数主要有两项职责:初始化promise的异步行为和控制状态的最终转换。

其中,控制promise状态的转换是通过调用它的两个函数参数实现的。

调用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) 

在前面的例子中,并没有什么异步操作,因为在初始化promise时,执行器函数已经改变了每个promise的状态。这里的关键在于,执行器函数是同步执行的。这是因为执行器函数是promise的初始化程序

通过下面的例子可以看出上面代码的执行顺序:

new Promise((
  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值