web前端: 解決“先發起請求後收到響應”問題

前言: 當我們發送請求時,由於請求是異步的,前一次發起的請求不會阻塞後一次請求的發起,順理成章地,前一次請求也未必會比後一次請求先返回。於是導致的直接後果就是後一次請求響應的數據可能先渲染,待前一次請求響應時,直接覆蓋了後一次請求的渲染結果。這可不是我們所期望看到的。

舉例

前言中所描述的問題,用代碼來表示的話,就是:

var result;

// A function to simulate async request delay
var request = function (msec, mockedData, callback) {
  setTimeout(function () {
    callback(mockedData);
  }, msec);
};

var sample = [
  { msec: 200, data: 'stale' },
  { msec: 100, data: 'fresh' }
];

// 模擬`先發起請求後響應`的情景
sample.forEach(function (item) {
  request(item.msec, item.data, function (resp) {
    result = resp;
  });
});

// wait 1s, we inspect the `result`
setTimeout(function () {
  // we expect `result` to be `fresh`, but it is `stale`
  console.log(result); // => `stale`
}, 1000);

代碼中後一次執行的異步操作覆蓋了前一次執行的,最後得到的result爲'stale',顯然是錯誤的。

解決方案

  • 第一種方法
    在前一次未返回結果時,禁止發送下一次請求,也就是把異步的請求強行改爲同步的,這顯然不是最好的解決方案。
  • 第二種方法
    在每次響應後且在渲染之前,判斷當前響應是不是對應最新一次請求的。是,則渲染;不是,則不渲染。
    這種思路最容易想到的實現就是使用全局變量標記最新請求,局部變量標記當前請求,然後在響應回調中判斷局部變量的值是否和全局變量的值一樣。如果不一樣,忽略響應結果;如果一樣,我們可以斷言這是最新請求的響應,直接渲染。
    以下是這種思路針對上面代碼的改寫:
var result;
var globalMark = 0;

// A function to simulate async request delay
var request = function (msec, mockedData, callback) {
  setTimeout(function () {
    callback(mockedData);
  }, msec);
};

var sample = [
  { msec: 200, data: 'stale' },
  { msec: 100, data: 'fresh' }
];

// 模擬`先發起請求後響應`的情景
sample.forEach(function (item) {
  var localMark = ++globalMark;

  request(item.msec, item.data, function (resp) {
    if (localMark !== globalMark) {
      return;
    }

    result = resp;
  });
});

// wait 1s, we inspect `result`
setTimeout(function () {
  // now our `result` is `fresh`, just as expected
  console.log(result); // => `fresh`
}, 1000);

因爲我們通常是用promise的異步請求來進行調用接口的,所以我們可以改爲promise的版本:

var result;
var globalMark = 0;


// 將異步函數轉換爲promise式
var promisify = function (fn) {
  // Is `fn` thenable?
  return fn.then ? fn : function () {
    var args = Array.from(arguments);
    return new Promise(resolve => void fn(...args.concat(resolve)));
  };
};

// 將request方法promise化

// A function to simulate async request delay
var request = function (msec, mockedData, callback) {
  setTimeout(function () {
    callback(mockedData);
  }, msec);
};

// decorate `request` with `promisify`
request = promisify(request);

var sample = [
  { msec: 200, data: 'stale' },
  { msec: 100, data: 'fresh' }
];

// 模擬`先發起請求後響應`的情景
sample.forEach(function (item) {
  var localMark = ++globalMark;

  request(item.msec, item.data).then(function (resp) {
    if (localMark !== globalMark) {
      return;
    }
    result = resp;
  });
});

// wait 1s, we inspect `result`
setTimeout(function () {
  // now our `result` is `fresh`, just as expected
  console.log(result); // => `fresh`
}, 1000);

這種思路可以正常工作,而且簡單直觀。但是當我們要處理大量這類請求問題時,這類重複邏輯的代碼將散落在各個地方,不是很優雅。問題不在於思路上,而在於實現。我們需要在源頭(請求)處規避(控制)問題,而不是到結果(響應)處解決問題。

  • 第三種方法
    我們可以對原有promisify後得到的方法再進行一層裝飾,在請求處就控制問題,而不是在得到結果後再進行解決問題。
var result;

// 將promise再經過一層處理
var mutePrior = function (promisifiedFunc) {
  var registry = [0];

  return function () {
    var promise = promisifiedFunc(...arguments);
    registry.push(promise);

    return new Promise(function (...actions) {
      var proxyCallbacks = actions.map(action => function (result) {
        if (registry.indexOf(promise) === registry.length - 1) {
          action(result);
          registry.length = 1;
        }
      });

      promise.then(...proxyCallbacks);
    });
  };
};

// 將異步函數轉換爲promise式
var promisify = function (fn) {
  // Is `fn` thenable?
  return fn.then ? fn : function () {
    var args = Array.from(arguments);
    return new Promise(resolve => void fn(...args.concat(resolve)));
  };
};

// 將request方法promise化

// A function to simulate async request delay
var request = function (msec, mockedData, callback) {
  setTimeout(function () {
    callback(mockedData);
  }, msec);
};

// decorate `request` with `promisify`
request = mutePrior(promisify(request));

var sample = [
  { msec: 200, data: 'stale' },
  { msec: 100, data: 'fresh' }
];

// 模擬`先發起請求後響應`的情景
sample.forEach(function (item) {
  request(item.msec, item.data).then(function (resp) {
    result = resp;
  });
});

// wait 1s, we inspect `result`
setTimeout(function () {
  // now our `result` is `fresh`, just as expected
  console.log(result); // => `fresh`
}, 1000);

這樣就可以在請求的源頭解決這個問題O(∩_∩)O哈哈~。

參考文章: http://myunlessor.github.io/blog/2015/12/19/promise-solve-continuous-request-and-respone-not-in-order-problem/
本文僅供交流學習使用,有疑問請與作者聯繫,如侵必刪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章