欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

ES6 - 异步处理技术(Promise、async、await)优化

最编程 2024-01-03 07:43:46
...

异步场景

现在有个程序输入电影名字就去下载这部电影,然后下载的时候只能同时下载一部,下载完成一部再能开始下一部的下载。下载成功会有相关提示,下载失败也有相关提示。如下。

// 这个函数有三个参数 1.要下载的电影名字 2.下载成功的回调 3.下载失败的会回调
function download(name, resolve, reject) {
    // 模拟正在下载
    console.log("正在下载......");
    
    // 模拟一秒钟后下载成功或是失败
    setTimeout(() => {
        if(Math.random() < 0.7) {
            resolve(`下载《${name}》成功`);
        }else {
            reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
        }
    }, 1000)
}

// 调用这方法下载《电影1》
download("电影1", data => {
    console.log(data);
}, reason => {
    console.log(reason);
})

上面是下载一部电影的场景,那么如果要下载4部呢,下载四十部呢,要怎么调用这个函数。在说明一下要求:下载多个电影的时候,只有前一个下载任务完成,下一个下载任务才能开始。下面是示例代码。

// 下载《电影1》
download("电影1", (data) => {
    console.log(data);
    // 下载成功继续下载《电影2》
    download("电影2", (data) => {
        console.log(data);
        // 下载成功继续下载《电影3》
        download("电影3", (data) => {
            console.log(data);
            // 下载成功继续下载《电影4》
            download("电影4", (data) => {
                console.log(data);
                // 到这里全部下载任务完成
                console.log("全部下载任务完成")
            }, (reason) => {
                // 如果失败,后续下载任务直接崩溃
                console.log(reason);
            })
        }, (reason) => {
            // 如果失败,后续下载任务直接崩溃
            console.log(reason);
        })
    }, (reason) => {
        // 如果失败,后续下载任务直接崩溃
        console.log(reason);
    })
}, (reason) => {
    // 如果失败,后续下载任务直接崩溃
    console.log(reason);
})

可以看到,才下载4部电影就要写这么长的代码,如果是40部呢!!!

  • 上面就是一个异步的场景,通过回调函数的方式来解决。
  • 上面可以看到回调一层一层的嵌套,形成了传说中的回调地狱
  • 以前没有一个好的方法解决异步问题只能使用回调的方式,但是现在ES6推出了一个叫Promise的东西,就是专门来解决回调的。这个时候我们的Promise闪亮登场。

Promise规范

概述

  • 什么是范?
    • 就是说,我规定这个地方应该有什么东西,这个东西应该能干什么事。跟规划一个道理
  • 什么是Promise?
    • Promise是一套专门处理异步场景的规范,他能有效的避免回调地狱,使代码清晰统一。

Promise的规范有什么

  1. 所有的异步场景,都可以看作是一个异步任务,每个任务在js中应该表现为一个对象,这个对象成为Promise对象。如下
  • 使用网络请求数据
  • 延时操作
  1. 每个Promise对象都应该有两个阶段三个状态;
  • 未决阶段(unsettled)
    • 挂起状态(pending)
  • 已决阶段(settled)
    • 成功状态(fulfilled)
    • 失败状态(rejected)
  1. 状态转变过程,转变过程都是从未决状态(unsettled)已决状态(settled)

    • 挂起(pending) --> 完成(fulfilled)的过程,称之为resolve

    • 挂起(pending)--> 失败(rejected)的过程,称之为reject

    • 成功状态可以有成功的相关数据,失败有失败的相关错误信息。后续代码演示

  2. 改变状态之后可以针对完成或者失败进行后续操作。

  • 针对“完成状态”的后续处理称之为onFulfilled
  • 针对失败的后续处理成为onRejected

Promise API

Promise构造函数

ES6提供了一个Promise构造含数,用来帮我们创建Promise对象,我们每个异步任务都是一个对象。这个函数有两个参数,一般我们第一个参数写成resolve,第二个参数写成reject

// 创建一个Promise对象
const demo = new Promise((resolve, reject) => {
    ......
})

// 可以查看一下当前这个对象的状态
console.log(demo);   // Promise { <pending> }

这个时候我们就可以用一个异步场景来使用一下这个Promise。比如现在小明去贩卖机买瓶可乐,但是这个贩卖机有时候会出问题,给钱1秒钟后会有50%的几率会把可乐吐出来50%的几率机器会出问题没有反应。

