axios基本实现逻辑

298 阅读10分钟

theme: channing-cyan highlight: agate

axios是目前前端最流行的网络请求库。 秉着对流行库的学习,那就开一个新坑,介绍下axios核心源码逻辑。

用Vite新建一个项目最简单的ts项目

npm create vite@latest axios-debugger 

npm install axios

大致回顾下axios基本用法

调用request方法

axios
  .request({
    baseURL: "https://api.github.com",
    url: "/users/mzabriskie",
  })
  .then((res) => {
    console.log(res.data);
  })
  .catch((err) => {
    console.log(err);
  });

亦或者调用对应的请求方法

axios.get("url").then((res) => {
  console.log(res.data);
});

axios.post('url')

axios.put('url')

或者创建一个拥有全局配置的axios对象

const instance = axios.create({
  baseURL: "https://api.github.com",
  method: "get",
	// ... 其他配置
});

axios是如何做到的。

我们通过打断调试点的方式来看下源码逻辑

实现在如图所示的地方下一个断点

image.png

打开vscode的debuger菜单,点击JavaScript调试终端

image.png

在终端中执行运行命令

npm run dev

通过调试链接打开项目

image.png

此时端点就会断在这里

image.png

你就可以沿着断点执行,直接定位到源码里面。

axios是如何初始化的?

axios初始化

// 创建实例方法
function createInstance(defaultConfig) {
	// 创建 axios 实例
  const context = new Axios(defaultConfig);
	// 绑定request方法内部的this指向 axios 实例
	// 注意:此时的instance是一个function
  const instance = bind(Axios.prototype.request, context);
  
  // 将Axios原型对象绑定到instance中
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // 将 axios 实例 绑定到 instance function中
  utils.extend(instance, context, null, {allOwnKeys: true});

  // 申明create方法
	// 这个是我们最常用的: axios.create({}) 
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

经过createInstance方法后,我们看看instance 这个function上到底有哪些属性

image.png

可以看到最常用的请求方法requestgetpostputdelete以及拦截器interceptors都在里面。很显然这些方法都在Axios.prototype实例对象上。来看下Axios这个类

在源码中Axios.js这个文件中,默认就执行了请求方法绑定到Axios.prototype实例上

列举了所有用到的请求方法。

// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method,
      url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/

  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }

  Axios.prototype[method] = generateHTTPMethod();

  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

axios有一个默认配置,放在源码目录中的defaults文件夹下index.js中。

在初始化axios中,默认传进去

const axios = createInstance(defaults);

拦截器实现原理

下面重点看Axios这个类的实现,最重要的拦截器和请求方法都在这个里面实现的。

  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }

构造函数很简单,保存了外部传入的默认配置,如果用户不传,那就用默认的一套。另外保存了requestresponse拦截器相关信息。

提供了2个方法,一个是对外的核心请求方法requst, 当调用request,执行内部的_request方法

image.png

在实际开发中,在真正的发送请求前,我们都会对请求参数和已经返回的response做一些处理。比如在请求参数中根据业务添加特定参数,例如用户信息(token)。

回顾下拦截器基本用法


instance.interceptors.request.use(function (config) {
  console.log(111);
  return config;
});


instance.interceptors.request.use(function (config) {
  console.log(222);
  return config;
});


instance.interceptors.response.use(
  function (response) {
    console.log(333);
    return response;
  },
  function (err) {
    console.log(err);
  }
);

instance.interceptors.response.use(
  function (response) {
    console.log(444);
    return response;
  },
  function (err) {
    console.log(err);
  }
);

输出

222
111
333
444

可以看到,对于请求拦截器执行顺序是倒序,响应拦截器是正序。

首先InterceptorManager管理器会收集请求拦截器和响应连接器。


  constructor() {
    this.handlers = [];
  }


  use(fulfilled, rejected, options) {
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false, // 默认是异步
      runWhen: options ? options.runWhen : null
    });
    return this.handlers.length - 1;
  }

拦截器默认执行顺序是异步。runWhen这个参数决定了本次拦截器要不要执行。

use函数返回了一个数字,本次拦截器所在数组中的位置, 表示了本次拦截器的唯一标识id,可用作移除拦截器

  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null;
    }
  }

再发送请求前,遍历用户设置的请求拦截器和响应拦截器

    const requestInterceptorChain = [];
    let synchronousRequestInterceptors = true;
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
        return;
      }

      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
    });

请求拦截器的方法它是通过unshift压进数组中,最后一个拦截器会被推到要执行的数组中中第一个, 响应拦截器则是顺序添加。

接下来看拦截器最核心的逻辑:

    let promise;
    let i = 0;
    let len;

   // 默认是异步
    if (!synchronousRequestInterceptors) {
      const chain = [dispatchRequest.bind(this), undefined];
      chain.unshift.apply(chain, requestInterceptorChain);
      chain.push.apply(chain, responseInterceptorChain);
      len = chain.length;

      promise = Promise.resolve(config);

      while (i < len) {
        promise = promise.then(chain[i++], chain[i++]);
      }

      return promise;
    }

