axios 回调不到 axios回调地狱_数组


目录:

一、axios与其他请求库的区别

二、axios的实现思路(干货)

三、你不知道的axios

四、思路借鉴

内容:

先贴上axios源码的地址,便于大家down下来阅读:https://github.com/axios/axios.git

一、axios与其他方法请求库的区别

一般而言用的比较多的是jQuery的ajax、fetch和axios这几个用于请求的库。

1、早期没有vue、react的时候我们都是使用的jQuery的ajax库,它的优缺点如下:

1)基于原生xhr,贴近底层,支持jsonp

2)为了使用ajax而引入jQuery库过于庞大

3)回调地狱问题

4)不太适用于现在比较流行的Vue、React等框架

2、fetch并不是基于原生xhr的,是ES6新的一个API

1)基于promise,解决了回调地狱的问题,写起来也更加简洁

2)也是偏底层,需要我们手动去封装一些东西,比如请求的返回,状态码的处理等

3)默认不带cookie,需要自己添加

4)兼容性还不是很好

3、request

1)仅作为服务端发起请求的工具

4、axios

axios的官方文档对它是这么介绍的

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止CSRF/XSRF

可以看到,axios的功能还是非常强大的,具备且不限于之前所有请求库的功能,下面我们就来具体讲讲axios是如何实现这些功能的吧!

二、axios的实现思路

axios作为一个小而精的请求库,写的非常清晰明了,大致实现思路如图所示,对照这张图我们再进行分析。


axios 回调不到 axios回调地狱_axios 回调不到_02


我们以一个最简单的请求为例,来看它是怎么走过这一生的。


import axios form 'axios'
axios.get('/api/getsomething')
.then((response)=>{
    console.log('response', response)
})
.catch((error)=>{
    console.log('error', error)
})


首先axios实例是从axios.js文件中导出的,那我们就先来看看这个入口文件吧。


// /lib/axios.js
// 重点方法  是用来生成axios实例的
function createInstance(defaultConfig) {
  // 传入default默认参数来创建一个Axios实例
  var context = new Axios(defaultConfig);
  // 使instance指向request方法,且上下文指向context
  // 这个request方法是核心方法,一会会讲到,基本上都是用它来发请求的
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  // 这个是把Axios.prototype上的方法扩展到instance对象上,使得我们可以使用axios.get等方法
  utils.extend(instance, Axios.prototype, context);
  // 把context对象上的自身属性和方法扩展到instance上
  // 这样instance 就有了 defaults、interceptors 属性。
  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// ...这里省略了一些代码。是用来给axios增加额外的方法的,比如取消请求、合并请求等,在第三部分会讲到

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;


核心在于Axios.prototype.request这个方法,那接下来我们就进入Axios.js这个核心文件来看看Axios到底是个什么东西吧。


// /lib/core/Axios.js
// Axios构造函数,有默认defaults设置以及两个拦截器,请求拦截器和响应拦截器,分别用来对请求和响应做一些处理。
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  // ...重点请求方法,后面单独讲
};

// 为支持的请求方法提供别名,这样我们就可以用axios.get等等方法来调用了
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});


到这里我们就明白了,axios那么多调用方式,其实最终都是调用的Axios.prototype.request这个核心方法,接下来我们就进入这个方法细细品味。


Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  // 如果用户传入的第一个参数是string类型的,即url,config就取第二个参数,第一个参数就是url,否则参数就是第一个参数,这里再回顾一下我们会给他传什么,axios.get('url',config).then(),是不是确实第一个参数是url?第二个参数可能就是data、header之类的配置项了
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 首先把默认配置和用户传入的config合并,当然,用户自身的配置优先级更高。然后改写config中的请求方法,如果有,改成小写的,没有就加个默认get
  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : 'get';


接下去就是整个axios我觉得比较复杂的部分了,也是它最精华核心的部分了,这里重点要理解一下chain数组存放的东西,它是用来盛放拦截器方法和dispatchRequest方法的,这两个东西很重要,一个是用来拦截请求和响应的,一个用来发送请求的, 通过promise从chain数组里按序取出回调函数逐一执行,最后将处理后的新的promise在Axios.prototype.request方法里返回出去, 并将response或error传送出去。最终我们才得到了想要的数据。(这里我们暂时先不管拦截器,后面在第三节我们单独来讲)


// Hook up interceptors middleware
  // 这里就是存放拦截器与发送请求的chain数组
  var chain = [dispatchRequest, undefined];
  // 先把promise的状态设为resolved,参数是处理过的config
  var promise = Promise.resolve(config);

  while (chain.length) {
  // 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
// 每次执行while循环,从chain数组里按序取出两项,并分别作为promise.then方法的第一个和第二个参数
// 这里先假设没有拦截器,我们就是直接调用了dispatchRequest这个方法
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};


那么这个dispatchRequest方法做了什么呢?和它的名字一样,它所做的事情就是发请求,具体怎么做的还是看代码。


// /lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // Ensure headers exist
  config.headers = config.headers || {};

  // 转换请求数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 合并header
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 删除header里没有用的属性
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 调用符合当前环境的请求适配器,一般用默认的即可,也可自己定义
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
  throwIfCancellationRequested(config);

  // Transform response data
  response.data = transformData(
    response.data,
    response.headers,
    config.transformResponse
  );

  return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};


其实很简单,把数据处理一下给adapter适配器进行请求,完了再对请求回来的数据进行一个转换后返回。

最后我们再进适配器看看,它是怎么处理我们的请求的。这里就看针对浏览器端的请求吧


// /lib/adapters/xhr.js
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ... 
  });
};


