AngularJS $q.all

106

我已经在AngularJS中实现了$q.all,但是我无法使代码正常工作。这是我的代码:

UploadService.uploadQuestion = function(questions){

        var promises = [];

        for(var i = 0 ; i < questions.length ; i++){

            var deffered  = $q.defer();
            var question  = questions[i]; 

            $http({

                url   : 'upload/question',
                method: 'POST',
                data  : question
            }).
            success(function(data){
                deffered.resolve(data);
            }).
            error(function(error){
                deffered.reject();
            });

            promises.push(deffered.promise);
        }

        return $q.all(promises);
    }

这是我的控制器,它调用了服务:

uploadService.uploadQuestion(questions).then(function(datas){

   //the datas can not be retrieved although the server has responded    
}, 
function(errors){ 
   //errors can not be retrieved also

})

我认为我的服务中设置$q.all存在一些问题。


1
你看到了什么行为?它是否调用了你的then(datas)? 尝试只使用push: promises.push(deffered); - Davin Tryon
@themyth92,你试过我的解决方案了吗? - Ilan Frumer
我已经尝试过两种方法,都在我的情况下起作用。但我会将@Llan Frumer作为正确答案。非常感谢你们俩。 - themyth92
1
为什么要将现有的promise再次封装成promise?$http已经返回了一个promise。使用$q.defer是多余的。 - Pete Alvin
2
它是 deferred 而不是 deffered :) - Christophe Roussy
3个回答

225
在 JavaScript 中,不存在{block-level scopes},只有{function-level scopes}:
阅读这篇关于JavaScript Scoping and Hoisting的文章。
看看我如何调试您的代码:
var deferred = $q.defer();
deferred.count = i;

console.log(deferred.count); // 0,1,2,3,4,5 --< all deferred objects

// some code

.success(function(data){
   console.log(deferred.count); // 5,5,5,5,5,5 --< only the last deferred object
   deferred.resolve(data);
})
  • 当你在 for 循环中写入 var deferred= $q.defer(); 时,它会被提升(hoisted)到函数的顶部,这意味着 javascript 在 for 循环 之外的函数作用域声明了这个变量。
  • 在每次循环中,最后一个 deferred 会覆盖上一个,没有块级作用域来保存对该对象的引用。
  • 当异步回调(成功/错误)被调用时,它们只引用最后一个 deferred 对象,因此 $q.all 永远不会被解决,因为它仍然在等待其他延迟对象。
  • 你需要为你迭代的每个项目创建一个匿名函数。
  • 由于函数确实有作用域,在函数执行后,闭包作用域中保留了对延迟对象的引用。
  • 正如 #dfsq 的评论所说: 不需要手动构造一个新的延迟对象,因为 $http 本身返回一个 promise。

使用 angular.forEach 的解决方案:

这里有一个演示plunker:http://plnkr.co/edit/NGMp4ycmaCqVOmgohN53?p=preview
UploadService.uploadQuestion = function(questions){

    var promises = [];

    angular.forEach(questions , function(question) {

        var promise = $http({
            url   : 'upload/question',
            method: 'POST',
            data  : question
        });

        promises.push(promise);

    });

    return $q.all(promises);
}

我最喜欢的方法是使用Array#map

这里有一个演示plunker: http://plnkr.co/edit/KYeTWUyxJR4mlU77svw9?p=preview

UploadService.uploadQuestion = function(questions){

    var promises = questions.map(function(question) {

        return $http({
            url   : 'upload/question',
            method: 'POST',
            data  : question
        });

    });

    return $q.all(promises);
}

14
好的回答。补充一点:不需要构建新的延迟(deferred),因为$http本身就返回一个promise对象,所以可以更短: http://plnkr.co/edit/V3gh7Roez8WWl4NKKrqM?p=preview。 - dfsq
4
喜欢使用map构建一个promise数组的方式。非常简单明了。 - Drumbeg
传奇答案!感谢您的解释,我可以看到由于您直接将 $http 调用返回的 promise 推入您的 promise 数组中,您不再需要一个单独的作用域来保存对不必要的中间 deferred 的引用。这意味着 OP 理论上可以回到使用简单的 for 循环,尽管我必须承认,在这种情况下使用“map”似乎是创建 promise 数组最优雅的方式。 - Niko Bellic
很好的答案。假设这不是POST而是GET方法,你如何迭代承诺以获取属于每个承诺的数据? - Tisha
2
需要注意的是,声明会被提升,但赋值仍然保持在原地。此外,使用 'let' 语句现在可以实现块级作用域。请参阅 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let。 - Spencer
显示剩余2条评论

37

$http也是一个Promise,你可以让它更简单:

return $q.all(tasks.map(function(d){
        return $http.post('upload/tasks',d).then(someProcessCallback, onErrorCallback);
    }));

2
是的,被接受的答案只是一堆反模式的聚合。 - Roamer-1888
我认为你可以省略.then()子句,因为OP想在控制器中完成所有操作,但原则是完全正确的。 - Roamer-1888
2
当然你可以省略then子句,我添加它是为了记录所有HTTP请求,我总是添加它们并使用全局的onError回调来处理所有服务器异常。 - Zerkotin
1
@Zerkotin,你可以在.then中使用throw,以便稍后处理它并将其暴露给$exceptionHandler,这应该可以为你节省麻烦和全局变量。 - Benjamin Gruenbaum
不错。这本质上与被接受的答案的最后一个解决方案/示例相同。 - Niko Bellic
.then 会为 e 的每个 promise 触发吗?我该如何让 someCallback 只在所有 promises 都返回时触发?将其添加到 $q.all(promises).then(someCallBack) 中吗? - Jimenemex

12
问题似乎在于您正在添加 deffered.promise,而您应该添加的是 deferred 本身作为 promise :
尝试更改为 promises.push(deffered);,这样您就不会将未包装的 promise 添加到数组中了。
 UploadService.uploadQuestion = function(questions){

            var promises = [];

            for(var i = 0 ; i < questions.length ; i++){

                var deffered  = $q.defer();
                var question  = questions[i]; 

                $http({

                    url   : 'upload/question',
                    method: 'POST',
                    data  : question
                }).
                success(function(data){
                    deffered.resolve(data);
                }).
                error(function(error){
                    deffered.reject();
                });

                promises.push(deffered);
            }

            return $q.all(promises);
        }

这只返回一个延迟对象数组,我已经检查过了。 - Ilan Frumer
我不知道他在说什么,只知道控制台上显示的信息。你可以看到它没有起作用:http://plnkr.co/edit/J1ErNncNsclf3aU86D7Z?p=preview - Ilan Frumer
4
文档明确说明$q.all接受的是Promise对象而不是Deferred对象。OP遇到的真正问题是作用域的问题,只有最后一个Deferred对象被解析(resolve)了。 - Ilan Frumer
Ilan,感谢您梳理defer对象和promises的关系。您也解决了我的all()问题。 - Ross Rogers
问题在两个答案中得到了解决,这个问题是作用域或变量提升,无论你想怎么称呼它。 - Zerkotin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接