首先chain数组中默认保存了请求方法,dispatchRequest是真正的发送请求的方法,该方法默认绑定了当前的axios对象。因为用户有可能会创建多个axios实例,传入当前的 axios,就保证了每个请求是对应的axios发起的。返回的数据不会错乱。

请求拦截器requestInterceptorChain被推chain数组中最前面,然后响应拦截器就被顺序添加在在dispatchRequest后面。

拿上面例子说:

对应的chain数据模型如下

['请求拦截器2', '请求拦截器1', dispatchRequest, '响应拦截器1', '响应拦截器2']

根据用户传入的config参数创建一个promise对象,循环chain,将请求拦截器和响应拦截器执行的方法都添加到promise链中。用户设置的请求拦截器config 会沿着promise链传递到dispatchRequest中,dispatchRequest方法返回的response数据沿着后续的promise链传递到响应拦截器中。

为了方面理解,我们可以模拟下这个流程。

let config = { a: 1 };

let promise = Promise.resolve(config);

function onRequest1(config: any) {
  config.age = 10;
  console.log("onRequest1", config);
  return config;
}

function onReques2(config: any) {
  config.name = { a: 1 };
  console.log("onReques2", config);
  return config;
}

function onResponse1(response: any) {
  response.data.name = { a: 1 };
  console.log("response1", response);
  return response;
}

function onResponse2(response: any) {
  console.log("response2", response);
}

function doFetch(config: any) {
  console.log('doFectch', config);
  return {
    data: {
      success: true,
    },
  };
}

[onReques2, onRequest1, doFetch, onResponse1, onResponse2].forEach((fn) => {
  promise = promise.then(fn);
});

image.png

除了默认异步执行外,还支持用户设置为同步操作,对应的代码如下

instance.interceptors.request.use(
  function (config) {
    return config;
  },
  function (err) {
    console.log(err);
  },
  { synchronous: true }
);

instance.interceptors.request.use(
  function (config) {
    return config;
  },
  null,
  { synchronous: true }
);

第三个参数支持传入synchronous明确表示同步。同步执行源码这块逻辑就比较好理解了

    len = requestInterceptorChain.length;

    let newConfig = config;

    i = 0;

	 // 循环拦截器
    while (i < len) {
      const onFulfilled = requestInterceptorChain[i++];
      const onRejected = requestInterceptorChain[i++];
      try {
        // 获取每个拦截器返回的config
        newConfig = onFulfilled(newConfig);
      } catch (error) {
        onRejected.call(this, error);
        break;
      }
    }

    try {
      // 将最后一个拦截器返回的config丢给请求方法
      promise = dispatchRequest.call(this, newConfig);
    } catch (error) {
      return Promise.reject(error);
    }

    i = 0;
    len = responseInterceptorChain.length;

    // 循环响应拦截器
    while (i < len) {
      promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
    }

    return promise;

循环requestInterceptorChain方法拿到每次拦截器返回的config,最后丢给dispatchRequest方法,该方法返回一个promise。循环响应式拦截器数据,将response数据沿着promise链传递下去。

这里总结下:

  1. 拦截器模型:请求拦截器倒序添加到chain数组中,即:最后一个请求拦截器会在第一个执行,紧接着就是dispatchRequest请求方法,然后就是响应拦截器顺序添加到chain中。最后每一步的执行结果都会沿着promise链传递下去
  2. 同步执行和异步执行的拦截器模型一直,不同的就是,会先执行请求拦截器获取到每次返回的config数据,丢给dispatchRequest方法,该方法返回一个promise,响应数据会沿着这个promise链传递下去。

更新下:拦截器同步异步的区别

评论区有小伙伴在问:拦截器同步我们实际开发基本没用过。能列举具体场景么?

确实, 我们在实际开发中很少指定拦截器是同步执行。我举个例子大概就能理解了。

比如某个字段需要你通过一个异步操作获取然后添加到data,或者headers中,或者其他参数中。此时拦截器你可以这么写

// 模拟异步获取token
function getToken() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("token"), 1000);
  });
}

instance.interceptors.request.use(function (config) {
  console.log(config);
  return config;
});

instance.interceptors.request.use(function (config) {
  return new Promise((resolve) => {
    getToken().then((token) => {
      config.headers.set("Authorization", `token ${token}`);
      resolve(config);
    });
  });
});

由于异步请求拦截器的config是promise传递的,所以你可以在上游的请求拦截器中做一些异步操作,丢给下游的请求拦截器

如果改为同步拦截器,你就只能提前获取token,在设置请求拦截器,以及请求。

function getToken() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("token"), 1000);
  });
}

