如何使用Node.js中的异步功能与数据库进行交互


到目前为止,在本系列异步文章中,我们已经介绍了Node.js样式回调,Async模块和promises。在本系列的最后一部分中,我们将学习异步功能(AKA async / await)。对我而言,自Ajax以来,异步功能是JavaScript发生的最激动人心的事情。最终,我们可以以同步方式读取JavaScript代码,而它却始终像以前一样异步执行。

异步功能概述 异步功能是JavaScript的一项相对较新的功能(并非特定于Node.js)。通过对V8 JavaScript引擎的更新,该功能的支持首先在Node.js v7.6中实现。由于异步功能严重依赖于Promises,因此建议您先阅读上一篇文章,然后再继续。

我喜欢将异步功能分为两部分:异步和等待。让我们依次看一下每个部分。

async 在最长的时间内,我们已经能够使用函数语句(必须命名)或函数表达式(通常是匿名的)在JavaScript中创建函数。

function getNumber() { // Function statment
  return 42;
}

​
let logNumber = function() { // Function expression
  console.log(getNumber());
}
​
logNumber(); // 42

如果您在Node.js中运行上面的脚本,则应该看到已 42 打印到控制台。

JavaScript现在具有这些构造的异步副本。async 在函数语句或表达式之前放置new 关键字将返回AsyncFunction(异步函数)对象

async function getNumber() { // Async function statment
  return 42;
}
​

let logNumber = async function() { // Async function expression
  console.log(getNumber());
}
​
logNumber(); // Promise { 42 }

在Node.js中运行此脚本应打印Promise { 42 }。如您所见,当异步函数被调用时,它们返回的是promise,而不是返回的实际值!

为了使基于异步的脚本在功能上等同于第一个脚本,我们必须按如下所示对其进行重写。

async function getNumber() { // Async function statment
  return 42;
}

let logNumber = async function() { // Async function expression
  getNumber() // returns a promise
    .then(function(value) {
      console.log(value);
    });
}

logNumber(); // 42

现在我们回到记录值 42。

就像我们在前一篇博文中看到的Promise链所看到的那样,如果async函数正确无误地完成,那么它返回的Promise就会得到解决。如果函数返回一个值,则该值将成为promise的值。如果引发了错误并且无法处理错误,则承诺将被拒绝,错误将成为承诺的价值。

尽管很有趣,但返回诺言并不是使异步函数变得特别的原因。毕竟,我们可以只返回常规函数中的promise。使异步功能与众不同的是 await。

await await 操作符是发生魔术的地方,该 操作符仅在异步函数内部可用。这就像点击代码上的“暂停”按钮,以便它可以在继续执行之前等待承诺被解决或拒绝。这是一个称为协程的概念。自引入生成器函数以来,协程已在JavaScript中可用,但异步函数使它们更易于使用。

等待不会阻塞主线程。取而代之的await是,允许当前运行的调用堆栈(直到) 结束,以便可以执行回调队列中的其他功能。当承诺被解决或被拒绝时,代码的其余部分将排队等待执行。如果承诺被解决,则返回其值。如果承诺被拒绝,则拒绝的值将被抛出到主线程上。

下面是一个演示 await 使用 setTimeout 模拟异步API。我添加了一些其他控制台输出,以帮助说明正在发生的事情。

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;

      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumber() {
  let number;

  console.log('before await', number);

  number = await getRandomNumber();

  console.log('after await', number);
}

console.log('before async call');

logNumber();

当此脚本在Node.js中运行而没有发生错误时,输出将类似于以下内容(我在发生两秒钟延迟的位置添加了注释)。

before async call
before await undefined
after async call
# 2 second delay

请注意,after async call 在等待0.22454453163016597之后才记录。异步功能中只有剩余的代码被暂停;调用堆栈中其余的同步代码将完成。

如果抛出错误,您将看到UnhandledPromiseRejectionWarning 我们在上一篇 文章中介绍的内容。拒绝可以使用该文章中提到的方法或使用 try…catch!

try…catch 在本系列的第一篇文章中,我解释了为什么 try…catch 块不能与异步操作一起使用–您无法捕获在当前调用堆栈之外发生的错误。但是现在我们有了异步功能, try…catch 可以用于异步操作了!

这是先前脚本的精简版本,可捕获异步API中发生的错误,并改用默认值。

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
​

      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumber() {
  let number;

  try {
    number = await getRandomNumber();
  } catch (err) {

    number = 42;
  }

  console.log(number);
}

