【异步Javascript】用一个例子搞清Promise->asycn&await编程

421 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

Udemy Node.js, Express, MongoDB & More: The Complete Bootcamp

同步和异步

同步,异步,阻塞,非阻塞等关系轻松理解

image.png

"同步模式": 只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

"异步模式": 每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。

在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

单线程

image.png 而PHP就会为每个用户分配一个线程

异步和回调

使用回调函数并不会使代码自动成为异步,只有在使用一些Node API(如readFile)时的回调是异步的。回调是拿到异步结果的一种方式;同时,回调也可以拿到同步的结果。 image.png

实现功能

步骤1 从本地dog.txt中获取狗品种(字段)

步骤2 利用superagent方法向某网站请求获取对应照片

步骤3 将获取到的照片地址写入本地dog-img.txt中

需要的模块:

const fs = require('fs'); //文件操作
const superagent = require('superagent'); //获取图片

1.先写一个Callback Hell版

fs.readFile('./dog.txt', 'utf-8', (err, data) => { //1
  if (err) console.log('Can not read find this file');
  superagent 
    .get(`https://dog.ceo/api/breed/${data}/images/random`)//2
    .then((res) => {
      console.log(res.body.message);
      fs.writeFile('./dog-img.txt', res.body.message, (err) => {//3
        if (err) return console.log('Can not write file');
        console.log(`Successfully get the dog image`);
      });
    });
});

层层嵌套,可读性差

2.利用Promise对象优化

如何定义Prmoise对象?

Promise对象是一个构造函数,我们使用时要new出它的实例。

接受一个函数作为参数。该函数有两个参数resolvereject,它们可以获取到异步操作的消息,即成功返回的value或失败返回的error,从而改变Promise实例对象的“状态”,状态改变后建议分别使用then(),catch()方法指定resolved状态和rejected状态的回调函数。

const promise = new Promise((resolve, reject) => {
  //...
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

如何使用Promise对象?

就本例来说,异步操作是文件的读写。这里不用变量接收Promise实例对象,而是直接将它作为返回值:

const readFilePro = (file) => {
  return new Promise((resolve, reject) => {
    fs.readFile(file, 'utf-8', (err, data) => {
      if (err) reject(`Could not find that file 🥲`);
      resolve(data);
    });
  });
};
const writeFilePro = (file, data) => {
  return new Promise((reslove, reject) => {
    fs.writeFile(file, data, err => {
      if (err) reject(`Could not write file 🥲`);
      reslove('Success');
    });
  });
};

这也是使用Promise解决callback hell的关键步骤:让函数返回一个Promise实例对象,这样就可以在其后链式调用then方法,即将异步操作以同步操作的流程表达出来。

readFilePro('./dog.txt')
  .then(data => {
    console.log(`${data}🐶`);//1
    return superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);
  })
  .then(response => {
    console.log(response.body.message);//2
    return writeFilePro('./dog-img.txt', response.body.message);
  })
  .then(() => {
    console.log(`Random dog image saved to file !`);//3
  })🐶
  .catch((err) => {
    console.log(err);
  });

3.asycn/await——Promise的语法糖

async关键字,该声明表示函数内部有异步操作,即表示它并不会阻塞Event Loop。并且,该函数会自动返回一个Promise对象,对,和我们之前自己写的函数一样。

当async函数执行的时候,一旦遇到await就会先暂停,直到其后Promise异步操作完成,再接着执行函数体内后面的语句。这使我们的代码看起来更像同步,可读性强,但本质上和方法2是一样的。等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者throw err

就本例,我们定义一个async函数getDogPic,注意用关键字声明:

const getDogPic = async () => {
  try {
    const data = await readFilePro(`./dog.txt`);//1
    //我们用变量data储存await表达式的值,也就是Promise成功后返回的参数。
    console.log(`Breed: ${data}`);

    const response = superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);//2
    
    await writeFilePro('./dog-img.txt', response.body.message);//3
    console.log('Random dog image saved to file!');
  } catch (err) {
    console.log(err);
    throw err;
  }
  return 'READY 🐶';
};

对“自动返回一个Promise对象”的理解:

//调用getDogPic函数并用x接收它的返回值
const x = getDogPic();
console.log(x)

//输出
Promise { <pending> }

如上,打印x,并不是我们返回的'READY 🐶'字符串,而是返回一个状态为resolved的 Promise 对象,且then方法接收到的参数为字符串‘READY 🐶’。如果我们想得到该字符串,如前文,可以用then方法接收Promise成功的参数,并设置回调:

getDogPic().then(x=>console.log(x))

//输出
'READY 🐶'

但这不是最好的写法。因为我们已经使用了async&await,又在这里用then方法,很混乱。更好的解决方法是使用另一个立即执行的async函数

(async () => {
  try {
    const x = await getDogPic();
    console.log(x);
  } catch (err) {
    console.log('ERROR 💥');
  }
})();

Promise.all

拓展一下功能,如果我们想同时获得三张狗狗的照片,那么需要定义三个Promise实例对象。使用多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发,这就要用到all方法。

    const res1Pro = superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);
    const res2Pro = ...
    const res3Pro = ...
    const all = await Promise.all([res1Pro, res2Pro, res3Pro]);
    const imgs = all.map(el => el.body.message);
    

控制台输出:

Snipaste_2022-04-24_00-09-50.png

参考资料

阮一峰 Promise 对象

Node.js, Express, MongoDB & More: The Complete Bootcamp P39-P45