10分钟搞定axios源码

251 阅读8分钟

先看一张Axios的整体流程图!!! img

Axios方法的定义

function Axios(instanceConfig) {
  //接受默认的配置参数
  this.defaults = instanceConfig;
  //定义请求和响应的拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

我们在创建Axios对象时会传入一个默认的配置对象,将它赋值给内部的属性最后会和我们自己配置的参数进行合并。然后会定义请求和响应的拦截对象,用户可以手动去添加拦截函数。

Axios.prototype.request = function request(config) {
  //这里就是对参数进行预处理,因为axios传入参数的方式有好几种
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  //将我们配置的参数和原始参数进行合并
  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (thod) {
    config.methconfig.meod = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // chain数组用来保存对config或者result处理的函数,同时这些处理函数都是成对出现的,默认传入的是dispatchRequest和undefine,dispatchRequest内部会发起真正XML的请求。
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
//将用户添加的请求拦截函数放入chain中,此时放入的是队头,因为它要在请求发起前执行。
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
//将用户添加的响应拦截函数放入chain中,此时放入的是队尾,因为这是要对返回的数据进行处理。
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  //循环执行,最终promise拿到的就是最终的数据
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

Axios中的request方法其实就是做了三件事:

  • 对用户传进来的配置参数进行合并
  • 将用户配置的请求拦截和响应拦截添加到执行队列中
  • 执行请求拦截函数---->发起请求----->执行响应拦截函数
//将['delete', 'get', 'head', 'options']方法加入到Axios的原型中
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
    }));
  };
});
//将['post', 'put', 'patch']方法加入到Axios的原型中
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对象上的请求方法发起请求,比如axios.get('xxx', {}),其实最终调用还是我们的Axios上的request方法。

axios对象的创建

function createInstance(defaultConfig) {
  //创建axios对象,将默认配置参数传入
  var context = new Axios(defaultConfig);
  //通过bind方法重新创建一个函数,并向将Axios原型上的方法和属性复制给这个函数
  var instance = bind(Axios.prototype.request, context);
  utils.extend(instance, Axios.prototype, context);
  utils.extend(instance, context);
  return instance;
}
//创建请求方法,这个对象就是我们最终发送请求使用的方法,这一提醒下axios是一个函数。
var axios = createInstance(defaults);

createInstance函数的目的首先明确一下:它创建出来的是一个函数,我们可以直接调用这个函数或者原型上的请求方法发送请求,下面详细讲解下过程:

  • 创建一个axios对象,将我们的默认配置参数传入

  • 调用bind函数,创建一个新的函数,函数的返回值是执行Axios.request后的返回值,其实bind实现很简单:

    function bind(fn, thisArg){
    	return function wrap(...args){
    		return fn.apply(thisArg,args);
    	}
    }
    

    args就是我们发送请求时配置的参数,最终aegs会交给Axios.request函数,而之所以我们最后能使用axios.get()发送请求是因为我们将Axios原型上的方法复制给了wrap函数,也就是上面的extend函数。

    axios.Cancel = require('./cancel/Cancel');
    axios.CancelToken = require('./cancel/CancelToken');
    axios.isCancel = require('./cancel/isCancel');
    

    这一块就是向axios上添加取消请求的方法,让我们能够中断发送的请求,具体实现后面会介绍。

调用dispatchRequest发起请求

  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

首先是将我们传入的参数进行转换,axios.defaults.transformRequest 数组中默认就有一个函数,他会根据我们参数的类型进行不同的转换,比如我们将**{"name":"zhangsan","age":12}**作为参数传入,最终结果如下:

image-20210819220851881.png

他会被转换成JSON字符串。如果我们想自定义转换函数可以通过concat链接自定义的函数。

axios.get('xxxx', {
  transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {
    //....
    return data;
  })
})

transformData执行的时候就会遍历axios.defaults.transformRequest数组中的转换函数对数据进行转换,接下来就是发送请求。

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    //....对response进行处理
    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        //...对reason进行处理
      }
    }
    return Promise.reject(reason);
  });

这里最关键的其实就是调用adapter函数发送请求,然后对返回的数据进行处理,也就是说dispatchRequest函数最终返回的是一个Promise,我们可以通过调用其then方法拿到数据。

adapter方法

首先我们来追溯下dispatchRequest中调用的adapter是怎么拿到的。

var adapter = config.adapter || defaults.adapter;

看到这里就明白了,adapter是从我们的config中拿到的,默认情况下config上的adapter就是从默认配置中继承过来的,下面看下defaults.adapter是怎么的来的:

var defaults = {
	adapter: getDefaultAdapter(), 
    //.....
}

function getDefaultAdapter() {
  var adapter;
  //根据XMLHttpRequest判断当前是不是浏览器环境,如果不是就根据process判断是不是在node环境
  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    adapter = require('./adapters/http');
  }
  return adapter;
}

