Can this promise nesting be changed to chaining?

2019-07-20 10:18发布

问题:

This is the pseudo scenario

           | then (items)          | then (items, actions)
getItems() | getActions(for:items) | apply(actions -> items)
 :promise  |  :promise             | model <= items
           |                       |  :synchronous

So in words:

  1. I need to get a list of global items.
  2. Fine. Items fetched.
  3. Make a request for actions that user has taken over previously fetched items.
  4. Fine. Actions fetched.
  5. Apply actions to items list.
  6. Put items on the model and display in view.

This is somewhat the code I'm using

return itemsResource
       .getItems(userId)
       .$promise
       .then(function(items) {
           return actionsResource
                  .getActions(items.map(i => i.id)) // pseudo mapper code here
                  .$promise
                  .then(function(actions) { // THIS IS NESTED so it sees both promise results
                      return [items, actions];
                  });
       })
       .then(helper.spread(function(items, actions) {
           applyActions(items, actions);
           $scope.model.items = items;
           return items;
       }));

As you can understand I can't use $q.all initially because second (actions) promise depends on results of the first one (items).

Why not return actions with items? Because I'm caching items for all users, so item fetching is really quick. This is similar to how Stackoverflow works. They just return questions regardless of user requesting them. Then they subsequently also request for preferred and ignored tags and apply to fetched questions. This scales very well, otherwise SO would require many more servers to handle just initial list of questions for all users, because every user's request would yield different results, so caching wouldn't make much sense.

Why not apply items to $scope immediately after being fetched? This would eliminate the additional nested then, that's true. but I'm not doing that because there are few more steps afterwards and every time one of the promises gets resolved at least one $digest cycle is being executed. With many items (being rather complex objects) this can account for quite some processing. That's the reason I'm holding on to passing items to the view to the very last moment possible.

Question

Is there a way to avoid the nested then beside these two workarounds:

  1. Apply items to $scope as soon as possible
  2. Save items to local variable and use it afterwards

I'd like to flatten my code as much as possible without introduction of additional resources if possible?

回答1:

Unless I'm missing something, this should be fairly straightforward, no?

(I have simplified some of your internal function signatures for clarity)

itemsResource.getItems(userId)
  .then(function(items) {
    return $q.all({
      items: items,
      actions: actionResource.getActions(items)
    });
  })
  .then(function(data) {
    applyActions(data.items, data.actions);
    $scope.model.items = data.items;
    return data.items;
  });

plunker for illustration



回答2:

How about wrapping the results of the items->actions chain in a promise? Something like

return $q(function(resolve, reject) {

    var outerItems;

    itemsResource.getItems(userId).$promise
        .then(getActions)
        .then(function (actions) {
            resolve([outerItems, actions]);
        })
        .catch(function (err) { reject(err); });

    function getActions(items) {
        outerItems = items;
        return actionsResource.getActions(items).$promise
    }

}).then(function (itemAndActions) {
    var items = itemsAndActions[0], 
        actions = itemsAndActions[1];

    return helper.spread(function (items, actions) {
        applyActions(items, actions);
        $scope.model.items = items;
        return items;
    })
});