多次请求因为网络无法保证响应顺序,如何保证获得所有响应结果后生成的列表结果的顺序与请求顺序一致?

cjwj
  • 628

看很多人都误会成了一次性发送10次,补充为这样:

假如用户可以连续点击按钮,每次发1次请求,每次获得响应结果后会生成一个li,因为网络状态的影响后发出的请求可能先响应,假如点了10次,如何保证10个li的顺序与请求的顺序一致!

使用Promise.all的答案肯定都是不对的,因为你无法预测用户点击按钮的时机,用户可能一两秒完成10次点击,但也可能在10秒内完成......肯定不能说我等用户点完10次再使用Promise.all请求,何况实际情况不一定是10次。
这是我一次面试唯一没答的很好的题目,所以印象比较深刻,我开始也答的Promise.all,被直接否定,后面我提到在请求报文中携带相关参数,响应中返回,本地做mapping关系来实现,但面试官依然不是很满意。后面经我询问后面试官只提到了思路,说了几个词记得不是很清楚,后续查资料猜测貌似是在请求响应包含一个Request-Id字段,并使用 UUID 作为该值......没实践过不敢确定!

回复
阅读 6k
14 个回答

Promise.all可以保证顺序但是得等到所有请求完毕才会触发

function p1(time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(time);
        }, time);
    })
}

Promise.all([p1(5000), p1(1000)]).then(function (res) {
    console.log(res);//[5000,1000]
});

不知道你说的是不是X-Request-ID,如果是,那我也不清楚怎么用这个header来区分不同的请求,除非是服务端配合。

我所想到的方法是使用一个自增的闭包变量来区分每个AJAX请求,这一点楼上很多人都想到了。

接下来,是如何在请求返回之后根据这个闭包变量的值在适当的位置插入<li>元素,以保证插入后的顺序与点击顺序相同(而不是与请求返回的顺序相同)。

我的方案是在请求的时候就append一个<li>元素,并设置为隐藏,同时给这个元素设置一个data,值为闭包变量的值。

当请求返回之后,根据闭包变量找到对应的<li>元素,插入内容后将其显示出来即可。类似于下面这样:

var idx = 0;

$(btn).on("click", function() {
    let i = idx++;
    $(...).append('<li class="hide" data-idx="`${i}`"></li>');
    $.ajax({
        ...
        success: function(resp) {
            var li = $("li[data-idx='`${i}`']");
            ...
        };
    });
});

在请求的时候带过去要显示的li的顺序;然后响应回来顺序;按顺序排列吧

一,可以把请求回来的数据做一个标识,然后把所有数据 都放到一个数组中,按标识 排序。
二,使用Promise.all,接收的是一个数组,等到数组中的请求全部完成,就执行Promise.all().then(values => {}),其中values就是一个数组,且排好序的

万无一失的方法是:
从第一个开始执行,等第一个执行结束后,再执行第二个,依次类推。
这种同步方法效率太低。
那么可以考虑Promise.all呢?
担心promise.all中的所有task都是异步执行的?
那么真正返回结果是依task列表顺序返回,所以就它了。

以下代码实现了:

  • 并发请求
  • 顺序操作
  • 不需要等待全部请求完毕
  • 可以直接运行
(()=>{
    Promise.allInOrder = (promises, thenForEach)=>{
        let sequence = Promise.resolve();
        
        promises.forEach(function(request){
            sequence = sequence.then(function(){
              return request.then(thenForEach);
          });
        });

    };
})();

// test
(()=>{
    let timeConsumingFunc = param=>new Promise(
            (resolve)=>{
                let timeout = Math.random() * 5000;
                console.log(`task ${param} will be resolved in ${timeout}ms`);
                setTimeout(()=>{
                    console.log(`${param} resolved`);
                    resolve(param+10);
                }, timeout);
            }
        );
    Promise
        .allInOrder(
            [timeConsumingFunc(1), timeConsumingFunc(2), timeConsumingFunc(3)],
            d=>{
                return new Promise(function(resolve) {
                  console.log(d);
                resolve();
              });
            }
        )
})();

这是我以前的一个提问的最终解决方法,也可以采用我采纳的哪个答案,只是代码会臃肿一些

补充:
针对你说的情况,可以间隔1s内的鼠标点击作为一组请求用上面方法并发请求,间隔大于1s的同步继发操作

在请求里带一个时间戳参数,然后再原封不动把这个时间戳返回来,并根据时间进行排序。

其实如果后天和前台能够联动,最好是请求时有一个标号,然后返回时带标号,这样肯定没有问题,且无论多少返回都可以先展示,后期再依序调整。
其实如果更友好的,发一次请求,就在数组中填一个展位符信息,接收到一个就更新数组,并刷新展示。

我的话可能会这么做:

function Ajax (option) {
    this.list = []
    let ajax = (data) => {
        let i = arr.length
        this.list[i] = '等待结果'
        $.ajax(option.url, {
            data:data
            // ...
            success: (res) => {
                this.list[i] = res
                option.success(res, i)
            }
        })
    }
    return ajax
}
let userAjax = new Ajax({url:xxx,method:xxx})
每次请求,直接userAjax(data),然后请求结果的顺序在list上固定好的,怎么响应影响不了

然后看你说响应里加字段,那就是后端控制了,你只需要对id进行排序,不过感觉跟你前面说的携带相关参数,响应中返回,本地做mapping关系来实现没什么区别啊,只不过一个前端生成uuid,一个后端生成uuid。

我觉得没有联系和必要性的话这样来标识是比较简单的。
ps:稍微改了下,动态点击。

var i = 0;
var list = [];
var count = 0;
var delay = 0;//如果希望连击可以更多的话加延时
var onallback = console.log;
var dopost = function() {
    $.post('', {}, (function(j, res) {
        count++;
        console.log(i,j,count);
        list[j] = res;
        setTimeout(function(){
          if (count === i) {
             count = i = 0;
             var tmp = list;
             list = [];
             onallback(tmp);
          }
        }, delay);
    }).bind(null, i++));
    list.length = i;
}

关于添加dom的顺序的话还是得学习下那些mvvm框架吧。搞个数组。

我觉得es6提到的Generator的异步应用中提到的Thunk函数可能解决你的这个问题。但是由于技术有限,仅仅只能提供可能实现的一个思路

es6的async函数可以解决这个问题,异步函数一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
http://es6.ruanyifeng.com/#do...

用伪代码说下怎么弄,具体有没有问题,谁试一下,不玩jq好多年了

let seq = 0
btn.click(li => {
  seq ++;
  ui_lis.append(<li>pending</li>) ;
  function(idx) {
      axios.get(url, param).then(rsp) {
        ui_lis[idx].content <<<< rsp.data
  }}(seq)
})

如果不是面试中,是实际项目中碰到类似问题,而且必须要保证返回顺续的话,因为有的时候第二次请求需要第一次的结果,我会采用当用户单击按钮以后,在请求没有回来之前,按钮变为禁用状态,只有成功返回了,才可以进行下一次请求,原因是你永远无法预测用户是猫还是狗,所以最好的办法就是不让用户有连续点击的机会

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