axios源码分析

214 阅读4分钟

Axios是一个基于promise的http库,同时可以支持node和浏览器环境

创建axios实例的过程

// Axios.js
function Axios(config) {
    this.defaults = config;
    this.interceptors = {
        request: new InterceptorManager(),
        response: new InterceptorManager()
    };
}

// axios.js 
function createInstance(defaultConfig) {
    const conext = new Axios(defaultConfig);
    
    const instance = bind(conext.prototype.request, context);
    
    // 将一些在context上的属性复制到instance上 context的原型和context实例本身属性
    /*
        context.prototype = {request: func, getUri: func, get/post/...: func}
        属性主要是函数 因为克隆之后 内部this不确定了 就是需要手动修改到当前context上
    */
    
    extend(instance, conext.prototype, context);
    extend(instance, conext);
    
    // 添加上create方法
    instance.create = function (instanceConfig) {
        // 合并配置
        const combinedConfig = merge(defaultConfig, instanceConfig);
        // 返回一个新的实例
        return createInstance(combinedConfig);
    }
}

const axios = createInstance(config);

module.exports = axios;

image-20220215114356486.png

借助Axios类构建一个实例,通过包装原型链上的request方法,得到一个被“包装”的函数(本质就是调用request函数),该“包装”函数有克隆了Axios实例和原型上的一些属性和方法,并添加了create方法,最终进行导出。

调用请求的过程

  1. 拦截器的实现
// interceptorManager.js
function InterceptorManager () {
    // 维护一个自己的队列
    this.handlers = []
}

Interceptor.prototype.use = function (onResolved, onReject, options) {
    this.handlers.push({
        fulfilled: onResolved,
        onRejected: onRject,
        // 此属性最终会决定 当前请求的调用过程是异步的还是同步的 --> 阻塞性
        synchronous: options.synchronous || false,
        // 当前属性是一个函数 调用的时候传递config 如果最终返回false就不会将当前拦截器加入到队列中
        runWhen: options.runWhen
    })
    // 返回在handlers中对应的位置 用于取消使用拦截器
    return id
}
  1. 请求过程
