从 Axios 源码 中我学到了这5点 🚀

806 阅读7分钟

介绍

axios 作为最知名的请求库,大量应用在web,node环境中,有很多有趣的特点。今天就学习一下他的源码,看看有哪些宝藏思想 💎

❤ 工厂实例 与 调用

axios.create([config])
const instance = axios.create({
  baseURL: "https://some-domain.com/api/",
  timeout: 1000,
  headers: { "X-Custom-Header": "foobar" },
});
实例方法

以下是可用的实例方法。指定的配置将与实例的配置合并

  • axios#request(config)
  • axios#get(url[, config])
  • axios#post(url[, data[, config]])
  • .....

入口文件lib > axios.js

code.png

平时我们是直接使用的axios.get本质是调用了 createInstance的返回值(即 instance) 上 的方法

当调用axios的实例方法 axios.create 时,是调用了 instance.create(即createInstance)

👨 当我使用instance.get的时候,就和原来的axios.get 没有差别了

instance.create接受的参数instanceConfig 与默认参数defaultConfig进行合并,然后产出一个 instance,这就又回到了平时我们最常使用的那种方法

不得不说,很巧妙 👍

🧐 函数 or 对象

axios 有两种用法,一种是函数调用,一种是对象方式

  1. 函数用法
 axios({url:"http://xxx",  method:"get",data:{ name:"zs" }  })
  1. 对象用法
axios.get("http://xxx")

既可以当作函数来调用,也可以当作对象

原理就在上面的那个createInstance 方法里面

前置知识

bind

先看 bind 方法 code.png 作用和 Function.prototype.bind 方法目的是一样的,绑定 this 指向

extend

utils.extend 第一个是原对象(source),第二个是要遍历属性的对象(target),第三个是 原对象的 this 指向
可以简单理解成把 target 身上的属性 向 source 身上添加

可以简化成

code.png

ok,前置知识讲到这里,现在说说为什么可以既可以当函数,也可以当作对象
💣大家都知道,在 js 的世界里,万物皆对象,函数也不例外

  1. 绑定 this,生成初始函数

request 方法中的 this 绑定为Axios,因为在 request中有用到 this
同时生成一个 函数(instance)

// 和下文效果没有差别
 const instance = bind(Axios.prototype.request, context);
 // const instance = Axios.prototype.request.bind(context);
  1. 添加方法

前面说了,万物皆对象,在对象身上绑定几个方法不过分吧, 传入 context 是为了保证 this 指向

extend(instance, Axios.prototype, context, { allOwnKeys: true });

instance 身上 加上 Axios.prototype身上的方法,比如get/post 等等,

此时,instance 就可以使用 instance.get / instance.post

Axios的原型方法: code.png

code.png

  1. 添加属性
extend(instance, context, null, { allOwnKeys: true });

经过这一步,instance还把Axios的实例属性也给拷贝了一份

code.png

🎇 终极形态:

image.png

小结

当你使用 axios 函数式的时候,你调用的是Axios.prototype.request
当你使用 axios 的对象形式的时候,你调用的是Axios.prototype 的实例方法

链式调用 - 请求响应拦截器

在请求或响应被 then 或 catch 处理前拦截它们

先看使用

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


// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

用过axios的程序员应该对拦截器都不陌生吧,可以对 请求的参数 / 返回的结果 做进一步的统一处理
那么是怎么做到的?🧐

image.png
答案就在这个文件里

简化代码

interceptor.fulfilled 可以理解成 axios.interceptors.request.use 传入的第一个函数

interceptor.rejected 可以理解成 axios.interceptors.request.use 传入的第二个函数,有可能是 undefined

// 请求拦截数组
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor){
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
})

// 响应拦截数组
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

现在 requestInterceptorChainresponseInterceptorChain 的数据结构类似于

requestInterceptorChain = [
// 第二个请求拦截
  function(config){  },
  function( error){ },
  // 第一个请求拦截,如果传入第二个函数的话
  function(config){  },
   undefined
];

🎗️ Tip:
请求拦截器 requestInterceptorChainunshfit 后面的插在队头
响应拦截器 responseInterceptorChainpush 插入队尾

后面的是重点 🚀

dispatchRequest.bind(this) 是返回一个 Promise,可以直接当作 Promise.resolve 看待
🔥小技巧:
由于requestInterceptorChain / responseInterceptorChain是一个数组, 如果直接使用unshift / push,那么直接插入的是一个数组,需要使用 apply 方法把参数分开插入

  // const chain = [dispatchRequest.bind(this), undefined];
  const chain = [Promise.resolve(config), 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++]);
  }

chain 最终的结构大概是[请求拦截器2,请求拦截器1,发送请求,响应拦截器1,响应拦截器2]

Promise作为流转的中间过程,通过 Promise 的 then 链, 这样就可以实现不断地链式调用

做一个简化版,加深理解

对对象 config = { name :'zs'} 通过请求拦截器添加属性 ageaddress
如果 age 大于 18,则通过 响应拦截器 返回 成年人,如果没有,返回未成年人

    const config = {
        name: "zs"
     }
     
     // 第一个请求拦截器
    const firstRequestInterceptor = [
           function (config) {
              config.age = 20;
              return config
           }, function (err) {
                Promise.reject(err)
           }
     ]
     
