了解问题并实现承诺和异步事物如何与循环一起工作

时间:2019-07-19 19:34:43

标签: node.js asynchronous ecmascript-6 promise callback

我正在学习节点,我有一段代码可以查询要传递给回调函数的db中的数据。但是我只想返回唯一成员。

这意味着我必须遍历查询结果,并将唯一成员推入数组,然后将该数组传递给cb函数。

问题是,输入也是我构建查询的数组。所以已经有一个循环。

我已经看过一些以前的带有promise,await等的SO线程。但是在像这里这样的循环中实现它们时遇到了一些麻烦。

static getDestination(envName, cb){
    var ans = new Array();
    var promises;
    DataBase.initDB((db) => {
        for (var i = 0; i < envName.length; i++){
            db.collection('Environment').find({'environmentName' : envName[i]}, (err, data) => {
                if (err) {
                    console.log('error getting data in getDestination()');
                    return;
                }
                data.toArray((err, content) => {
                    if (err) {
                        console.log('error converting cursor into array');
                        return;
                    }
                    else {
                        for (const doc of content){
                            if (!ans.includes(doc.destination)){
                                ans.push(doc.destination);
                            }
                            //cb(ans); 
                        }
                    }
                }); 
            });
        }
    })
    cb(ans); 
}

现在,回调只是console.log(ans),它正在打印出一个空数组[]。

如果我在最内层的循环中调用回调,它将返回适当的成员,但在循环进行时会被调用,如下所示:

[]
[ 'DEV' ]
[ 'DEV' ]
[ 'DEV', 'QA' ]

我只想获取输出的最后一行。详尽的解释也将是惊人的!

编辑:据我了解,我应该创建一个Promise数组,并且在循环的每次迭代中,我都应该使用该操作作为参数来创建一个新的Promise。然后在一切结束时,我可以打电话

Promise.all(promises).then(cb(ans));

但是我在理解应该创建承诺的交易上遇到了麻烦。因为如果我在最内层的循环中创建诺言,则甚至在第一个循环创建之前就调用了循环外的cb。

编辑:

exports.initDB = (cb) => {
console.log('Connecting...');
const uri = <mongo connection uri here>;
const client = new MongoClient(uri, { useNewUrlParser: true });
client.connect(err => {
    if (err) {
        console.log('eror connecting to db');
        console.log(err);
        return;
    }
    cb(client.db(process.env.DB_NAME));
});

}

1 个答案:

答案 0 :(得分:1)

回调

在这里,您将创建承诺数组并使用它来汇总ans,然后调用一次cb()。我还建议使用cb(error, result)的{​​{3}}而不是cb(result),因为有多个步骤可能会出错,并且应该在查询成功或失败时通知getDestination()的调用方当它失败时。

static getDestination (envName, cb) {
  DataBase.initDB(db => {
    const envCollection = db.collection('Environment');
    const promises = envName.map(value => new Promise((resolve, reject) => {
      envCollection.find({ environmentName: value }, (error, data) => {
        if (error) return reject(error);

        data.toArray((error, content) => {
          if (error) reject(error);
          else resolve(content);
        });
      });
    }));

    Promise.all(promises).then(contents => {
      const ans = contents.reduce(
        (ans, content) => content.reduce(
          (ans, doc) => ans.add(doc.destination),
          ans
        ),
        new Set()
      );

      cb(null, [...ans]);
    }).catch(error => {
      cb(error);
    });
  });
}

请注意,每个the Node.js callback convention是同步构造的,并且resolve()reject()被异步调用。这是必需的,因为您的数据库查询尚未返回Promise

如果您使用的API确实会返回承诺,则应避免使用new Promise()并从现有的承诺中链接。

我还通过使用explicit promise construction antipattern而不是数组来优化了ans的聚合,因此实际上我们可以省略对现有值的检查,因为集合不像数组那样只保留每个唯一值中的一个。

承诺链

如果您希望getDestination()函数返回一个Promise而不是接受回调,则可以这样重写它:

static getDestination (envName) {
  return new Promise(resolve => {
    DataBase.initDB(resolve);
  }).then(db => {
    const envCollection = db.collection('Environment');
    const promises = envName.map(value => new Promise((resolve, reject) => {
      envCollection.find({ environmentName: value }, (error, data) => {
        if (error) return reject(error);

        data.toArray((error, content) => {
          if (error) reject(error);
          else resolve(content);
        });
      });
    }));

    return Promise.all(promises);
  }).then(contents => {
    const ans = contents.reduce(
      (ans, content) => content.reduce(
        (ans, doc) => ans.add(doc.destination),
        ans
      ),
      new Set()
    );

    return [...ans];
  });
}

主要区别在于:

  • DataBase.initDB()包装在另一个Promise中,因为结果取决于db
  • getDestination()返回承诺链
  • [...ans]返回.then(),而不是将其传递给回调
  • 删除.catch()并让呼叫者处理所有错误

请注意,由于我们是return Promises.all(promises)的{​​{1}},因此我们可以从外部承诺获得.then(),而不是链接到内部contents。这是承诺可以摆脱Set的主要好处之一。

异步/等待

最后,如果您想使用callback hell / async而不是Promise.all(),可以像这样再次重写它:

.then()

这里的主要区别是每个

static async getDestination (envName) {
  const db = await new Promise(resolve => {
    DataBase.initDB(resolve);
  });

  const envCollection = db.collection('Environment');
  const promises = envName.map(value => new Promise((resolve, reject) => {
    envCollection.find({ environmentName: value }, (error, data) => {
      if (error) return reject(error);

      data.toArray((error, content) => {
        if (error) reject(error);
        else resolve(content);
      });
    });
  }));

  const contents = await Promise.all(promises);
  const ans = contents.reduce(
    (ans, content) => content.reduce(
      (ans, doc) => ans.add(doc.destination),
      ans
    ),
    new Set()
  );

  return [...ans];
}

替换为

return somePromise.then(someVariable => { ... });

模块化

P.S。如果要从const someVariable = await somePromise; ... 函数中删除构造的promise,可以将代码重构为两个可以这样使用的辅助函数:

getDestination()

它可能比以前的代码要多一些,但是现在您有了两个可重用的函数,除了static getCollection (collectionName) { return new Promise(resolve => { DataBase.initDB(resolve); }).then( db => db.collection(collectionName) ); } static getDocuments (collection, ...args) { return new Promise((resolve, reject) => { collection.find(...args, (error, data) => { if (error) return reject(error); data.toArray((error, content) => { if (error) reject(error); else resolve(content); }); }); }); } static async getDestination (envName) { const envCollection = await this.getCollection('Environment'); const promises = envName.map( environmentName => this.getDocuments( envCollection, { environmentName } ) ); const contents = await Promise.all(promises); const ans = contents.reduce( (ans, content) => content.reduce( (ans, doc) => ans.add(doc.destination), ans ), new Set() ); return [...ans]; } 之外,还可以从其他类型的查询中调用它们。