const buy = new Promise((resolve, reject) => {
    console.log("付钱了,等待贩卖机出可乐...");
    setTimeout(() => {
        // 模拟50%的几率出可乐,和不出可乐
        if(Math.random() - 0.5 > 0) {
            // 购买成功,resolve是一个函数,这个函数会把promise对象的状态改成 fulfilled
            resolve("购买成功, 贩卖机没有出问题!!!");
        }else {
            // 购买失败,reject是一个函数,这个函数会把promise对象的状态改成 rejected
            reject("完蛋了, 3块钱没了, 人品也太差了点!!!");
        }
    }, 1000)
})

构造函数里面的代码会马上运行。但是还有一个问题,就是现在异步任务也创建了,也已经从未决阶段变成已决阶段,那完成然后呢,然后要干什么事情呢,这里还没有定义,还没办法定义已决状态之后要干什么事。这个时候Promise对象的then方法闪亮登场。

then方法

这个方法传入两个回调函数,第一个函数是状态为fulfilled时的回调,第二个函数是状态rejected时的回调。如下

// 调用上面创建的对象的then方法,输出对应状态的状态信息
buy.then((data) => {
    console.log(data);
}, (reason) => {
    console.log(reason);
})

/* 
可能会输出以下信息:
付钱了,等待贩卖机出可乐...
完蛋了, 3块钱没了, 人品也太差了点!!!
*/

优化下载电影的方法

// 改成返回一个Promise对象
function download(name) {
    return new Promise((resolve, reject) => {
        // 模拟正在下载
        console.log(`正在下载《${name}》......`);
        // 模拟一秒钟后下载成功或是失败
        setTimeout(() => {
            if (Math.random() < 0.8) {
                resolve(`下载《${name}》成功`);
            } else {
                reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
            }
        }, 2000)
    })
}

download("电影1").then(data => {
    console.log(data);
}, reason => {
    console.log(reason);
})

这个时候,还没有解决回调地狱的问题。

catch

这个方法是专门处理失败的回调,比如说,一个任务成功了但是成功了你不想做任何操作,当失败的时候再进行相应的操作就可以使用这个方法。例子:你去贩卖机买一瓶可乐,如果给了钱贩卖机也把可乐吐出来了,这时候任务就是成功了不做任何后续操作。但是如果你付了钱贩卖机没有把可乐吐出来,那你就会想我的血汗钱怎么能说没就没呢,这个时候你就要吧这个贩卖机拆了,代码如下。

// 一个买可乐的任务
const buy = new Promise((resolve, reject) => {
    console.log("付钱,等待出可乐......");
    // 一秒钟之后没有出可乐
    setTimeout(() => {
        reject();
    }, 1000)
})

// 我们使用then方法第一个参数是成功的回调,第二个参数是失败的回调,如果只想处理失败的话可以写成下面这样
buy.then(null, () => {
    console.log("你这小贩卖机敢吞我钱,看我不把你拆了");
})

我们还可以使用更简洁的方法处理失败状态,那就是使用catch方法。就不用像上面那样在第一个参数写一个null

// 只处理失败状态
buy.catch(() => {
    console.log("你这小贩卖机敢吞我钱,看我不把你拆了")
})

buy.then(null, () => {}) = buy.catch(() => {})

链式调用

什么是链式调用:

在看我们去贩卖机买可乐的例子,50%几率出可乐50%几率不出可乐。如果出可乐了是不是有对应的操作,不出可乐又有对应的操作。

  • 出可乐了
    • 买可乐 -> 拿到可乐 -> 打开拉环
  • 没有出可乐
    • 买可乐(没有买到) -> 既然没有买到是不是拿到可乐和打开拉环这个任务就没办法执行了 我们买可乐是一个任务,然后买了拿到可乐也是一个任务,拿到可乐打开拉环又是一个任务。所以我们的then方法也是一样,这个方法会返回一个新的Promise对象。代码解释如下
const buy = new Promise(resolve => {
    // 直接买到可乐了
    resolve("买到了");
})

// 针对买到进行后续操作
const result = buy.then(data => {
    console.log(data);       // 买到了
})

// 我们看看result是一个什么东西
console.log(result);        // Promise { <pending> }

我们可以看到上面then方法的返回的也是一个Promise对象

既然如此,返回的对象我们可以在次调用then方法,如下

const buy = new Promise(resolve => {
    resolve("买到了");
})

buy.then(() => {
    console.log("拿可乐");      
}).then(() => {
    console.log("打开拉环");
})

我们根据前一个任务的返回值再继续调用方法,就叫做链式调用。我们的then方法会返回Promise对象(新任务)那么这个对象也是会有状态的,那这个状态是怎么改变的呢,then方法里面又没有resolve和reject可以调用让其改变状态。