getToken().then((token) => {
  instance.interceptors.request.use(
    function (config) {
      console.log(config);
      return config;
    },
    null,
    {
      synchronous: true,
    }
  );

  instance.interceptors.request.use(
    function (config) {
      config.headers.set("Authorization", `token ${token}`);
      return config;
    },
    null,
    {
      synchronous: true,
    }
  );

  instance.get("/users/mzabriskie").then((res) => {
    console.log(res?.data);
  });
});

异步请求拦截器你可以在每个拦截器做单独的异步操作,同步拦截器,你就只能拿到所有你想要的数据,才能设置拦截器的config。

runWhen

第三个参数,还支持runWhen函数,该函数控制当前拦截器要不要执行。

instance.interceptors.request.use(
  function (config) {
    console.log(111);
    return config;
  },
  null,
  {
    runWhen(config) {
      return false
    },
  }
);

内部源码判断该钩子函数是否返回false, 是的话,则没有加入到requestInterceptorChain数组中。

  if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
        return;
      }

      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);

实际项目中会有以下场景:

对全局拦截器处理header信息,比如加入token认证,所有接口都要加入headers信息,但是登录接口不需要。

instance.interceptors.request.use(
  function (config) {
    config.auth = {
      username: "111",
      password: "222",
    };

    // 或者是下面这样:
    config.headers.set('token', '服务端返回的token')
    return config;
  },
  null,
  {
    runWhen(config) {
      // 单独针对 login接口,过滤该请求拦截器
      // 其他接口都要设置headers信息
      return config.url !== "/login";
    },
  }
);

dispatchRequest执行

说到了真正发请求的地方了。

axios有一个适配器的概念,默认有2个,一个是node端的http请求,一个web端的xhr请求。axios内部会判断当前环境支持哪种请求方式:

const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

拿web端来说,就是对xhr请求做了一程封装,返回一个promise。整体逻辑还算简单。这里就不介绍了。有兴趣可以看下源码:

export default isXHRAdapterSupported && function (config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    let requestData = config.data;
    const requestHeaders = AxiosHeaders.from(config.headers).normalize();
    let {responseType, withXSRFToken} = config;
    let onCanceled;
    function done() {
      if (config.cancelToken) {
        config.cancelToken.unsubscribe(onCanceled);
      }

      if (config.signal) {
        config.signal.removeEventListener('abort', onCanceled);
      }
    }

    let contentType;


    // 处理header信息
    if (utils.isFormData(requestData)) {
      if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
        requestHeaders.setContentType(false); // Let the browser set it
      } else if ((contentType = requestHeaders.getContentType()) !== false) {
        // fix semicolon duplication issue for ReactNative FormData implementation
        const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
        requestHeaders.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
      }
    }

    // xhr请求
    let request = new XMLHttpRequest();

    // 支持Auth认证
    if (config.auth) {
      const username = config.auth.username || '';
      const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
    }

    // 请求全路径
    const fullPath = buildFullPath(config.baseURL, config.url);

    // 指定请求方式
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

   // 处理请求成功数据
    function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      const responseHeaders = AxiosHeaders.from(
        'getAllResponseHeaders' in request && request.getAllResponseHeaders()
      );
      const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
        request.responseText : request.response;
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      };

      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // Clean up request
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }

        // The request errored out and we didn't get a response, this will be
        // handled by onerror instead
        // With one exception: request that using file: protocol, most browsers
        // will return status as 0 even though it's a successful request
        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

    // 取消请求
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));

      // Clean up request
      request = null;
    };

    //  处理错误
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

      // Clean up request
      request = null;
    };

    // 处理超时
    request.ontimeout = function handleTimeout() {
      let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
      const transitional = config.transitional || transitionalDefaults;
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(new AxiosError(
        timeoutErrorMessage,
        transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
        config,
        request));

      // Clean up request
      request = null;
    };

    //  csrf
    if(platform.hasStandardBrowserEnv) {
      withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config));

      if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
        // Add xsrf header
        const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName);

        if (xsrfValue) {
          requestHeaders.set(config.xsrfHeaderName, xsrfValue);
        }
      }
    }

    // Remove Content-Type if data is undefined
    requestData === undefined && requestHeaders.setContentType(null);

    // Add headers to the request
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
        request.setRequestHeader(key, val);
      });
    }

    // Add withCredentials to request if needed
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
    }

    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }

    const protocol = parseProtocol(fullPath);

    if (protocol && platform.protocols.indexOf(protocol) === -1) {
      reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
      return;
    }


    // 发送请求
    request.send(requestData || null);
  });
}

最后

axios源码不算太难,其中比较难理解的就是拦截器那块异步执行流程,搞懂了那块逻辑就基本上算是了解了axios核心。面试题中最常见的就是喜欢考拦截器原理: 就是用户配置拦截器数据沿着promise链传递的过程。配合文章中辅助代码,就能够很好理解了。