搞懂 axios 核心原理

从入口开始

查看 axios 源码,找到 package.json main 属性值 index.js,也就是说入口文件是根目录下的 index.js

分析入口文件可以看到,引入了 ./lib/axios 模块,接着导出成员 axios 作为默认导出成员:

import axios from './lib/axios.js';

export {
  axios as default,
  ...
}

也就是说,我们平时引入使用的 import axios from 'axios' 就是 ./lib/axios 模块里导出的成员。

所以,分析 axios 核心源码,我们便可以从 ./lib/axios 入手。

axios 实例

大概过一遍该文件,不用细究细节,可以了解到一个大概的代码情况,一般我会先看最后导出的成员,然后接着往上看。

该模块导出了一个默认成员 axios

export default axios;

往上找,axios 是什么呢?分析代码发现其实是一个创建的实例,然后在该实例上绑定了很多的属性、方法

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

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

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.default = axios; // 这个应该在项目中经常看到,做一些默认配置用
...

// this module should only have a default export
export default axios

顺着找, createInstance 这里,这个函数是专门用来新建一个 axios 实例的。

createInstance

// ./lib/axios.js
function createInstance(defaultConfig) {
  // 
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});

  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// ./lib/helpers/bind.js
export default function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

在这里做了几件事:

  • 基于 Axios 类新建了一个 context 实例
  • 把 context 绑定给 Axios 原型的 request 方法,这样 request 调用的时候,里面的 this 才会指向这个内部新建的 context 实例,这样即使是创建多个 axios 实例,它们互相之间是隔离不干扰的
  • bind 函数在这里的作用就是修改 Axios.prototype.requestthis 指向。instance 可以理解为 Axios.prototype.request,这里虽然有点绕,但还是很好理解,instance 是一个 wrap包装函数,只要 instance 被调用,也就是里面的包装函数 wrap()执行,也就是 Axios.prototype.request 被立即调用
  • 换句话说,instance 指的是 Axios.prototype.request(不正确但好理解)
import axios from 'axios'

// 这里我们平时是这样调用 axios 的,
// 而实际上,是调用了内部的 Axios.prototype.request 方法
axios({
    url: '/api',
    method: 'get'
})

// 平时我们使用的创建 axios 实例的用法:
// 其实是因为源码内部 instance.create 方法里,内部调用的也是 createInstance
// 也就是说,官方导出的默认 axios 实例,和我们自己使用 axios.create 新创建的 axiosInstance 本质上是一样的创建逻辑
const axiosInstance = axios.create({})
  • Axios 的原型拷贝到 instance,为什么?下方解释
  • context 拷贝到 instance,为什么?下方解释

Axios 类

Axios 的原型拷贝到 instance

作用是让 instance 共享 Axios 原型对象上的属性、方法,比如使用 axios.get、axios.post 等 API 发送请求

原因是 我们在使用 axios.get(url)axios.post(url) 等请求时,在 instance 上面是没有绑定这些方法的,所以需要手动绑定这些方法到 instance。

在源码中,这部分代码是在 Axios.js 模块实现的:

'use strict';

import utils from './../utils.js';

const validators = validator.validators;

class Axios {
  constructor(instanceConfig) {
  }

  async request(configOrUrl, config) {
  }
  
}

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

export default Axios;

context 拷贝到 instance

作用是让 instance 共享创建的 Axios 实例即 context 的属性、方法,比如 inteceptors、request方法

根据 axios 的官方 API,我们可以看到这些常见的 API:

import axios from 'axios'

axios.request(url)
axios.interceptors.request.use()
axios.interceptors.response.use()

这些方法都是 Axios 类自身需要实现的,request 就是 Axios 实例 context 的方法,而 interceptors 拦截器的实现则是分离出去抽象实现了一个 InterceptorManager 类,封装了处理拦截器相关的操作。拦截器又分为请求拦截器、响应拦截器。

// ./lib/core/Axios.js
class Axios {
    constructor(defaultConfig) {
        this.defaults = defaultConfig;
        this.interceptors = {
            request: new InterceptorManager(),
            response: new InterceptorManager(),
        }
    }
    
    async request(configOrUrl, config) {
        await this._request(configOrUrl, config);
    }
    
    // 重要的实现在这里
    _request(configOrUrl, config) {
        // TODO
    }
}

// ./lib/core/InterceptorManager.js
class InterceptorManager {
    constructor() {
        this.handlers = [];
    }
    
    use(fulfilled, rejected) {
        this.handlers.push({
            fulfilled,
            rejected,
        })
    }
    
    forEach(fn) {
        utils.forEach(this.handlers, function forEachHandler(h) {
          if (h !== null) {
            fn(h);
          }
        });
    }
}