新任务的状态取决于后续处理

  1. 如果没有后续处理,那么新任务的状态跟旧任务的状态一致,相关数据就是旧任务的数据。如下
const buy = new Promise((resolve) => {
    resolve("买到了");
})

const result = buy.catch(() => {
    console.log("没买到")
});

// 这里得延迟一点时间再输出状态不然状态永远为pending
setTimeout(() => {
    console.log(result);        // Promise { '买到了' }
})

上面刚开始直接把状态改为成功,并返回相关数据,但是只对失败状态进行处理,没有对成功状态进行处理。那么新任务的状态和相关数据,就跟旧任务一致

  1. 有后续处理,但是还没有执行,新任务状态为挂起。代码如下
const buy = new Promise((resolve) => {
    setTimeout(() => {
        resolve("买到了");
    }, 2000)
})
const result = buy.catch(() => {
    console.log("没买到")
});

setTimeout(() => {
    console.log(result);        // Promise { <pending> }
})

可以看到输出的状态为挂起,因为旧任务还没有结果,新任务就得等着。

  1. 如果有后续处理并且执行了,就会根据后续处理的情况决定新任务的状态
    • 后续处理代码没有报错新任务状态为完成,相关数据为return的返回值
    • 后续处理代码报错新任务状态为失败,相关数据为错误对象
    • 后续处理的是return new Promise(),返回一个新对象,新任务状态跟这个对象的状态一致
  • 先看第一个情况后续处理没有报错,比如说买可乐买到手了,后续操作是要打开拉环
const buy = new Promise((resolve) => {
    resolve();
})
const result = buy.then(() => {
    console.log("买到了");
    return "成功打开拉环";
});

setTimeout(() => {
    console.log(result);        // Promise { '成功打开拉环' }
})

// ----------------------------------------------------------------------
// 新的情况,你去买可乐,但是付钱了没有买到,你又重新付了一次钱。这个重新付一次钱是新的任务,代码如下
const buy2 = new Promise((resolve, reject) => {
    reject("没买到");
})
const result2 = buy2.catch(() => {
    console.log("重新付钱,购买成功");
    return "成功";
})

setTimeout(() => {
    console.log(result2);       // Promise { '成功' }
})

上面代码可以发现,只要后续处理没有报错,新任务的状态就一定会是成功。

  • 接下来是后续代码出错,也是买可乐也买到,但是你发现这个可乐易拉罐没有拉环!!!。
const buy = new Promise((resolve) => {
    resolve();
})
const result = buy.then(() => {
    // throw new Error("什么鬼,居然没有拉环!!!")
    throw "什么鬼,居然没有拉环!!!";
});
result.catch(() => {});  // 得加一个后续处理不然会弹出警告,不用管这行代码

setTimeout(() => {
    console.log(result);        // Promise { <rejected> '什么鬼,居然没有拉环!!!' }
})

看上面代码你就会发现,抛出一个错误后,状态变成了rejected。你可以手动抛出错误,代码书写错误报错一样会导致任务失败

  • 最后一种情况是后续处理里面return new Promise(())
const buy = new Promise((resolve) => {
    resolve();
})
const result = buy.then(() => {
    return new Promise((resolve, reject) => {});
});

setTimeout(() => {
    console.log(result);        // Promise { <pending> }
})

上面第一步将状态改为成功了,但是新任务里面返回了一个新任务对象,这个时候新任务的状态就要取决于这个新对象的状态。

使用链式调用优化下载电影功能

function download(name) {
    return new Promise((resolve, reject) => {
        // 模拟正在下载
        console.log(`正在下载《${name}》......`);
        // 模拟一秒钟后下载成功或是失败
        setTimeout(() => {
            if (Math.random() < 0.8) {
                resolve(`下载《${name}》成功`);
            } else {
                reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
            }
        }, 2000)
    })
}

// 调用then方法连续为成功状态做后续处理,在最后处理失败状态
download("电影1").then(data => {
    console.log(data);
    return download("电影2");
}).then(data => {
    console.log(data);
    return download("电影3");
}).then(data => {
    console.log(data);
    return download("电影4");
}).then(data => {
    console.log(data);
    console.log("全部下载完成")
}, reason => {
    console.log(reason);
})

其他API