// 第二个请求拦截器
    const twoRequestInterceptor = [
          function (config) {
            config.address = "杭州";
             return config
            }, undefined
     ]
     
     
    //😶 第一个响应拦截器
     const firstResponseInterceptor = [
          function (config) {
          // { name:"zs",age:20,address:"杭州" }
             if(config.age > 18){
                 return "成年人"
              }else {
                 return "未成年人"
              }
            },
            undefined
        ]

        // 模拟 unshift
        const requestInterceptorChain = [
            ...twoRequestInterceptor, ...firstRequestInterceptor
        ];
        
        const responseInterceptorChain = [
            ...firstResponseInterceptor
        ]
        
        const chain = [Promise.resolve(config), undefined];
        
        chain.unshift.apply(chain,requestInterceptorChain);
        chain.push.apply(chain,responseInterceptorChain);

        len = chain.length;
        let promise = Promise.resolve(config);
        let i = 0;
        while (i < len) {
            promise = promise.then(chain[i++], chain[i++]);
        }
        promise.then(res=>{
            console.log(res,"res") 
        })

通过 promise链条把chain中的函数串起来, then中的函数的返回值作为下一个 then 函数的初始值,这样可以对 config 不断 添加属性,响应同理

❤ Promise 的发布订阅 - 取消请求

在文件lib > cancel > CancelToken.js

const CancelToken = axios.CancelToken;
let cancel;

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

// 取消请求
cancel();

显而易见,这个方法用来取消请求的,在类CancelToken中的接收一个函数,这个函数的参数c赋值给外部变量cancel,执行 cancel 来达到取消请求的目的

class CancelToken {
    constructor(executor){
    let resolvePromise;
    // ....
    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });
     // ....
     executor(function cancel(message, config, request) {
      resolvePromise(message);
    });
    }
}

那么 axios中的参数cancelToken 是怎么和 CancelToken类 扯上关系的呢?
原因在 lib>adapters>xhr.js(xhr 发送请求函数) 中

code.png

如果config中有cancelToken,那么就调用 cancelToken.subscribe 方法
cancelToken.subscribe 方法,在文件lib > cancel > cancelToken.js

CancelToken类的一个实例方法

code.png

那么 CancelToken 这个类就有一个this._listerersonCancel数组了

那么执行外界变量cancel的时候,会发生什么呢

🚩这就是有趣的地方了 code.png

// CancelToken 类中
  let resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
   });
   
   const token = this;
   this.promise.then(cancel=>{
      let i = token._listeners.length;
      while (i-- > 0) {
        token._listeners[i](cancel);
      }
      token._listeners = null;
   })
   
   executor(function cancel(message, config, request) {
      resolvePromise(message);
 });

它会依次调用_listeners中存储的onCanceled方法,类似于发布订阅

简化版

code.png 代码贴在这,有兴趣可以试试

function xhr(config) {
  let conf = {
    name: "zs",
  };
  function onCancel(message) {
    console.log(message, "错误消息");
    conf.name = null;
  }

  if (config.CancelToken) {
    // 把 onCancel 注入到 CancelToken 中
    config.CancelToken.subscribe(onCancel);
  }

  // 模拟异步
  setTimeout(() => {
    console.log(conf, "conf");
  });
}

class CancelToken {
  constructor(executor) {
    // 现在 resolvePromise 控制着 this.promise 的执行,
    // resolvePromise 执行,this.promise 就会执行
    let resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });

    const token = this;
    this.promise.then(message => {
      token._listeners.forEach(fn => {
        fn(message);
      });
    });

    // 外部传入的函数 c == cancel
    executor(function cancel(message) {
      resolvePromise(message);
    });
  }
  subscribe(listener) {
    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  }
}

// 模拟使用
let cancel;
xhr({
  CancelToken: new CancelToken(function executor(c) {
    cancel = c;
  }),
});
cancel("取消请求");

元辅音 - 判断类型

在文件lib > helpers > validators

const validators = {};

['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach((type, i) => {
  validators[type] = function validator(thing) {
    return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;
  };
});

validators 会是这样的数据结构

image.png

使用

console.log(validators.number(2)) // true
console.log(validators.boolean(2)) // 'a boolean'

本身平平无奇,有点好奇这个 👉i < 1 ? 'n ' : ' ' 是什么意思?

console.log(validators.boolean(2)) // 'a boolean'
console.log(validators.object(2)) // an object

原来是 an 放在元音音素前面,a 放在辅音音素前面, o 是元音音素,其余都是辅音音素,细节

总结

axios 作为一个经久不衰的库,必然有他的过人之处,通过阅读它的源码,确实可以学到很多思想

通过阅读源码,不仅可以对这个库的使用有更深刻的理解,也能够与大佬隔空对话,学习他们的思想,毕竟一般大量使用的库都是有其独特优势。而且他山之石可以攻玉,当遇到类似的问题的时候,可以从脑海里检索出最优解,毕竟输出是由输入决定的

不仅可以看他们的逻辑思路,也可以思考文件分割设计、代码技巧、代码风格、语法规范,是否有可取之处,为什么这么设计,自己有没有更好的想法,如果有更优解,提个pr 也是不错的