拦截器的实现思路

  _request(configOrUrl, config) {
    // 处理配置信息
    if (typeof configOrUrl === 'string') {
      config = config || {};
      config.url = configOrUrl;
    } else {
      config = configOrUrl || {};
    }
    // 合并配置
    config = mergeConfig(this.defaults, config);
    
    ...

    // 存放请求拦截器
    const requestInterceptorChain = [];
    let synchronousRequestInterceptors = true; // 判断是否同步执行拦截器(主要控制请求拦截器)
    // forEach 是 axios 自身实现的,遍历拦截器
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {// interceptor 实则是对应到 InterceptorManager 的一个 handler
       
      // 只有在所有的拦截器的 synchronus 为 true,才是同步(默认为false,即异步)
      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    
      // 后添加的先执行,fulfilled 和 rejected 成对添加
      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
    
    // 存放响应拦截器
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      // 响应拦截器按书写的先后顺序执行,fulfilled 和 rejected 成对添加
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
    });

    let promise;
    let i = 0;
    let len;

    // 异步执行请求拦截器
    if (!synchronousRequestInterceptors) {
      // dispatchRequest 内部封装了真正的发起请求功能
      const chain = [dispatchRequest.bind(this), undefined]; // 默认的调用链,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;
    }
    
    len = requestInterceptorChain.length;
    let newConfig = config;
    i = 0;

    // 同步执行请求拦截器
    while (i < len) {
      // 把请求拦截器中的 fulfilled、reject取出
      const onFulfilled = requestInterceptorChain[i++];
      const onRejected = requestInterceptorChain[i++];
      try {
        newConfig = onFulfilled(newConfig); // 执行
      } catch (error) {
        // 捕获到错误,执行错误回调,且 break 终止下一个请求拦截器
        // 与异步的区别:同步模式的错误处理针对与当前执行函数
        onRejected.call(this, error);
        break;
      }
    }

    try {
      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;
  }

从拦截器源码中,可以总结以下几点重要的点:

  1. 请求拦截器分为同步执行和异步执行
  2. 只有所有的请求拦截器配置了synchronus=true才是同步,不然异步
  3. 同步、异步的区别在于错误捕获的处理,异步捕获的错误是之前节点最近的一次错误;同步捕获的错误是当前请求拦截器的错误,www.jianshu.com/p/710636bd7…
  4. 在发出请求前,执行请求拦截器,需要在请求拦截器执行函数最后返回 config,且后添加的先执行;请求完成后,把 response 响应结果返回传给响应拦截器,执行响应拦截器函数,也需要把 response 传给下一个响应拦截器

发送 XHR 请求

在上面我们看到有一个 dispatchRequest 函数,这个方法就是用来发起请求的,内部的实现兼容了浏览器端的 xhr 和 node 端的 http。

// lib/core/dispatchRequest.js
export default function dispatchRequest(config) {
  ...
  
  // 在这里调用了getAdapter
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

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

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

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

getAdapter

// ./lib/adapters/adapters.js
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';

const knownAdapters = {
  http: httpAdapter, // 用于 node 端发送 http 请求
  xhr: xhrAdapter // 用于浏览器端发送 XMLHttpRequest 请求
}
const isResolvedHandle = (adapter) => utils.isFunction(adapter) || adapter === null || adapter === false;

export default {
  getAdapter: (adapters) => {
    adapters = utils.isArray(adapters) ? adapters : [adapters];

    const {length} = adapters;
    let nameOrAdapter;
    let adapter;

    const rejectedReasons = {};

    for (let i = 0; i < length; i++) {
      nameOrAdapter = adapters[i];
      let id;

      adapter = nameOrAdapter;

      if (!isResolvedHandle(nameOrAdapter)) {
        // 在这里根据 knownAdapters 映射得出对应的 adapter
        adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
      }
    }
    return adapter;
  },
  adapters: knownAdapters
}

发送 xhr 请求

源码比较复杂,可以做个大概了解

// ./lib/adapters/xhr.js
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

export default isXHRAdapterSupported && function (config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {

    let request = new XMLHttpRequest();

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

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

        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        setTimeout(onloadend);
      };
    }
    
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      
      ...

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      ...
      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      ...
      // Clean up request
      request = null;
    };

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }
  
    // Send the request
    request.send(requestData || null);
  });
}

但是实现一个 XMLHttpRequest 请求必须的三个是 o o sopen onload send,以下是使用 Promise 实现 XHR 请求的简单例子:

export default isXHRAdapterSupported && function (config) {
    return new Promise((resolve) => {
        const { url, method, data = {} } = config;
        
        const xhr = new XMLHttpRequest();
        
        xhr.open(url, method, true);
        
        xhr.onload = function() {
            resolve(xhr.responseText);
        }
        
        xhr.send(data)
    })
}

原文链接:https://juejin.cn/post/7334723606259908645 作者:jiayinkong

(0)
上一篇 2024年2月15日 上午10:05
下一篇 2024年2月15日 上午10:16

相关推荐

发表回复

登录后才能评论