方法 作用
Promise.resolve() 直接返回一个状态为fulfilled的promise对象
Promise.reject() 直接返回一个状态为reject的promise对象
Promise.all() 返回一个promise对象,传入一个任务数组,所有的任务为成功这个对象就成功,有一个失败这个对象的状态就为失败
Promise.any() 返回一个promise对象,传入一个任务数组,有一个成功即为成功,所有任务失败这个对象就失败
Promise.allSettled() 返回一个promise对象,传入一个任务数组,所有任务的状态从未决阶段已决阶段,这个对象就成功,不然这个对象就是pending
Promise.race() 传入一个数组,返回第一个变为已决阶段的任务

async和await

概述

有了Promise异步任务有了一种统一的处理方式,有了统一的方法ES官方进行了进一步的优化,ES2016提出了两个关键字asyncawait,用于更好的表示一个promise

async

这个关键字用于修饰一个函数,但凡是使用async修饰的函数那么这个函数一定会返回一个promise。如果返回值不是Promise就会被隐式包装成Promise

如果函数中的代码没有报错,返回的就是成功状态的promise,相关数据就是return的值。

async function demo() {
  return "demo";
}
demo();         // Promise { 'demo' }

// 上面的写法香相当于
function demo1() {
    return new Promise(resolve => {
        resolve("demo");
    })
}

// 当然也可以标记立即执行函数
(async function () {}())
(async () => {})()

有错误则返回一个失败状态的promise,相关数据就是报错的信息。如下

async function demo() {
  throw new Error("代码出错了");
}
demo();         //Promise {<rejected> Error: 代码出错了}

如果一个被async标记的函数里面return new Promise()的话,照上面的逻辑,函数是不是应该返回一个Promise然后相关的数据是这个函数里面return的Promise,像这样Promise {Promise}。但其实不然,当一个函数用async标记了return的是一个Promise那么这个async就跟没有写一样,如下。

async function demo() {
    return Promise.resolve("demo");
}
demo();        // Promise {"demo"}

// 等价于
function demo() {
    return Promise.resolve("demo");
}
demo();        // Promise {"demo"}

await

这个关键字表示等待某一个Promise完成,必须在async函数中使用。完成之后会返回这个Promise的相关数据。如下

function buy() {
    return new Promise(resolve => {
        resolve(10);
    }) 
}

async function demo() {
    const result = await buy();
    console.log(result);
}
// 等价于
buy().then(data => console.log(data));

如果没有相关数据也可以写成下面这样

function buy() {
  return new Promise(resolve => {
      setTimeout(() => {
          resolve();
      }, 1000)
  }) 
}
async function demo() {
  await buy();
  console.log("执行完成")
}
demo();         // 执行完成

一个Promise任务可能会有成功,也可能有失败,那么怎么处理失败呢,失败await他会抛出一个错误,可以使用try...catch,如下。

function buy() {
  return new Promise((resolve, reject) => {
      if(Math.random() < 0.5) {
          resolve("成功");
      }else {
          reject("失败");
      }
  })
}

// 使用try...catch的方式处理成功或失败。
async function demo() {
  try{
      const result = await buy();
      console.log(result);
  }catch (error) {
      console.log(error);
  }
}
demo();

await不止可以以等待Promise,还可以等待其他数据,但是他会隐式封装成Promise。如下

async function demo() {
  const n = await 10;
  console.log(10);
}
demo();               // 10

// await 10    等价于    await Promise.resolve(10)

下载电影的完美调用方法

function download(name) {
    return new Promise((resolve, reject) => {
        // 模拟正在下载
        console.log(`正在下载《${name}》......`);
        // 模拟一秒钟后下载成功或是失败
        setTimeout(() => {
            if (Math.random() < 0.9) {
                resolve(`下载《${name}》成功\n`);
            } else {
                reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
            }
        }, 2000)
    })
}

// 需要下载的电影
let films = [
    '电影1', '电影2', '电影3', '电影4',
    '电影5', '电影6', '电影7', '电影8',
    '电影9', '电影10', '电影11', '电影12',
    '电影13', '电影14', '电影15', '电影16',
    '电影17', '电影18', '电影19', '电影20',
    '电影21', '电影22', '电影23', '电影24',
    '电影25', '电影26', '电影27', '电影28',
    '电影29', '电影30', '电影31', '电影32',
    '电影33', '电影34', '电影35', '电影36',
    '电影37', '电影38', '电影39', '电影40'
];

// 使用async标记立即执行函数,然后里面使用循环配合await
(async () => {
    for (let i = 0; i < films.length; i++) {
        try {
            const result = await download(films[i]);
            console.log(result);
        } catch (data) {
            console.log(data);
            break;
        }
    }
})()