Axios源码解析

基类 Axios

跟随入口 index.js 进入/lib/axios.js,第一个方法则是createInstance创建Axios实例。先理解一些属性后,再看 /core/Axios.js 的代码。

  • interceptors,拦截器 /core/InterceptorManager.js

interceptors.request,interceptors.response为InterceptorManager的实例
InterceptorManager的本质是一个订阅发布者模型
handlers 是收集订阅者的容器
use 是订阅方法,向容器中添加{ fulfilled, rejected },分别代表Promise的resolve和reject的两种状态
eject 是退订方法
forEach 进行了重写,绑定方法,遍历通知订阅回调函数的执行发布

  • dispatchRequest,请求的触发

dispatchRequest 的本质是调用了config中的adapter方法,adapter在客户端是返回一个Promise,内部逻辑是对XMLHttpRequest的封装,服务端是一个基于Node.jshttp server。后面会讲到 adapter

/core/dispatchRequest

module.exports = function dispathRequest(config) {
  // ...
  // config.adapter 返回Promise,在客户端本质上是对XMLHttpRequest的封装
 var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    // ...
    return response;
  }, function onAdapterRejection(reason) {
    // ...
    return Promise.reject(reason)
  })
}
  • cancelToken 取消请求的令牌

cancelToken 是用于执行 XMLHttpRequest 中断请求的方法abort,内部通过高阶函数实现,稍显绕脑,作者的设计思路,尤其是外部调用 Promise 中的 resolve 方法让人眼前一亮,我们放在最后讲。

基类Axios /core/Axios.js

function Axios(instanceConfig) {
  // 缓存请求设置
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  }
}

// axios[method]实际上就是调用的request
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    // 满足axios('example/url')调用
    config = arguments[1] || {};
    config.url = arguments[0]
  } else {
    config = config || {};
  }
  // ...
  // 优先入参中的方法,其次为实例化时默认的方法,再次默认为 GET请求
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 拦截器请求订阅放在dispatchRequest前
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 拦截器响应订阅放在dispatchRequest后
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 拦截器依次执行,并修改原订阅数组,触发dispatchRequest,执行请求后,执行响应拦截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift())
  }
  return promise;
}

// 返回请求路径,处理了get请求的queryString拼接
Axios.prototype.getUri = function (...) {
  // ...
}
// ...
module.exports = Axios;
  • methods,请求方法

优先参数设置,默认为GET方法
'delete', 'get', 'head', 'options'方法类似于get,request参数中接收method,url但不接收data
'post', 'put', 'patch'方法类似于post,request参数中接收method,url以及data
axios[method]实际上就是调用的request({ method, url, ... })

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
    }));
  };
});

defaultConfig adapter - xhrAdapter,客户端XMLHttpRequest

/lib/axios.js 首先会创建一个默认请求设置的Axios实例,默认设置中adapter属性在客户端指向文件/adapters/xhr,导出一个方法,即请求的发起 new XMLHttpRequest(),并返回一个Promise。

