Skip to content

Axios源码分析&自定义Axios与源码分析(Day20 1/3) ​

本页对应 Day20-Axios,共 7 段视频,约 1 小时 54 分。

学习目标 ​

  1. 能够从 npm 包或 GitHub 仓库获取 axios 源码,并指出 lib/ 为源码目录、dist/axios.js 为浏览器可直接引入的构建产物。
  2. 能够说明 axios 源码主要目录的职责,至少准确定位 lib/axios.js、lib/core/Axios.js、lib/defaults、lib/adapters、lib/cancel 的作用。
  3. 能够解释并验证“小写 axios 本质上是函数而非 Axios 实例”,并说明调用 axios() 最终会调用 Axios 实例原型上的 request 方法。
  4. 能够在浏览器中通过 type="module"、import/export default 正确引入自定义模块,并说明与 script 全局暴露方式的差异及跨域限制。
  5. 能够实现 axios(url, config) 与 axios(config) 两种调用形式的参数归一化,合并默认配置与传入配置,并在未指定请求方式时默认使用 GET。
  6. 能够封装发送请求函数并返回 Promise 对象,根据 XHR 响应状态码(200–299)分别执行 resolve 或 reject。

知识点 ​

先拿到源码:从 npm 到 lib 与 dist ​

为什么需要:分析 Axios 不能只看文档,得先拿到真实源码,知道入口、核心类和浏览器产物分别在哪。怎么想:最直接的方式是通过 npm 安装,源码会进入 node_modules/axios;GitHub 也能拿,但转写里明确提到访问可能不稳定,所以课堂选择 npm。怎么做:安装后重点看两个位置:lib/ 是源码目录,dist/axios.js 是打包后的浏览器产物。入口 lib/axios.js 本身很“轻”,主要做导入导出;真正的核心在 lib/core/Axios.js。lib/adapters/xhr.js 负责浏览器发请求,http.js 负责 Node 环境;cancel 处理取消请求,defaults 放默认配置,helpers、platform、env 是辅助工具。易错点:不要以为浏览器能直接 <script> 引入 lib/axios.js,它内部还有模块导入,浏览器不能完整支持;实际能用的是 dist 下打包后的文件。

自定义 axios 的骨架:先暴露一个函数 ​

为什么需要:只看源码容易迷路,所以转写里的策略是“边分析边模仿”,自己写一个精简版。怎么想:先验证第三方 Axios 是什么。转写中 console.log(axios) 输出为函数,调用它可以发请求;把第三方注释掉后页面报“axios 不存在”,说明必须先暴露全局变量或模块。怎么做:可以用立即执行函数把内部变量包起来,只暴露 axios;也可以改成 ES Module。易错点:如果只写一个空函数并暴露,页面不报错但请求不会发,因为还没有核心逻辑。

js
(function (window) {
  function axios() {}
  window.axios = axios;
})(window);

创建 axios 函数:本质不是 Axios 实例 ​

为什么需要:转写反复强调,axios 看起来像实例,实际是函数。怎么想:源码里 createInstance(defaults) 创建 new Axios(defaults) 得到 context,再把 Axios.prototype.request 通过 bind 绑定到 context,最后返回这个绑定后的函数。怎么做:先定义默认配置,再定义 createInstance;内部 new Axios(defaults),并让 context.request 指向 Axios.prototype.request 绑定 context 后的函数。易错点:default 是关键字,不能拿来当变量名;axios 不是 new Axios() 的实例,调用 axios() 等价于调用 Axios.prototype.request,只是 this 被绑定到了内部 context 实例上。

js
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);
  return instance;
}

bind 与参数搬运:为什么调用 axios 等于调用 request ​

为什么需要:axios(url, config) 和 axios(config) 都要能用,而且最终要进入 request。怎么想:bind 返回一个新函数,调用新函数时用 apply 执行原函数,并把 arguments 原样传进去。怎么做:bind(fn, thisArg) 内部返回 function () { return fn.apply(thisArg, arguments); }。这样 axios 收到的参数会原样交给 request。易错点:这里容易把 apply 和 call 混掉;apply 第二个参数是数组/类数组,会把元素拆开传参。转写中调试时先让 request 打印,刷新后能看到 request 执行,说明链路通了。

js
function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

request 参数处理:一个参数或两个参数 ​

为什么需要:Axios 支持 axios(url, config) 和 axios(config)。怎么想:如果第一个参数是字符串,就把它塞进 config.url;否则认为第一个参数就是配置对象。第二个参数给默认空对象,避免 config.url 赋值时报错。怎么做:在 request(configOrUrl, config) 中判断 typeof configOrUrl === 'string',是则 config.url = configOrUrl,否则 config = configOrUrl || {}。易错点:不要直接改外部传入对象导致污染;转写里后续合并配置时用 Object.assign({}, defaults, config) 生成新对象,既合并又不改原对象。