logNumber();

如果您运行该脚本足够的时间,您最终将获得 42 输出。 try…catch 再次工作,呜呼!

异步循环 除了能够try…catch 再次使用 块之外,我们还可以执行异步循环!在下面的示例中,我使用了一个简单的 for 循环,该循环连续注销三个值。

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;

      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumbers() {
  for (let x = 0; x < 3; x += 1) {
    console.log(await getRandomNumber());
  }
}

logNumbers();

在Node.js中运行此脚本,您应该每两秒钟看到三个数字打印到控制台。没有第三方库,没有复杂的承诺链,只有一个简单的循环。循环再次起作用,是的!

并行执行 显然,异步函数使执行顺序流和将标准JavaScript构造与异步操作结合使用变得容易。但是并行流又如何呢?这是 Promise.all 和 Promise.race 派上用场。因为它们都返回promise, await 所以可以像使用任何其他基于promise的API一样使用它们。

这是一个使用Promise.all并行获取三个随机数的示例。

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
​
      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}
​

async function logNumbers() {
  let promises = [];
​
19
  promises[0] = getRandomNumber();
  promises[1] = getRandomNumber();
  promises[2] = getRandomNumber();

  Promise.all(promises)
    .then(function(values) {
      console.log(values);
    })
    .catch(function(err) {
      console.log(err);
    });
}

由于 Promise.all 如果拒绝任何传入的承诺,则拒绝其承诺,因此您可能需要运行脚本几次才能看到打印出的三个随机数。

异步功能演示应用 异步功能演示应用程序由以下四个文件组成。这些文件也可以通过此Gist获得。

package.json:

{
  "name": "async-functions",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Dan McGhan <dan.mcghan@oracle.com> (https://jsao.io/)",
  "license": "ISC",
  "dependencies": {
    "oracledb": "^1.13.1"
  }
}

这是一个非常基本的 package.json 文件。唯一的外部依赖关系是oracledb。

index.js:

const oracledb = require('oracledb');        
const dbConfig = require('./db-config.js');      
const employees = require('./employees.js');

async function startApp() {
  try {
    await oracledb.createPool(dbConfig);

    let emp = await employees.getEmployee(101);

    console.log(emp);
  } catch (err) {
    console.log('Opps, an error occurred', err);
  }
}


startApp();

node-oracledb中的所有异步方法都被重载以与回调函数或promises一起使用。如果没有将回调函数作为最后一个参数传递,则将返回promise。此版本的版本index.js 使用await 操作员和驱动程序的promise API来创建连接池并获取员工。尽管该池是从调用返回的 createPool,但此处未引用它,因为将在中使用 内置池缓存employees.js。

db-config.js:

module.exports = {
  user: 'hr',
  password: 'oracle',
  connectString: 'localhost:1521/orcl',
  poolMax: 20,
  poolMin: 20,
  poolIncrement: 0
};

db-config.js文件用于 index.js 提供数据库的连接信息。此配置应与DB App Dev VM一起使用,但需要针对其他环境进行调整。

employees.js:

const oracledb = require('oracledb');

function getEmployee(empId) {
  return new Promise(async function(resolve, reject) {
    let conn; // Declared here for scoping purposes.

    try {
      conn = await oracledb.getConnection();

      console.log('Connected to database');

      let result = await conn.execute(
        `select *
        from employees
        where employee_id = :emp_id`,
        [empId],
        {
          outFormat: oracledb.OBJECT
        }
      );
​

      console.log('Query executed');
​

      resolve(result.rows[0]);
    } catch (err) {
      console.log('Error occurred', err);
​
      reject(err);
    } finally {
      // If conn assignment worked, need to close.
      if (conn) {
        try {
          await conn.close();
​
          console.log('Connection closed');
        } catch (err) {
          console.log('Error closing connection', err);
        }
      }
    }
  });
}

员工模块的此版本与Promise版本相似,因为该 getEmployee 函数被编写为基于Promise的API-它立即返回一个新的Promise实例,该实例被异步解析或拒绝。主要区别是 await 与驱动程序的promise API一起使用来获得与数据库的连接,使用它执行查询,然后关闭连接。

使用一个 try…catch…finally 块来捕获错误并确保以任何一种方式关闭连接。对我来说,该模块的版本是该系列中所有模块中最容易阅读的,它也具有最少的代码行,这也没有什么坏处。

希望您现在对异步函数有了更好的了解,并像我对使用异步函数一样感到兴奋!


原文链接:https://codingdict.com/