从getDefaultAdapter中我们就能知道axios是支持浏览器和node环境的,如果是在浏览器环境就引入XMLHttpRequest,如果是node环境就引入http,下面主要探究浏览器环境。

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    //...数据处理
    var request = new XMLHttpRequest();
	//...
    request.open();
    //设置超时时间
    request.timeout = config.timeout;
    request.onreadystatechange = function handleLoad() {
      //...处理返回的数据
      settle(resolve, reject, response);
      request = null;
    };
    //监听中断操作
    request.onabort = function handleAbort() {};
    //监听错误
    request.onerror = function handleError() {};
    //监听超时
    request.ontimeout = function handleTimeout() {};
    if (requestData === undefined) {
      requestData = null;
    }
    request.send(requestData);
  });
};

上面就是XML请求的大致过程,从上面我们可以知道axios内部可以进行错误处理请求拦截中断请求请求超时处理等功能,下面会介绍一下实现的其他功能。

withCredentials :

 if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
 }

了解过CORS的同学应该对withCredentials不陌生,这个属性是一个布尔值,用来设置在进行跨域请求时是否携带授权信息,比如cookie或授权header头。

监听上传和下载进度:

//监听下载进度    
if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
}
//监听上传进度
if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
}

progress是XML自带的一个事件,当请求接受到更多数据时,会周期性的触发,我们在定义config的时候可以自定义上传或者下载事件,然后通过event对象可以知道上传或者下载的进度。

axios.post(action, formData, {
   onUploadProgress: (e) => {
       //获取当前上体积与总体积
       let percentage = Math.round((e.loaded * 100) / e.total) || 0;
       if(percentage < 100) {
            //....进度条展示当前进度
       }
    }
})

中断请求:

img

首先看个例子,怎么去中断请求:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('xxxxx', {
  cancelToken: source.token
}).then((res) => {
    console.log(res)
}).catch((err) => {
  console.log(err);
});
source.cancel('中断请求');

从代码中可以知道我们首先需要给我们的请求配置上cancelToken这个属性,然后就可以在请求返回之前随时调用cancel方法中断请求,

下面一步一步来解析这个过程。

  • 获取CancelToken

    axios.CancelToken = require('./cancel/CancelToken');
    

    现在知道了,axios上的CancelToken就是从./cancel/CancelToken这个模块中导出来的。

    function CancelToken(executor) {
      var resolvePromise;
      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);
        resolvePromise(token.reason);
      });
    }
    

    原来CancelToken是一个方法,这个方法就是在this上绑定了一个promise,根据以往的经验这个this要么指向调用CancelToken的实例,要么指向new出来的新对象,暂时先放着。

    接着执行executor函数,这个函数里面传递了一个cancel方法,其实这个cancel方法就是用户调用的cancel方法。

    在cancel方法中我们看到它调用了promise的resolve方法将message抛出去,看到这里可以再去看下上面的流程图。

  • 执行CancelToken.source

    CancelToken.source = function source() {
      var cancel;
      var token = new CancelToken(function executor(c) {
        cancel = c;
      });
      return {
        token: token,
        cancel: cancel
      };
    };
    

    source方法首先帮我们new了一个新实例token,现在就知道了上面的this指向的就是这个新实例,也就是说token上有一个pending状态的promise,等待用户去触发cancel。

    这个函数返回的一个是token,这个token会作为config的CancelToken属性的值,另一个返回值就是cancel函数,也就是CancelToken中executor函数传递过来的参数。

  • 执行cancel函数

    我们现在知道执行cancel目的就是让CancelToken中的promise的状态从pendinig变为resolve状态,那这有啥用呢?因为我们生成的token最终会作为CancelToken属性的值传入,所以我们看看最终是怎么处理CancelToken属性的。

    if (config.cancelToken) {
          config.cancelToken.promise.then(function onCanceled(cancel) {
            if (!request) {
              return;
            }
    
            request.abort();
            reject(cancel);
            request = null;
          });
    }
    

    现在应该可以恍然大悟了,在执行xml请求时会执行这段代码,如果我们配置了CancelToken就会进入判断条件里面,里面就会调用cancelToken.promise.then,如果用户不调用cancel方法让cancelToken.promise从pending状态变为resolve状态,那么他的then方法就不会被执行,也就不会影响正常的请求。

    如果我们调用了cancel方法,那么就会执行then方法中的代码,我们看到,里面就触发了xml的abort函数,同时执行了reject函数,我们知道axios最终的返回值就是一个promise,这里的reject对应的就是这个promise。

总结

axios是一个非常强大的库,一般日常开发都会使用,同时里面的一点调用逻辑也是值得去学习的,比如说最后的cancel逻辑,他将axios返回的promise控制权交给用户,当用户想终止可以直接触发他的reject方法。