js
request(configOrUrl, config) {
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
}

合并默认配置与默认请求方式 ​

为什么需要:全局默认配置和单次请求配置要合并,单次请求优先级更高;method 不传时应默认 get。怎么想:源码用 mergeConfig,自定义时可用 Object.assign 简化。怎么做:config = Object.assign({}, this.defaults, config);然后 config.method = (config.method || this.defaults.method || 'get').toUpperCase()。易错点:顺序不能反,否则全局配置会覆盖单次配置;toUpperCase 拼写要准确,转写中调试时曾写错导致报错。

dispatchRequest 与 Promise:真正发请求 ​

为什么需要:源码没有在 request 里直接 new XMLHttpRequest,而是通过适配器区分浏览器和 Node。怎么想:自定义版可以简化为 dispatchRequest,内部用 XHR;request 返回 Promise。怎么做:request 中 return dispatchRequest.call(this, config);dispatchRequest 返回 new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); ... })。监听 onload,状态码 200 到 299 算成功;否则 reject。易错点:事件监听是同步注册、请求回调异步执行,所以监听写在 send 后通常也没事,但更清晰的是先注册再发送;404 在 XHR 的 onload 里仍会触发,必须自己判断状态码。

js
function dispatchRequest(config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
      else reject(new Error('Request failed: ' + xhr.status));
    };
    xhr.onerror = () => reject(new Error('Network Error'));
    xhr.send();
  });
}

浏览器模块化调试:type="module" 与 live server ​

为什么需要:把自定义 Axios 改成 ES Module 后,不能再普通 <script> 引入。怎么想:浏览器支持一部分 ES Module,但要求 type="module",并且 import 路径要写 ./axios.js。怎么做:HTML 中 <script type="module" src="./index.js"></script>,JS 中 import axios from './axios.js'。易错点:双击打开 HTML 可能因跨域加载模块失败,转写中用 Live Server 解决;浏览器模块支持不完整,省略 .js 可能找不到文件。

获取源码:从 NPM 下载,不直接改 node_modules ​

为什么需要:分析第三方库源码是进阶手段,但先要拿到“可读的源码”,而不是只看压缩后的发布文件。
怎么想:转写里明确说 GitHub 有时打不开,所以直接从 NPM 装包,装完在 node_modules/axios 里取源码;package-lock.json 和 node_modules 不是分析重点,可以删掉,目的只是把包下下来。
怎么做:安装后进入 node_modules/axios,把 axios 目录复制到课堂案例,改名为 01-axios源码,后续分析它。
易错点:不要把 node_modules 当成项目源码目录直接改;分析时关注 lib 与 dist 的区别。

目录结构:先看 lib 与 dist ​

为什么需要:源码文件很多,逐个读不明智,要先建立地图。
怎么想:lib/axios.js 是源码入口;dist/axios.js 是打包后的浏览器可用文件。转写中强调:浏览器不能直接 script 引入 lib/axios.js,因为里面有 import/export,浏览器支持不完整;发布时用构建工具把 lib 下多个模块合并到 dist。
怎么做:重点看 lib/axios.js、lib/core/Axios.js、lib/adapters/xhr.js 与 http.js、lib/cancel、lib/defaults、lib/env、lib/helpers、lib/platform。
易错点:lib/axios.js 只是入口,负责导入导出;核心逻辑在 core/Axios.js。适配器负责判断浏览器用 XMLHttpRequest、Node 用 http。

创建 axios 函数:本质不是 Axios 的实例 ​

为什么需要:我们平时调用 axios(config) 就能发请求,要弄清它为什么是个函数。
怎么想:源码不是直接定义 axios 函数,而是 createInstance(defaults) 创建;内部先 new Axios(defaults) 得到实例,再把 Axios.prototype.request 绑定到该实例上返回。
怎么做:

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

bind 的核心是用 apply 保证 this 指向 context:

js
function bind(fn, thisArg) {
  return function wrap(...args) {
    return fn.apply(thisArg, args);
  };
}

易错点:default 是关键字,不能当变量名;axios 不是 new Axios() 的实例,而是绑定后的函数。调用 axios 最终调用的是 Axios.prototype.request,参数要原样搬运过去。

浏览器里用模块方式引入自定义 axios ​

