我如何在没有 promise.defer 的情况下一起使用 promises 和循环?

How do I use promises and loops together without promise.defer?

我对 promises 还很陌生,我在避免一些我认为被描述为 promise 反模式(比如 Q.defer())的事情时遇到了问题。我有一个大部分是同步的循环,但有时可能需要进行异步调用。在我添加异步调用之前,代码非常简单,但为了让它与异步调用一起工作,我必须做出的更改非常混乱。

我想要一些关于如何重构我的代码的建议。该代码试图获取一个对象的选定属性并将它们添加到另一个对象。简化版如下:

function messyFunction(user, fieldArray) {
    return Promise.fcall(() => {
        var deferred = Promise.defer();

        if (!fieldArray) {
            deferred.resolve(user);
        } else {
            var temp = {};
            var fieldsMapped = 0;

            for (var i = 0; i < fieldArray.length; i++) {
                var field = fieldArray[i];
                if (field === 'specialValue') {
                    doSomethingAsync().then((result) => {
                        temp.specialField = result;
                        fieldsMapped++;
                        if (fieldsMapped === fieldArray.length) {
                            deferred.resolve(temp);
                        }
                    });
                } else {
                    temp[field] = user[field];
                    fieldsMapped++;
                    if (fieldsMapped === fieldArray.length) {
                        deferred.resolve(temp);
                    }
                }
            }
        }

        return deferred.promise;
    });
}

下面是我将如何重构它。我对q不是很熟悉,所以直接用原生的Promises,不过原理应该是一样的。

为了让事情更清楚,我对你的代码进行了一些去概括化,将 messyFunction 变成 getUserFields,异步计算年龄。

我没有使用 for 循环并使用计数器来跟踪收集了多少字段,而是将它们放在一个数组中,然后将其传递给 Promise.all。收集完字段的所有值后,Promise.all 承诺上的 then 将解析为新对象。

// obviously an age can be calculated synchronously, this is just an example
// of a function that might be asynchronous.
let getAge = (user) => {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve((new Date()).getFullYear() - user.birthYear);
    }, 100);
  });
};

function getUserFields(user, fieldArray) {
  if (!fieldArray) {
    // no field filtering/generating, just return the object as
    // a resolved promise.
    return Promise.resolve(user);
    // Note: If you want to make sure this is copy of the data,
    // not just a reference to the original object you could instead
    // use Object.assign to return it:
    //
    // return Promise.resolve(Object.assign({}, user));
  } else {
    // Each time we grab a field, we are either store it in the array right away
    // if it is synchronous, if it is asyncronous, create a thenable representing
    // its eventual value and store that.
    // This array will hold them so we can later pass them into Promise.all
    let fieldData = [];

    // loop over all the fields we want to collect
    fieldArray.forEach(fieldName => {
     // our special 'age' field doesn't exist on the object but can
     // be generated using the .birthYear
     if (fieldName === 'age') {
        // getAge returns a promise, we attach a then to it and store
        // that then in fieldData array
        let ageThenable = getAge(user).then((result) => {
          // returning a value inside of then will resolve this
          // then to that value
          return [fieldName, result];
        });
        
        // add our thenable to the promise array
        fieldData.push(ageThenable);
      } else {
        // if we don't need to do anything asyncronous, just add the field info
        // to the array
        fieldData.push([fieldName, user[fieldName]]);;
      }
    });
    
    // Promise.all will wait until all of the thenables to be resolved before
    // firing it's then. This means if none of the fields we were looking for
    // is asyncronous, it will fire right away. If some of them were
    // asyncronous, it will wait until they all return a value before firing.
    // We are returning the then, which will transform the collected data into
    // an object.
    return Promise.all(fieldData).then(fields => {
      // fields is an array of 2-element arrays containing the field name
      // and field value for each field we are collecting. We will loop over
      // this array with reduce and transform them into an object containing
      // all of the properties we collected.
      let newUserObj = fields.reduce((acc, [key, value]) => { 
        acc[key] = value;
        return acc;
      }, {});
      // Above I am using destructuring to get the key/value if the browsers
      // you are targeting don't support destructuring, you can instead just
      // give it a name as an array and then get the key/value from that array:
      //
      // let newUserObj = fields.reduce((acc, fieldData) => { 
      //   let key = fieldData[0],
      //     value = fieldData[1];
      //   acc[key] = value;
      //   return acc;
      // }, {});
      
      // return new object we created to resolve the then we returned
      return newUserObj;
    });
  }
}


let users = [
  {
    name: 'Tom',
    birthYear: 1986
  },
  {
    name: 'Dick',
    birthYear: 1976
  },
  {
    name: 'Harry',
    birthYear: 1997
  }
];

// generate a new user object with an age field
getUserFields(users[0], ['name', 'birthYear', 'age']).then(user => {
 console.dir(user);
});

// generate a new user object with just the name
getUserFields(users[1], ['name']).then(user => {
 console.dir(user);
});

// just get the user info with no filtering or generated properties
getUserFields(users[2]).then(user => {
 console.dir(user);
});