最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浅析axios内部实现原理

    正文概述 掘金(卑微前端)   2020-12-12   382

    axios源码学习

    之前就粗略地阅读了axios的一些核心源码,最近闲来无事,就对axios的构建过程及重要特性的实现又进一步地去阅读,毕竟是吃饭的家伙嘛,还是要做到心里有数的。axios的常见用法几相关特性就不在这里一一罗列了,不清楚的同学可以先移步到这里。篇幅有限,node环境相关的学习,大家感兴趣的可以进一步学习。

    源码目录

    我们分析的源码都在/lib这个大目录下,目录如下。

    • adaapters 适配器(适配浏览器环境和node环境)
    • cancel 取消请求类(定义请求取消的类)
    • core 核心功能模块 (请求定义及实现的核心逻辑)
    • helper (辅助功能模块)
    • axiox.js (axios导出的文件,入口文件)
    • default.js (axios的默认配置文件)
    • utils (常用的工具类方法)

    axios工作流程图

    开局一张图,剩下全靠编。 (浏览器环境) 浅析axios内部实现原理

    axios工作原理解析

    我们先不关注axios的一些扩展方法,先理清axios是如何进行工作的,我们先找到入口文件axios.js

    var bind = require('./helpers/bind'); // 绑定上下文函数
    var Axios = require('./core/Axios'); // 核心代码入口
    var mergeConfig = require('./core/mergeConfig'); // 合并对象
    var defaults = require('./defaults'); // 默认配置文件
    
    function createInstance(defaultConfig) { // 创建axios对象实例
      var context = new Axios(defaultConfig);
      var instance = bind(Axios.prototype.request, context);
      utils.extend(instance, Axios.prototype, context);
      utils.extend(instance, context);
      return instance;
    }
    
    // Create the default instance to be exported
    var axios = createInstance(defaults);
    modules.export.default axios
    
    1. 我们可以看到,我们一直使用的axios是通过createInstance这个方法返回的
    2. createInstance里的返回的axios函数,是对Axios这个核心类生成的context实例进行处理后返回的
    3. 有两个关键的工具函数extend和bind,对context实例进行处理
    extend函数
    function extend(a, b, thisArg) {
      forEach(b, function assignValue(val, key) {
        if (thisArg && typeof val === 'function') {
          a[key] = bind(val, thisArg);
        } else {
          // 将b对象上的key value, 赋值到a对象上
          a[key] = val;
        }
      });
      return a;
    }
    bind函数 基本于原生js的bind方法没有区别,其实就是返回Axios.prototype.request函数
    function bind(fn, thisArg) {
      return function wrap() {
        var args = new Array(arguments.length);
        for (var i = 0; i < args.length; i++) {
          args[i] = arguments[i];
        }
        return fn.apply(thisArg, args);
      };
    }
    

    这么一顿骚操作后,在createInstance返回的axios函数的属性上,就已经挂载了Axios原型上的方法和属性了。

    核心类Axios

    Axios类的实现在core/Axios.js中,

    function Axios(instanceConfig) {
      this.defaults = instanceConfig;
      this.interceptors = { // 拦截器
        request: new InterceptorManager(),
        response: new InterceptorManager()
      };
    }
    
    Axios.prototype.request = function request(config) {
      // Allow for axios('example/url'[, config]) a la fetch API
      if (typeof config === 'string') {
        config = arguments[1] || {};
        config.url = arguments[0];
      } else {
        config = config || {};
      }
      // 合并默认配置和用户自定义配置
      config = mergeConfig(this.defaults, config);
      if (config.method) {
        config.method = config.method.toLowerCase();
      } else if (this.defaults.method) {
        config.method = this.defaults.method.toLowerCase();
      } else {
        config.method = 'get';
      }
      // 定义promise链
      var chain = [dispatchRequest, undefined];
      
      var promise = Promise.resolve(config);
      
      // 请求拦截器是向前插入的,所以拦截器函数执行的顺序是倒序的
      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);
      });
      // 当且仅但chain链的为空时
      while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
      }
      // 最后返回一个promise
      return promise;
    };
    

    我们看下这个Axios实例化跟request函数做了什么事情。

    1. 首先对默认的config和自定义的config进行了合并处理。
    2. 接着定义了promise链chain数组,并且通过promise.resolve(config)生成一个新的promise,接着对请求拦截器和相应拦截器插入到chain数组中。
    3. 判断chain数组长度,如果length不等于0,不断的更新promise的值,最后返回最终的promise。

    这也是为什么我们可以链式调用的axios的原因,因为最终返回的是一个promise对象。接着我们来一一对其中的重要的组成部分进行分析。

    dispatchRequest

    代码在core/dispatchRequest.js下,这里我们先不关注请求取消的逻辑,只看请求发送的逻辑部分

    module.exports = function dispatchRequest(config) {
      // 判断请求是否取消
      throwIfCancellationRequested(config);
      // Ensure headers exist
      config.headers = config.headers || {};
      // Transform request data
      config.data = transformData(
        config.data,
        config.headers,
        config.transformRequest
      );
      // Flatten headers
      config.headers = utils.merge(
        config.headers.common || {},
        config.headers[config.method] || {},
        config.headers
      );
      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);
      });
    };
    

    dispatchRequest函数的逻辑非常清晰明了

    1. 首先会对通过transformData对config的data、headers、transformRequest属性进行处理,这里也验证了流程图汇总,在发送xhr前,会对confgi进行transformData的处理
    transform函数
    var utils = require('./../utils');
    module.exports = function transformData(data, headers, fns) {
      /*eslint no-param-reassign:0*/
      utils.forEach(fns, function transform(fn) {
        data = fn(data, headers);
      });
    
      return data;
    };
    

    可以从代码中看出传入的fns其实是一个函数数组,循环遍历函数数组,将当前的data和headers属性当作参数传入每一个fn中,并将返回值作为新的data的值,从而达到对config.data的多次处理,从官方文档中也可以看到这样的用法

    // `transformRequest` 允许在向服务器发送前,修改请求数据
      // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
      // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
      transformRequest: [function (data, headers) {
        // 对 data 进行任意转换处理
        return data;
      }],
    
      // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
      transformResponse: [function (data) {
        // 对 data 进行任意转换处理
        return data;
      }],
    
    1. 对config.headers字段进行了深度合并,接着将headers上绑定的请求方法名遍历循环删除掉
    2. 通过config.adpater来确定是node环境还是浏览器环境,从而决定是调用node.js的http模块方法还是浏览的XHR方法。在默认的配置文件中core/default.js
    function getDefaultAdapter() {
      var adapter;
      if (typeof XMLHttpRequest !== 'undefined') {
        // For browsers use XHR adapter
        adapter = require('./adapters/xhr');
      } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // For node use HTTP adapter
        adapter = require('./adapters/http');
      }
      return adapter;
    }
    

    通过getDefaultadapter方法,去引用不同的文件,浏览器环境的话就直接走到

    adapter = require('./adapters/xhr');
    
    1. 最后会对请求的返回的响应结果进行transofromData的处理,与上面处理请求参数的原理相似。

    xhr

    上面我们说了,在浏览器环境中,最终config.adapter的值会变成

    config.adapter = require('./adapters/xhr')
    

    xhr文件中,在发送数据之前也对传入的config作了一些处理,我们就不一一分析,这里的话我们主要关注xhr对象的onreadystatechange函数,为方便阅读,以下代码只截取了核心逻辑

    module.exports = function xhrAdapter(config) {
      return new Promise(function dispatchXhrRequest(resolve, reject) {
    // Listen for ready state
        var xhr = new XMLHttpRequest()
        request.onreadystatechange = function handleLoad() {
          if (!request || request.readyState !== 4) {
            return;
          }
          if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
            return;
          }
    
          // Prepare the response
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };
          settle(resolve, reject, response);
          request = null;
        };
      }
    }
    

    xhrAdapter函数返回的是一个promise,通过创建xhr对象,监听onreadystatechange事件,在成功的时候,将拼装好的response变量,调用settle方法

    settle(resolve, reject, response);
    

    定义如下:

    module.exports = function settle(resolve, reject, response) {
      var validateStatus = response.config.validateStatus;
      if (!response.status || !validateStatus || validateStatus(response.status)) {
        resolve(response);
      } else {
        reject(createError(
          'Request failed with status code ' + response.status,
          response.config,
          null,
          response.request,
          response
        ));
      }
    };
    

    这个函数其实就是resolve和reject的另一层封装,只要response.status合法就直接将response作为参数resolve出去,这里的validateStatus方法就是对response的用户自定义校验,官方文档中也提及了相应的参数

    // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
      validateStatus: function (status) {
        return status >= 200 && status < 300; // default
      },
    

    到此为止,一个axios的构建和请求发送的核心流程就已经结束了。 接下来,对上文提及的取消请求和拦截器的特性进行一些补充。

    拦截器

    拦截器类的定义在core/InterceptorManager.js,可参考上文中的工作流程图。

    'use strict';
    
    var utils = require('./../utils');
    
    function InterceptorManager() {
      this.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;
      }
    };
    
    InterceptorManager.prototype.forEach = function forEach(fn) {
      utils.forEach(this.handlers, function forEachHandler(h) {
        if (h !== null) {
          fn(h);
        }
      });
    };
    
    module.exports = InterceptorManager;
    
    

    InterceptorManager是一个函数,保存了handlers数组。

    1. use方法:传入两个函数fulfilled, rejected(类似于promise.then方法的传参),并把将包含两个函数的对象放进this.handlers数组中,最后返回当前数组的索引值
    2. eject方法: 传入id,这里的id其实就是hanlders的索引,通过索引将对应的值置为null,从而达到删除的作用
    3. forEach方法: 遍历handlers数组,通过外部传入的fn,执行fn(handler)。

    方法梳理完后,我们重新回顾下Axios的初始化过程,

    function Axios(instanceConfig) {
      this.interceptors = {
        request: new InterceptorManager(), // 请求拦截器
        response: new InterceptorManager() // 响应拦截器
      };
    }
    

    我们在使用axios定义拦截器是这样子写的,

    
    // 添加请求拦截器
    axios.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      return config;
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error);
    });
    

    这里的使用方式就跟源码中的设计思路不谋而合了,能够这么使用,就是因为axios的实例上就挂载了interceptors属性,并且有request和response这两个属性对应的拦截类。 在接下来

    Axios.prototype.request方法
    var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
    
    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) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    
    return promise;
    

    可以看到,在request函数中定义了chain的Promise链,用来存储拦截器中的fulfilled和rejected函数。

    1. 从代码中可以看到,请求拦截器的函数会放在dispatchRequest函数的前面,且定义越早的请求拦截器会放在数组的前方。
    2. 相应拦截器会放在dispatchRequest函数的后面,越晚定义的会放在数组的后面。
    3. 将chain数组拼装好后,就遍历chain数组,每次都将头两个函数当作promise.then方法的fulfilled和rejected方法传入,并将promise的值进行更新,从而达到链式调用的作用。

    可能有同学对这种写法不大敏感,不知道为什么可以链式调用,其实这种写法就相当于

    let promise = Promise.resolve(config)
    let newPromise = promise.then(fn1, fn2).then(fn3, fn4).then(fn5, fn6)
      .then(.., ..).then(.., ..)
    return newPromise
    

    相信对于promise比较熟悉的同学到这里就明白链式调用的原理了,不熟悉的同学也没关系,可以在掘金上搜索相对应的文章进行学习。 所以在我们使用axios请求数据时,

    axios(config).then(res => ....)
    

    这里的res就是经过了requestInterceptor->transformRequest->dispatchRequest->transformResponse->responseInterceptor,最终返回的数据。 到这里可能有人会疑问,为啥这个transformResponse会在responseInterceptor之前执行啊, 而且明明之前是传入的config进行链式调用的,那么response的值又是何时被传入,供响应拦截器进行处理的呢? 浅析axios内部实现原理

    我们在dispatchRequst.js文件中有这么一段逻辑

    这里的adapter就是xhr.js返回的promise
    return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);
        // Transform response data
        response.data = transformData(
          response.data,
          response.headers,
          config.transformResponse
        );
        return response
      }
    

    一旦adapter状态变成fulfilled时,就会执行onAdapterResolution函数,进而执行transformData的逻辑,并把response返回。注意,dispatchRequest.js本身就返回adapter(config).then(.., ..)一个新的promise实例。

    假设我们当前的fulfilled为dispatchRequest函数,
    那么这时候的config就是经过了请求拦截器的处理的,当我们执行下面函数的时候
    promise = promise.then(dispatchRequest, ...)
    

    因为dispatchRequest本身返回一个promise,所以promise会变成dispatchRequest返回的promise,那么接下来,如果有响应拦截器的话,继续执行

    promise = promise.then(responseFn1, ...)
    

    这个时候,传入responseFn1的参数就是response了,那么接下来响应拦截器处理的就是response了,这里就完成了从request到response的转换了。

    取消请求

    取消请求这个特性,我愿称之为最强,在实际工作中也用到了很多,常用于的场景主要是查询列表方面,防止数据紊乱。 浅析axios内部实现原理

    想象一下这么一个场景,比如你要做一个歌单的查询列表页面,当你输入一个关键字的时候,就会发出请求,当然这样是非常粗暴的,前端的话我们都会做防抖的措施,减少请求的开销,一般情况是能够满足的。

    假设我们防抖的时间设为300ms,但是我们通过关键字搜索,返回的数据的大小也不同,每次请求的网络状态也可能会有很大的起伏,假设我们第一次在T1(300ms)后发起了第一次的请求,但是如果这一次的查询的数据量很大加上网络状态不是很好,假设响应时间time1为700ms,接着我们在发起请求后的T2(300ms)又进行了查询,查询的数据量很小且网络状态良好,假设响应时间time2为300ms。

    很明显的可以看到

    T2 + time2 < time1
    

    说明我们第一次请求的响应会在第二次响应之后,这时候就出现前一个请求的数据会覆盖掉第二个请求的数据,这就造成了数据的紊乱了,为了避免这种情况的发生,当我们再发出同一请求时,希望在发送新请求的时候,把上一次未进行响应的请求阻止掉,axios也为我们提供了这一个特性。

    Cancel及CancelToken类

    我们首先看看官网上,关于取消请求的一些例子

    第一种,是通过axios的CancelToken属性,然后通过该属性的source方法生成一个source对象,每次发送请求的时候,都把source.token当作config的cancelToken属性的值,调用source.cancel取消请求

    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 {
         // 处理错误
      }
    });
    
    // 取消请求(message 参数是可选的)
    source.cancel('Operation canceled by the user.');
    

    第二种,是通过new CancelToken实例化cancel实例传给config.cancelToken,将c赋值给cancel函数,在通过调用cancel函数进行请求的取消

    const CancelToken = axios.CancelToken;
    let cancel;
    
    axios.get('/user/12345', {
      cancelToken: new CancelToken(function executor(c) {
        // executor 函数接收一个 cancel 函数作为参数
        cancel = c;
      })
    });
    
    // cancel the request
    cancel();
    

    在cancel.js,找到cancel定义

    function Cancel(message) {
      this.message = message;
    }
    Cancel.prototype.toString = function toString() {
      return 'Cancel' + (this.message ? ': ' + this.message : '');
    };
    Cancel.prototype.__CANCEL__ = true;
    module.exports = Cancel;
    

    在cancel/cancelToken.js中找到cancelToken定义的代码

    var Cancel = require('./Cancel');
    
    function CancelToken(executor) {
      if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
      }
    
      var resolvePromise;
      // 给this.promise赋值一个promise,为了异步地去取消请求
      this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve; // 将promise中的resolve方法赋值给resolvePromise,供后面调用
      });
    
      var token = this;
      // 执行executor函数,参数是一个函数fn,
      // 这个fn函数的参数是一个错误提示信息
      executor(function cancel(message) {
        if (token.reason) {
          // Cancellation has already been requested
          return;
        }
        // token.reason 存储的是一个Cancel实例对象
        token.reason = new Cancel(message);
        // 调用会把this.promise从pending状态变成fulfilled状态,异步取消请求
        resolvePromise(token.reason);
      });
    }
    
    CancelToken.prototype.throwIfRequested = function throwIfRequested() {
      if (this.reason) {
        throw this.reason;
      }
    };
    
    CancelToken.source = function source() {
      var cancel;
      var token = new CancelToken(function executor(c) {
        cancel = c; // 这个c其实就是取消请求的函数
      });
      return {
        token: token,
        cancel: cancel
      };
    };
    
    module.exports = CancelToken;
    

    通过上面源码的学习,我们就知道为什么axios支持两种取消请求的方式,其实CancelToken.source是对CancelToken的进一步封装,使用方式不同但是原理是一样的。

    CacnelToken实例化的过程:(cancelToken函数接受的参数是一个excutor函数)

    1. 先通过new Promise生成一个promise,接着把这个promise赋值给this.promise,并把promise方法中的resolve方法赋值给一个新的变量resolvePromise。这意味着,这个resolvePromise未来是可以控制this.promise状态的切换的,从而实现对外部的一些异步处理逻辑。
    2. 执行excutor函数,并且将fn函数作为参数传给excutor。
    3. fn函数的入参是一个错误提示的变量(message),fn函数内部会执行以下逻辑
      • token.reason存在,直接return
      • token.reason不存在,调用token.reason = new Cancel(message),接着通过resolvePromise(token.reason),相当于调用了resolve(token.reason),将this.promise的状态从pending变为fulfilled

    上面一直再说this.promise的状态从pending切换成了fulfilled,那这到底有什么用呢? 我们接下来继续找到adapters/xhr.js文件,找到取消相关的代码

    if (config.cancelToken) {
        // Handle cancellation
       // config.cancelToken就是CancelToken的实例对象,
       //config.cancelToken.promise状态从pengding切换成fulfilled后就执行取消请求操作
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
    
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    

    可以看到,如果我们在config上定义了cancelToken属性,就会去执行 config.cancelToken的promise属性的then方法,而执行then方法的时机就在于cancel函数执行了之后,一旦promise的状态成功切换后,就接着就调用了xhr的abort方法,对还未进行响应的请求,进行请求的取消操作。注意,abort方法只会对未进行相应的请求进行取消,已响应的请求执行了也不会有什么作用,当然这一层也不需要我们关心。

    总结

    1. axios是一个基于promise的Http库,在浏览器环境使用XHR,在node环境中使用http模块发送请求
    2. 拦截器的原理就是通过chain的Promise链,在请求之前加入请求拦截器,请求结束之后加入响应拦截器,循环遍历chain,从而达到链式调用的作用。在dispatchRequest的前后,传递的参数由request变成response。
    3. 取消请求的是一个异步分离的设计方案,利用promise的异步效果,通过切换promise的状态,从而达到异步取消请求的实现。

    文章中如有错误的地方,希望能够指出,我也会及时更正过来,大家周末愉快!


    起源地下载网 » 浅析axios内部实现原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元