我对承诺相当陌生,而且我遇到了一些问题,避免了我认为描述为承诺反模式的一些事情(比如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;
});
}
答案 0 :(得分:0)
以下是我如何重构它。我对q
并不是很熟悉,所以我只使用原生的Promises,但主体应该是相同的。
为了让事情变得更清晰,我已经将您的代码反省一点,将messyFunction
转换为getUserFields
,异步计算年龄。
我没有使用for
循环并使用计数器来跟踪已收集的字段数,而是将它们放入一个数组中,然后将其传递给Promise.all
。收集完所有字段值后,then
承诺上的Promise.all
会解析为新对象。
// 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);
});