为什么需要:模块天生有作用域,不必自己写立即执行函数包住所有变量。
怎么想:把自定义 axios.js 改成 export default axios,HTML 里用 import 引入。
怎么做:

js
export default axios;
html
<script type="module">
  import axios from './axios.js';
  console.log(axios);
</script>

易错点:不能再用普通 script src 直接引;必须写 type="module";本地双击打开可能跨域,要用 Live Server;浏览器模块支持不完整,转写里省略 .js 会找不到文件。

request 接收一个或两个参数 ​

为什么需要:axios(url, config) 和 axios(config) 都要能用。
怎么想:判断第一个参数是不是字符串;是字符串就当 URL 放进配置对象,否则就当配置对象。
怎么做:

js
Axios.prototype.request = function(configOrUrl, config) {
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
  // 后续合并配置、发请求
};

易错点:第二个参数可能不传,先给空对象,否则 config.url 会报错;只考虑字符串和其他对象,传数字等不属于正常用法。

合并默认配置与设置默认 method ​

为什么需要:全局默认配置和单次请求配置要合并,且没写 method 时默认 get。
怎么想:单次传入的配置优先级更高,所以合并时放在后面;不要直接改 this.defaults,否则污染全局。
怎么做:

js
config = Object.assign({}, this.defaults, config);
config.method = (config.method || this.defaults.method || 'get').toUpperCase();

易错点:合并到空对象上;method 统一转大写,用户写 post 或 POST 都能处理。

返回 Promise 并封装 dispatchRequest ​

为什么需要:axios 返回 Promise,XHR 请求是异步的。
怎么想:request 里返回 new Promise,真正发请求交给 dispatchRequest;dispatchRequest 创建 XHR、open、send,用 onload 和 onerror 改状态。
怎么做:

js
Axios.prototype.request = function(configOrUrl, config) {
  // 参数处理、合并配置、method 处理略
  return new Promise((resolve, reject) => {
    dispatchRequest.call(this, config).then(resolve, reject);
  });
};

function dispatchRequest(config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
      else reject(new Error('Request failed with status ' + xhr.status));
    };
    xhr.onerror = () => reject(new Error('Network Error'));
    xhr.send();
  });
}

易错点:事件监听是同步添加、响应异步回来;404 在 XHR 里也可能走 onload,所以要判断 status 是否在 200–299;把发请求封装到 dispatchRequest 是为了可读性,不是硬性规定。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:创建 axios 函数(IIFE + window 暴露,axios 本质是函数而非实例) ​

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自定义 axios —— 创建 axios 函数</title>
</head>
<body>
  <script src="./axios.js"></script>
  <script>
    console.log('axios 是:', axios);   // 输出 function
    console.log(typeof axios);          // function

    // 两种调用方式都支持
    axios({ url: 'https://api.github.com/users' });
    axios('https://api.github.com/users', { method: 'get' });
  </script>
</body>
</html>
js
// axios.js
(function (window) {
  // 1. 默认请求配置项(注意:不能起名叫 default,default 是关键字)
  const defaults = {
    timeout: 0
  };

  // 2. 核心类 Axios —— 只供内部使用,不对外暴露
  function Axios(config) {
    this.defaults = config;           // 拿到配置项存到实例上
  }

  // request 是核心方法,真正发请求的逻辑都在这里
  Axios.prototype.request = function (config) {
    console.log('request 执行了,参数:', config);
    return Promise.resolve('假装返回一个 Promise');
  };

  // 3. bind:固定 fn 的 this 为 thisArg,并把参数原样搬运过去
  function bind(fn, thisArg) {
    return function wrap() {
      return fn.apply(thisArg, arguments);
    };
  }

  // 4. createInstance:创建 axios 函数
  function createInstance(defaultConfig) {
    const context = new Axios(defaultConfig);                 // 实例化核心类
    const instance = bind(Axios.prototype.request, context);  // 绑定 this
    return instance;                                          // 返回的是函数
  }

  // 5. 生成 axios 并挂到全局
  const axios = createInstance(defaults);
  window.axios = axios;
})(window);

关键行与易错点

  • axios 本质是 bind 返回的 wrap 函数,不是 Axios 的实例;new Axios(defaultConfig) 得到的 context 才是实例。
  • bind(fn, thisArg) 内部 fn.apply(thisArg, arguments):arguments 是类数组,apply 会把里面元素拆开逐个传给 fn,所以 axios(a, b) 会原样传给 request(a, b)。
  • Axios.prototype.request 写在原型上,所以能被 bind 取到;如果写成 class 的箭头函数属性,就取不到了。
  • 必须用 IIFE 包起来:内部声明了 Axios、defaults、bind、createInstance 一堆变量,不隔离就全变全局变量了。
  • default 是 ES 模块关键字,不能当变量名,源码里叫 defaults。

