主题
Axios源码分析&自定义Axios与源码分析(Day20 1/3)
本页对应 Day20-Axios,共 7 段视频,约 1 小时 54 分。
学习目标
- 能够从 npm 包或 GitHub 仓库获取 axios 源码,并指出
lib/为源码目录、dist/axios.js为浏览器可直接引入的构建产物。 - 能够说明 axios 源码主要目录的职责,至少准确定位
lib/axios.js、lib/core/Axios.js、lib/defaults、lib/adapters、lib/cancel的作用。 - 能够解释并验证“小写
axios本质上是函数而非Axios实例”,并说明调用axios()最终会调用Axios实例原型上的request方法。 - 能够在浏览器中通过
type="module"、import/export default正确引入自定义模块,并说明与script全局暴露方式的差异及跨域限制。 - 能够实现
axios(url, config)与axios(config)两种调用形式的参数归一化,合并默认配置与传入配置,并在未指定请求方式时默认使用GET。 - 能够封装发送请求函数并返回
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 是为了可读性,不是硬性规定。
课堂画面






代码示例
示例一:创建 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函数代码不过长(一个屏幕内能看完),提升可读性。
常见错误
现象:在 HTML 中用
<script src=".../lib/axios.js">直接引入源码,控制台报import/export语法错误。
原因:lib/axios.js是 ES Module 源码,浏览器不能按普通脚本直接执行。
修法:演示源码时引入dist/axios.js;或把自定义实现改成模块文件,用<script type="module">+import加载。现象:改成
type="module"后仍报跨域或模块加载失败。
原因:双击file://打开页面,模块请求受同源/协议限制。
修法:用 Live Server 等本地静态服务器打开页面;模块路径写相对路径,必要时保留.js后缀。现象:自定义
axios(config)能调用,但内部this不是 Axios 实例,取不到实例配置。
原因:只返回了普通函数,没有用bind/apply/call把Axios.prototype.request与实例绑定。
修法:用bind(Axios.prototype.request, context)生成对外函数,并在 wrapper 中用apply转发参数、保证this指向实例。现象:合并配置后,调用时传入的
timeout/url/method被默认配置覆盖。
原因:Object.assign合并顺序写反,把默认配置放在了后面。
修法:按“默认配置在前、传入配置在后”合并,如Object.assign({}, defaults, config),避免污染全局默认配置。现象:
axios('url', { method: 'post' })或只传一个对象时,URL/method 丢失或报错。
原因:request没有区分“第一个参数是字符串还是配置对象”,也没给第二个参数默认空对象。
修法:先判断首参类型;字符串则写入config.url,对象则作为配置;第二个参数给默认{},再统一处理config。现象:接口返回
201/204时被当作失败,只有200才进成功分支。
原因:状态码判断写成了status === 200。
修法:按 2xx 判断,如status >= 200 && status < 300;其他状态进入 reject,并保留响应信息便于排查。
练习
- 对照源码目录,画出
axios -> createInstance -> Axios -> request -> dispatchRequest -> adapter的调用链,并标注每一步的输入与返回值。 - 手写一个最小
createInstance:要求对外返回函数,调用该函数时能转发到Axios.prototype.request,并保证this指向实例。 - 实现
request(configOrUrl, config)的参数归一化:支持request(url, config)、request(config)、request()三种调用,并输出最终配置。 - 用
Promise + XMLHttpRequest封装dispatchRequest,要求成功走 resolve、失败走 reject,并正确判断 2xx 状态码。 - 把自定义 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 |
小结
- Axios 源码分析要抓主线,不必逐行读完;重点是入口、核心类、请求方法、适配器和配置合并。
- 对外使用的
axios本质是函数,不是Axios实例;调用它最终落到Axios.prototype.request。 createInstance的关键价值是:把实例能力包装成可直接调用的函数,同时保留实例上下文。request的第一层职责不是发请求,而是做参数归一化、配置合并、默认请求方式处理。- 真正发请求的逻辑应下沉到
dispatchRequest/adapter,这样浏览器与 Node 环境可以分离处理。 - 浏览器中使用 ES Module 必须开启
type="module",并建议通过本地服务器访问,避免file://带来的加载问题。 - 自定义 Axios 时,配置合并顺序、
this绑定、2xx 状态码判断是最容易出错的三个点。 - 学习源码的落点不是“背实现”,而是能把核心设计迁移到自己的工具函数和工程代码中。