module.exports = function xhrAdapter(config) {
  // ...
  if (utils.isFormData(requestData)) {
    // 如果提交的是form表单,则要浏览器去设置Content-Type,"multipart/form-data"
    delete requestHeaders['Content-Type'];
  }
  // 实例化XMLHttpRequest对象
  var request = new XMLHttpRequest();
  
  if (config.auth) {
    // ...
    // 设置 Authorization 头信息
    requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
  }
  // ...
  // 初始化一个异步请求
  request.open(method, url, true)

  // 设置超时时间  
  request.timeout = confit.timeout;

  // 当request的readyState变化时,触发
  // 0-UNSENT-代理被创建,但尚未调用open()方法
  // 1-OPENED-open()方法已经被调用
  // 2-HEADERS_RECEIVED-send()方法已经被调用,并且头部和状态已经可获得
  // 3-LOADING-下载中,responseText已包含部分数据
  // 4-DONE-下载操作已完成
  request.onreadystatechange = function handleLoad() {
    // 处理已完成的请求
    if (!request || request.readyState !==4) return;
  
    // status-只读状态码,请求完成前以及请求出错,状态码均为0
    // responseURL-响应的序列化URL
    // 处理已正常完成,且响应URL为非文件的请求
    if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
    var response = {
      data: requset.responseType === 'text' ? requset.responseText : request.response,
      status: request.status,
      statusText: request.statusText,
      headers: parseHeaders(request.getAllResponseHeaders()),
      config: config,
      request: request
    }
    resolve(response);
    request = null;
  }

  // 请求终止
  request.onabort = function ...
  
  // 请求异常
  request.onerror = function ...

  // 请求超时,config中可以设置属性timeoutErrorMessage
  // 这个属性是axios官方没有说的,定义用于reject提供的异常message
  request.ontimeout = function...

  // 配置XMLHttpRequest头信息属性 responseType, withCredentials等...

  // 绑定进度函数 config.onDownloadProgress config.onUploadProgress
  if (typeof config.onDownloadProgress === 'function') {
    request.addEventListener('progress', config.onDownloadProgress);
  }
  // 上传进度还需要判断浏览器是否支持,loadstart, loadend, progress等进度都需要绑定在upload上
  if (typeof config.onUploadProgress === 'function' && request.upload) {
    request.upload.addEventListener('progress', config.onUploadProgress)
  }
  // 取消令牌,终止请求,Promise状态reject
  if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      if (!request) return;
      request.abort();
      reject(cancel);
      request = null;
    })
  }
  // 发送请求
  request.send(requestData)
}

请求的流程

到这里,整个请求的流程已经清晰了。

  1. 当执行axios(url)或者axios[method]对应的都是Axios中的request方法
  2. 拦截器interceptors收集订阅,顺序为,请求拦截器,dispatchRequest,响应拦截器
  3. 拦截器Promise.then(chain.shift())执行,首先执行请求拦截器,并改变原订阅数组
  4. 直至dispatchRequest触发config.adapter(客户端是XMLHttpRequest, 返回Promise)
  5. 后继续Promise.then(chain.shift()),执行响应拦截器,直至订阅数组长度为0

在过程4,dispatchRequest触发请求即XMLHttpRequest的执行过程是,open初始化,绑定所有方法,添加属性和配置后,send发起请求。
过程中执行绑定的方法,非预期时reject;只有当readyState为4时,才有可能resolve拿到我们期望的数据。
常见的使用Axios的方法总是配合着then + catchasync/await + catch使用。

CancelToken,用于中断取消请求

首先对比下CancelToken的源码与CancelToken的使用方式

CancelToken 源码 /cancel/CancelToken.js

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    // executor必须是函数
    throw new TypeError('executor must be a function.');
  }
  
  // 很关键!!
  // promise可以在外部被调用resolve
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 标记1
  executor(function cancel(message) {
    // message是执行后面source.cancel()传入的参数
    if (token.reason) {
      // dispatchRequest 已经从通过 config.adapter 接收到响应结果了,会调用下面的 throwIfRequested 方法
      // 无法手动终止请求
      return;
    }
    // reason 理解成一个非空字符串就好
    token.reason = new Cancel(message);
    // 很关键!!
    // 非Promise内部执行CancelToken.promise的Promise.resolve(token.reason)
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

CancelToken.source = function source() {
  var cancel;
  // 定义 token 接收一个 CancelToken 实例
  // 上文的标记1中的 executor 的参数 function cancel(message) 就对应下面的参数 c
  //  定义 cancel 来接收c
  // !!!那么,cancel() 就可以调用 token.promise 中的 Promise.resolve
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    // token 中有 Promise
    token: token,
    // cancel 可以调用 token.promise 中的 Promise.resolve
    cancel: cancel
  };
};

module.exports = CancelToken;

example: 本质上就是 cancel 执行了 token.promise 中的 Promise.resolve

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
 
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});
 
axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})
 
// cancel the request (the message parameter is optional)

// 执行了 config.cancelToken.promise 中的 Promise.resolve
source.cancel('手动中断请求'');

Promise.resolve那么然后呢?还记得最初提到的XMLHttpRequest的abort方法吗?

xhr /adapters/xhr.js

// ...
// 都清晰了吧
if (config.cancelToken) {
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) return;
    request.abort();
    reject(cancel);
    request = null;
  })
}

// ...

好啦!Axios源码解析到这里就结束了,希望大家能够看明白,能够喜欢!

你可能感兴趣的:(Axios源码解析)