示例二:浏览器上使用 ES6 模块(export default + import) ​

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自定义 axios —— 模块化写法</title>
</head>
<body>
  <!-- 必须写 type="module",否则 import/export 报「无效的关键字」 -->
  <script type="module">
    import axios from './axios.js';   // 后缀 .js 不能省

    console.log('axios 是:', axios);
    console.log(typeof axios);        // function
    axios({ url: 'https://api.github.com/users' });
  </script>
</body>
</html>
js
// axios.js
const defaults = {
  timeout: 0
};

class Axios {
  constructor(config) {
    this.defaults = config;
  }
  request(config) {
    console.log('request 执行了,参数:', config);
    return Promise.resolve('假装返回一个 Promise');
  }
}

function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  return bind(Axios.prototype.request, context);
}

const axios = createInstance(defaults);

export default axios;   // 模块只对外暴露 axios 一个数据

关键行与易错点

  • type="module" 是硬性要求,不写浏览器不认 import 语法,直接报错。
  • import axios from './axios.js':自定义模块路径必须以 ./ 或 ../ 开头;后缀 .js 不能省略,浏览器对模块化支持不完整。
  • 必须用 Live Server 之类的 http 协议打开,双击 file:// 打开会因加载模块产生跨域报错。
  • export default 只能有一个;用了 export default 之后,导入方可以自己起名字。
  • 模块天生自带作用域,不需要再套 IIFE;但如果你在 HTML 里用 <script src> 直接引这个文件,会报错——它只能被 import。

示例三:发送请求 —— 处理一个/两个参数、合并配置、默认 GET、返回 Promise ​

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自定义 axios —— 发送请求</title>
</head>
<body>
  <script type="module">
    import axios from './axios.js';

    // 一个参数:请求配置对象
    axios({
      url: 'https://api.github.com/users',
      method: 'get'
    }).then(res => {
      console.log('胜利', res);
    }).catch(err => {
      console.log('败北', err);
    });

    // 两个参数:第一个是 URL,第二个是请求配置对象
    axios('https://api.github.com/users', { method: 'get' })
      .then(res => console.log('胜利2', res))
      .catch(err => console.log('败北2', err));
  </script>
</body>
</html>
js
// axios.js
const defaults = {
  timeout: 0,
  method: 'get'
};

// 真正发送请求的函数,返回 Promise
function dispatchRequest(config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    // 监听成功响应(事件本身异步,但注册监听是同步的,写在 send 后面也没问题)
    xhr.onload = function () {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve({
          data: xhr.response,
          status: xhr.status,
          statusText: xhr.statusText,
          config: config,
          request: xhr
        });
      } else {
        reject(new Error('请求失败,状态码:' + xhr.status));
      }
    };
    xhr.onerror = function () {
      reject(new Error('网络错误'));
    };

    // 初始化 + 发送
    xhr.open(config.method.toUpperCase(), config.url);
    xhr.send();
  });
}

class Axios {
  constructor(config) {
    this.defaults = config;   // 全局默认请求配置项
  }

  request(configOrUrl, config = {}) {
    // 1. 处理一个参数 / 两个参数
    if (typeof configOrUrl === 'string') {
      config.url = configOrUrl;            // 第一个是字符串 → 当作 URL 塞进配置对象
    } else {
      config = configOrUrl || {};          // 否则就是请求配置对象
    }

    // 2. 合并配置:全局默认在前,传入的在后(传入的优先级高)
    config = Object.assign({}, this.defaults, config);

    // 3. 设置默认请求方式,并统一转大写
    config.method = (config.method || 'get').toUpperCase();

    // 4. 交给 dispatchRequest 真正发请求,返回值就是 Promise
    return dispatchRequest.call(this, config);
  }
}

function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  return bind(Axios.prototype.request, context);
}

const axios = createInstance(defaults);

export default axios;

关键行与易错点

  • 参数处理:typeof configOrUrl === 'string' 就把它塞进 config.url,否则整体当作请求配置对象;形参用默认值 config = {},避免「只传一个 URL 时 config.url 报错」。
  • Object.assign({}, this.defaults, config):合并到空对象,不要直接合并到 this.defaults 上,否则会污染全局默认配置,下次调用受影响。顺序决定优先级,传入的 config 放后面才能覆盖全局。
  • 请求方式 config.method = (config.method || 'get').toUpperCase():不传默认 GET,大小写都兼容。
  • dispatchRequest.call(this, config) 用 call 固定内部 this 仍是 Axios 实例;因为只有一个参数,用 call 即可,不必用 apply。
  • request 直接 return dispatchRequest(...),所以 axios(...) 的返回值就是 Promise,可以在外面 .then/.catch。
  • xhr.onload 里必须判断 status >= 200 && status < 300 才算成功——404 也会触发 onload,不判断就会把失败当成功。
  • 把发请求单独封装成 dispatchRequest,是为了让 request 函数代码不过长(一个屏幕内能看完),提升可读性。