它用的是底层xhr发送的请求。返回的也是个promise,所以前面才可以拿到它的resolve和reject,xhrAdapter内的XHR发送请求成功后会执行这个Promise对象的resolve方法,并将请求的数据传出去, 反之则执行reject方法,并将错误信息作为参数传出去。

到现在整个axios的运行流程应该很清楚了,下一节我们再进行深入

三、你不知道的axios

上面的篇幅简单介绍了一个axios请求是如何一步步被执行并返回的全流程,下面更深层次介绍axios的一些方法,比如请求响应拦截、数据转换器、取消请求等

axios的亮点很多,我觉得写的很优美的一个还是要数拦截器的实现了。

先看看这个图,大概有个印象。


axios 回调不到 axios回调地狱_ios_03


我们先来回顾一下,拦截器在哪里首次出现的呢?是在Axios构造函数中。


// /lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;

  // 拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}


那我们进InterceptorManager这个构造函数中去看看。


// /lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];// 用来存放拦截器方法,数组内每一项都有两个属性,一个成功的一个失败的
}

// 我们使用的就是这个use方法,往handlers数组中加一个对象(成功+失败)
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 注销指定拦截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};


这个构造函数其实就三个方法,use、eject、forEach,都是操作拦截器里的handler这个数组的。

让我们看看拦截器的使用,就是上面说的use方法,可以看到确实是传了两个参数,一个成功的一个失败的


axios.interceptors.request.use(config => {
    // 在发送http请求之前做些什么
    return config; // 有且必须有一个config对象被返回
}, error => {
    // 对请求错误做些什么
    return Promise.reject(error);
});


后面在axios.prototype.request这个请求方法中拦截器再次出现并发挥了作用,我们重新来过一遍这个方法(这次是加上了拦截器的chain,看看有什么不一样)


// /lib/core/Axios.js
  Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];

  // 初始化一个参数为config的promise对象,状态为resolved
  var promise = Promise.resolve(config);

  // 将拦截器放进chain数组中
  // 这里注意一下请求拦截器是unshift方式入栈的,响应拦截器是push进去的,一个在头一个在尾
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    // 每次从chain数组里按序取出两项,作为promise.then方法的第一个和第二个参数
    // 这两个参数就是我们使用InterceptorManager.prototype.use方法添加的成功和失败回调
    // 注意一下请求拦截器是unshift进数组的,shift出来,所以请求拦截器是先添加的后执行
    // 而相应拦截器是push进数组的,shift出来,所以相应拦截器是先添加先执行
    // 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,
    // 而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,
    // 所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象

    // 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),
    // 而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,
    // 所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象

    // 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,
    // 所以dispatchRequest抛出的错误才会被响应拦截器接收到。

    // 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,
    // 而拦截器的执行顺序还是会按照我们上面说的顺序执行,
    // 也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,
    // 响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。

    promise = promise.then(chain.shift(), chain.shift());

  }

  return promise;
};


现在看看这个图是不是就很清晰了?


axios 回调不到 axios回调地狱_ios_03


接下来我们再讲一讲数据转换器

使用:


1、全局转换器添加
// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
  // ...处理数据
  return data;
});

// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
  // ...处理数据
  return data;
}];

2、 修改某次axios请求的转换器
axios.get(url, {
  // ...
  transformRequest: [
    ...axios.defaults.transformRequest, 
    (data, headers) => {
      // ...处理数据
      return data;
    }
  ]
})


数据转换器在default文件中有定义,这个default之前我们讲过,是和用户的config合并在一起传给axios的参数,也就是axios默认的参数


// /lib/defaults.js
var defaults = {

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    // ...
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

};


可以看到转换器的作用其实就是转换数据,根据请求和响应传过来的参数进行转换,返给用户。

下面再讲讲取消请求

其实axios底层使用的是xhr,这就给axios可以取消请求留下了可能

使用:


// 第一种取消方法
axios.get(url, {
  cancelToken: new axios.CancelToken(cancel => {
    if (/* 取消条件 */) {
      cancel('取消请求');
    }
  })
});

// 第二种取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
  cancelToken: source.token
});
source.cancel('取消请求');


如何实现的


// /cancel/CancelToken.js  -  11行
function CancelToken(executor) {

  var resolvePromise;
  // 设置一个状态为pending的promise
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }
    token.reason = new Cancel(message);
    // 将promise的状态设为resolve
    resolvePromise(token.reason);
  });
}

// /lib/adapters/xhr.js  -  159行
if (config.cancelToken) {
    // 这里将promise设为reject,取消后面的请求
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        request.abort();
        reject(cancel);
        request = null;
    });
}


看到这应该知道其实两种方法最后调用的都是cancelToken里面的executor函数,将canceltoken的promise设为resolved,传进最后调用的xhr中,将整个promise链设为reject,取消请求。

四、思路借鉴

读完了整个axios的源码之后,对我们平时写代码有什么可以借鉴之处呢?

1、拦截器的思路实现

将请求拦截器和响应拦截器分别放在chain数组的两端,中间是发送请求的方法,一步一步成对执行,将这么多promise进行串联,非常巧妙。

2、Adapter的处理逻辑

适配器是在自身的配置中默认引用,并根据环境自动选择,还可根据用户自行配置,降低耦合,且给用户留了口子,很人性化。

3、请求响应转换器

自动根据数据类型转换数据,不用用户手动转换,非常棒,用户自行也可以设置,人性化。

4、取消HTTP请求的处理逻辑

在取消HTTP请求的逻辑中,axios巧妙的使用了一个Promise来作为触发器,将resolve函数通过callback中参数的形式传递到了外部。这样既能够保证内部逻辑的连贯性,也能够保证在需要进行取消请求时,不需要直接进行相关类的示例数据改动,最大程度上避免了侵入其他的模块。

参考文章:

https://github.com/axios/axios

https://github.com/ronffy/axios-tutorial