thief系列之四:从实现getJSON中探索XHR和promise

@youngwind 2016-05-18 21:44:58发表于 youngwind/blog JQuery

问题

我们使用jquery的时候,经常用到的是$.ajax,可以发各种异步请求。不过这次我们先来实现一个更简单的方法,$.getJSON:向服务器发起一个get请求,获取相应的json数据。

分析

我们先来分析一下有哪几个方面需要注意:

  1. 调用getJSON的方式是T.getJSON,而非T('selector').getJSON,所以,getJSON方法需要直接定义在T的属性上,不能定义在T的原型属性上。
  2. 需要了解http协议的基本知识 #72
  3. 如何构造和使用XHR对象
  4. promise的运用

XMLHttpRequest

一个简单的XHR demo

// 创建一个请求对象
var client = new XMLHttpRequest();

// 启动请求(注意,此处请求仍未发送,只是准备好而已)
client.open('get','https://api.github.com/users/youngwind/starred');

// 发送请求
client.send();

我们如何知道监控请求的进度,知道它什么时候完成呢?xhr对象有个readyState属性,每当请求阶段改变的时候,readyState都会发生改变,从而触发readystatechange事件。代码如下:

T.getJSON = function (url, cb) {
    // 创建一个请求对象
    var client = new XMLHttpRequest();

   // 下面几行代码的顺序很重要
   // 必须是先open,然后各种set,最后是send,否则会报错
    client.open('get', url);
    client.setRequestHeader("Accept", "application/json");
    client.responseType = 'json';
    client.send();

    client.onreadystatechange = function () {
      if (this.readyState === 4) {
        if (this.status >= 200 && this.status < 300 || xhr.status == 304) {
          cb && cb(this.response);
        } else {
          console.log('error', this.status);
        }
      }
    }
  };

promise

ok,目前为止,我们已经成功实现:创建一个xhr对象,向服务器请求json数据,请求成功后传给回调函数进行处理。但是,问题在于,用callback的方法进行异步回调的处理太low了吧。
jquery在1.5.0版本之后开始引入deferred对象用于解决回调问题,可以参考这里。解决回调的方法有很多,比如 #49 #62 , 不妨我们用promise实现一个吧,因为
ES6标准都已经原生支持promise了

一个promise的简单demo

var promise = new Promise(function(resolve, reject){
  if(/*异步操作成功*/) {
     resolve(value);
  } else {
     reject(error);
  }
})

所以,我们需要将T.getJSON封装成返回一个promise对象,这样就能用.then了。

T.getJSON = function (url) {
    var promise = new Promise(function (resolve, reject) {
      // 创建一个请求对象
      var client = new XMLHttpRequest();

      client.open('get', url);
      client.setRequestHeader("Accept", "application/json");
      client.responseType = 'json';
      client.send();

      client.onreadystatechange = function () {
        if (this.readyState === 4) {
          if (this.status >= 200 && this.status < 300 || xhr.status == 304) {
            resolve(this.response);
          } else {
            reject(this.response);
          }
        }
      }
    });

    return promise;

  };

使用例子:

T.getJSON('https://api.github.com/users/youngwind/starred')
.then(function(res){
   console.log(res);
});

promise.all与promise.race

如果我们有这样的需求:请求A、B、C都完成之后再调用回调函数,又或者这样的需求:请求A、B、C任意一个率先完成都会调用回调函数,那该怎么处理呢?这种情况使用promise.allpromise.race可以轻松解决。

/**
   * 请求json数据
   * @param url {string}/{Array} 请求url(数组)
   * @param isAll {boolean} 如果url是数组,那么此状态位有用,默认为true
   *                        如果为true,则所有请求都结束之后再返回,传递给resolve的数据是所有请求返回数据构成的数组
   *                        如果为false,则任一请求结束之后都会返回,传递给resolve的数据是那个率先完成的请求返回的数据
   * @returns {Promise}
   */
  T.getJSON = function (url, isAll) {

    if (!Array.isArray(url)) {
      return generatePromiseXHR(url);
    }

    var promises = url.map(function (subUrl) {
      return generatePromiseXHR(subUrl);
    });

    if (isAll) {
      return Promise.all(promises);
    } else {
      return Promise.race(promises);
    }


    function generatePromiseXHR(url) {
      var promise = new Promise(function (resolve, reject) {
        // 创建一个请求对象
        var client = new XMLHttpRequest();

        client.open('get', url);
        client.setRequestHeader("Accept", "application/json");
        client.responseType = 'json';
        client.send();

        client.onreadystatechange = function () {
          if (this.readyState === 4) {
            if (this.status >= 200 && this.status < 300 || xhr.status == 304) {
              resolve(this.response);
            } else {
              reject(this.response);
            }
          }
        };
      });
      return promise;
    }

  };

参考链接:
http://es6.ruanyifeng.com/#docs/promise