常见错误 ​

  1. 现象:在 HTML 中用 <script src=".../lib/axios.js"> 直接引入源码,控制台报 import/export 语法错误。
    原因:lib/axios.js 是 ES Module 源码,浏览器不能按普通脚本直接执行。
    修法:演示源码时引入 dist/axios.js;或把自定义实现改成模块文件,用 <script type="module"> + import 加载。

  2. 现象:改成 type="module" 后仍报跨域或模块加载失败。
    原因:双击 file:// 打开页面,模块请求受同源/协议限制。
    修法:用 Live Server 等本地静态服务器打开页面;模块路径写相对路径,必要时保留 .js 后缀。

  3. 现象:自定义 axios(config) 能调用,但内部 this 不是 Axios 实例,取不到实例配置。
    原因:只返回了普通函数,没有用 bind/apply/call 把 Axios.prototype.request 与实例绑定。
    修法:用 bind(Axios.prototype.request, context) 生成对外函数,并在 wrapper 中用 apply 转发参数、保证 this 指向实例。

  4. 现象:合并配置后,调用时传入的 timeout/url/method 被默认配置覆盖。
    原因:Object.assign 合并顺序写反,把默认配置放在了后面。
    修法:按“默认配置在前、传入配置在后”合并,如 Object.assign({}, defaults, config),避免污染全局默认配置。

  5. 现象:axios('url', { method: 'post' }) 或只传一个对象时,URL/method 丢失或报错。
    原因:request 没有区分“第一个参数是字符串还是配置对象”,也没给第二个参数默认空对象。
    修法:先判断首参类型;字符串则写入 config.url,对象则作为配置;第二个参数给默认 {},再统一处理 config。

  6. 现象:接口返回 201/204 时被当作失败,只有 200 才进成功分支。
    原因:状态码判断写成了 status === 200。
    修法:按 2xx 判断,如 status >= 200 && status < 300;其他状态进入 reject,并保留响应信息便于排查。

练习 ​

  1. 对照源码目录,画出 axios -> createInstance -> Axios -> request -> dispatchRequest -> adapter 的调用链,并标注每一步的输入与返回值。
  2. 手写一个最小 createInstance:要求对外返回函数,调用该函数时能转发到 Axios.prototype.request,并保证 this 指向实例。
  3. 实现 request(configOrUrl, config) 的参数归一化:支持 request(url, config)、request(config)、request() 三种调用,并输出最终配置。
  4. 用 Promise + XMLHttpRequest 封装 dispatchRequest,要求成功走 resolve、失败走 reject,并正确判断 2xx 状态码。
  5. 把自定义 axios 改造成 ES Module 版本,在 HTML 中通过 type="module" 引入,并用本地服务器验证请求能发出。

原视频对照 ​

#视频时长
1获取axios源码08:37
2源码目录结构分析12:37
3创建axios函数-axios本质并不是Axios的实例49:34
4浏览器上运行的js使用模块08:43
5发送请求-接收一个或两个参数10:45
6发送请求-合并配置项和默认请求方式09:45
7发送请求并返回Promise对象13:59

小结 ​

  1. Axios 源码分析要抓主线,不必逐行读完;重点是入口、核心类、请求方法、适配器和配置合并。
  2. 对外使用的 axios 本质是函数,不是 Axios 实例;调用它最终落到 Axios.prototype.request。
  3. createInstance 的关键价值是:把实例能力包装成可直接调用的函数,同时保留实例上下文。
  4. request 的第一层职责不是发请求,而是做参数归一化、配置合并、默认请求方式处理。
  5. 真正发请求的逻辑应下沉到 dispatchRequest/adapter,这样浏览器与 Node 环境可以分离处理。
  6. 浏览器中使用 ES Module 必须开启 type="module",并建议通过本地服务器访问,避免 file:// 带来的加载问题。
  7. 自定义 Axios 时,配置合并顺序、this 绑定、2xx 状态码判断是最容易出错的三个点。
  8. 学习源码的落点不是“背实现”,而是能把核心设计迁移到自己的工具函数和工程代码中。