// Axios.js
// 当前函数 是所有请求方法调用的接口
Axios.prototype.request = function (url, config) {
    // 会对配置对象中的一些东西进行处理 比如将url合并到config中、判定请求方法等
    
    // 最终返回的promise
    let promise;
    // 构建队列 请求拦截器
    const requestInterceptorChain = [];
    let sync = true;
    // InterceptorManager原型上实现的方法
    this.interceptors.request.forEach(function (interceptor) {
        const {runWhen, synchronous, fulfilled, onRejected} = interceptor
        // 先判断当前拦截是否需要加入到队列中
        if (runWhen && !runWhen(config)) {
               // 直接结束
               return;
        )
        // 用于决定最终是同步还是异步
        sync = sync && synchronous;
        requestInceptorChain.unshift(fullfilled, onRejected);
    })
    
    // 构建队列 响应拦截器
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function (interceptor) {
        const {fulfilled, onRejected} = interceptor;
        responseInterceptorChain.unshift(fulfilled, onRejected);
    })
    // 此时就会所有的都放在promise中进行执行
    if (!sync) {
        // 第二项是undefined主要就是用于区分请求和响应拦截 第一项就是对应的请求函数
        let chain = [dispatchRequest, undefined];
        // 头部插入请求拦截器
        Array.prototype.unshift.call(chain, requestInceptorChain);
        // 尾插响应拦截器
        chain = chain.concat(responseInterceptorChain);
        
        promise = Promise.resolve(config);
        
        while(chain.length !== 0) {
            const fulfilled = chain.shift();
            const onRejected = chain.shift();
            // 此时请求拦截都是变成异步 不会阻塞后面其它代码的执行
            promise.then(fulfilled, onRejected);
        }
        
        return promise;
    }
    
    // 是同步的形式
    // 处理请求拦截器
    let newConfig = config;
    while (requestInterceptorChain.length) {
        const onFulfilled = requestInterceptorChain.shift();
        const onRejected = requestInterceptorChain.shift();
        try {
            newConfig =onFulfilled(newConfig);
        } catch(err) {
            onRejected(err);
            break;
        }
    }
    // 处理请求
    try {
        promise = dispatchRequest(newConfig);
    } catch(err) {
        // 直接rejected
        return Promise.reject(err);
    }
    // 处理响应拦截器
    while (responseInterceptorChain.length) {
        const onFulfilled = responseInterceptorChain.shift();
        const onRejected = responseInterceptorChain.shift();
        promise.then(onFulfilled, onRejected)
    }
    
    return promise;
}
  1. 触发请求: 在config中存在一个默认的配置项adapter,在创建实例的时候,就会根据所在的环境进行选择对应的发送请求的函数(函数最终会返回一个promise
// dispatchRequest.js
function dispatchRequest(config) {
    // 进行一些处理:头部数据格式(headers、data)处理(config.transfromRequest),头部压缩(原本被加入了一些东西)等
    adapter(config).then(response => {
        // 对响应的数据进行转换成指定格式(config.transformResponse) config中responseType属性决定最终的数据类型    
    })
}

image-20220213184155932.png

取消请求

原生的http请求取消

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (e) {
    // console.log(e, xhr.readyState)
    if (this.readyState === 4 && xhr.status === 200) {
        console.log(xhr) // 对象中包含一些关于请求的信息
    }
}
// 监听调用了abort函数
xhr.onabort = function (e) {
    console.log("请求被手动取消了", e)
}
xhr.open("get", 一个可以测试的接口)
xhr.send("我来测试一下")
// 取消请求 此时我们可以在控制台的network中看到被取消的请求
xhr.abort()

axios中的取消请求

假设我们在config中配置了cancelToken这个属性,我们就可以在发送请求之前,在CancelToken实例中去订阅一个函数(当前函数需要做两件事:1. 当前请求对应的promise的状态设置为rejected; 2. 将请求取消掉)。所以现在需要确定什么时候去调用在cancelToken中订阅的函数。在axios中实现是,在创建CancelToken实例的时候也对外暴露一个cancel函数,供用户去调用。

// CancelToken.js
function CancelToken(excutor) {
    // 传递过来的参数必须是一个函数
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }
    let promiseResolve = null;
    this.promise = new Promise((resolve) => {
        // 存储resolve函数
        promiseResolve = resolve;
    })
    
    this.promise.then(reason => {
        // 执行被订阅的函数
        if (!this._listener) {
            return;
        }
        // 执行被订阅的函数时 可以传递取消请求的消息
        this._listener.forEach(subscribedFunc => subscribedFunc(reason));
    })
    
    // 将取消函数抛给外部进行使用 
    excutor((message) => {
        // 添加取消的原因属性
        this.reason = new Cancel(message);
        // 将当前promise的状态设置为resolve
        promiseResolve(this.reason);
    });
}
// 订阅一个函数
CancelToken.prototype.subscribe = function (handler) {
    if (this._listener) {
        this._listener.push(handler);
    } else {
        this._listener = [handler];
    }
}

//  CancelToken.source的实现
CancelToken.source = function () {
    let cancel = null;
    const token = new CancelToken((c) => {
        // 抛出取消函数
        cancel = c;
    });
    return {
        token,
        cancel
    }
}

防xsrf攻击

在config中存在两个默认配置xsrfCookieNamexsrfHeaderName,分别对应存储在本地cookie的name、cookie加入到请求头中所对应的字段名。在发送请求之前,会依次判断是否为浏览器环境允许携带cookie(withCredential属性)或同源,如果满足上述两个条件就加入到请求头中供后端进行验证(这个地方应该是需要后端进行配合来进行验证!

其它

GitHub地址,可供参考,代码中也做了一些标注。