axios源码学习

161 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情


axios是一个基于promise的HTPP库,可用于浏览器node
这篇笔记只用于分析浏览器相关的代码

Features

  • 在浏览器端使用 XMLHttpRequest 对象通讯
  • 支持 Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消

使用axios

axios({
    url: "http://localhost:8090",
    method: "post",
    data: {
        test: 1
    }
})

axios.post('http://localhost:8090', { test: 1 }, { headers: "application/json" })

axios既支持当作方法调用,也可以通过axios.method方式调用
说明axios是一个函数也是一个对象

使用XMLHttpRequest来发起请求

axios底层是通过XMLHttpRequest这个API来发起请求

axios源码中通过new Axios()来创建一个对象,该对象的原型上挂载了很多方法
其中request方法是主要的方法,其它方法都是通过调用request来发起请求

class Axios {
    request(config) {
        // ... 上面都是一些例如 合并配置项 配置拦截器等等功能
        let promise = Promise.resolve(config) 
        // 发起请求主要是dispatchRequest方法
        return promise.then(dispatchRequest)
    }
    get() {}
    post () {}
    put() {}
    ...
}

function dispatchRequest(config) {
    const { method = "get", url, headers, data } = config
    let xhr = new XMLHttpRequest()
    
    xhr.open(method, url, true)
    
    xhr.onreadystatechange = function() {
        if(xhr.status >= 200 && xhr.status < 300 && xhr.readystate === 4) {
            // 请求成功
            ...
        }
    }
    
    xhr.send(data)
}

所以无论是通过axios.method还是axios()发起请求,调用的都是dispatchRequest这个方法

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

axios源码中是通过var instance = Axios.prototype.request.bind(axios)
axios绑定在上面的request方法中
在把var context = new Axios(defaultConfig)中的方法挂载到instance
来实现把axios当作方法或者对象使用

拦截器设计与实现

使用方法

axios.interceptors.request.use((config) => {
    //...
})

axios.interceptors.response.use((data) => {
    // ...
})

此时我们可以看到,axios上有interceptors这个属性
这个属性中有requestresponse两个属性,里面又有use,eject两个方法,eject方法传入一个id,可以取消一个拦截器

image.png

当我们使用axios.interceptors.request.use时,表示添加一个请求拦截器,那很明显,我们需要一个地方来存储这些拦截器,其实是requestresponse中有个handlers中存储了拦截器,所以在这里我们可以获取到所有的拦截器.

我们来看看拦截器是怎么使用的,后添加的请求拦截器先触发,请求拦截器执行完了之后,就到dispatchRequest,再到响应拦截器顺序触发,我们必须保证顺序执行,那么就是使用Promise链式调用

let chain = [request3, request2, request1, dispatchRequest, response1, response2, response3]
let promise = Promise.resolve(config)

while(chain.length) {
    const { resolved, rejected } = chain.shift()
    promise.then(resolved, rejected)
}

请求拦截器dispatchRequest响应拦截器按照顺序放入一个数组chain
声明一个fulfilled状态的Promise,把所有拦截器就放入该Promise的回调中,就能实现拦截器的功能了

eject

eject方法是取消某个拦截器,我们可以在使用use时,获取到该拦截器在数组的索引,然后再通过该索引来取消拦截器,例如设置为null

eject(id) {
        if (this.interceptors[id] !== null) {
            this.interceptors[id] = null
        }
    }

请求取消

取消功能底层是通过xhr.abort()来实现的,先来看看axios是怎么取消请求的

// 方法1:
let cancelToken = axios.CancelToken;
let cancel;
axios({
    ...
    cancelToken: new cancelToken(c => {
        cancel = c
    })
})

cancel(); // 调用cancel方法即可取消请求

//方法2
let source = cancelToken.source();
axios({
    ...
    cancelToken: source.token
})

source.cancel()

我们传入了一个cancelToken属性,再调用cancel方法就可以取消请求
第二种方法其实也是利用了第一种方法

原理

当我们发出请求之后,怎么能够调用xhr.abort()来取消请求呢,答案是通过Promise,我们先声明一个pending状态的Promise,当我们调用cancel方法之后,其实就是更改Promise的状态为fulfilled,此时去执行该Promise的回调,Promise的回调写死了就是调用xhr.abort()

传入的cancelToken其实就是一个Promise

//使用
let cancelToken = axios.CancelToken;
let cancel;
axios({
    ...
    cancelToken: new cancelToken(c => {
        cancel = c
    })
})

// 实现
class CancelToken {
    constructor(executor) {
        let resolvePromise;
        // 保存Promise的resolve方法
        this.promise = new Promise(resolve => {
            resolvePromise = resolve
        })
        
        executor(message => {
            resolvePromise(message)
        })
    }
}

当我们new CancelToken时,就是创建了一个Promise,并保存了resolve方法,执行传入的函数executor,把更改Promise状态的方法抛出去,也就是外面得到的cancel方法。

function dispatchRequest() {
    const { method = "get", url, headers, data, cancelToken } = config
    let xhr = new XMLHttpRequest()
    
    xhr.open(method, url, true)
    
    if(cancelToken) {
        cancelToken.promise.then(res => {
            xhr.abort()
            throw new Error(cancelToken.message)
        })
    }
    
    xhr.onreadystatechange = function() {
        if(xhr.status >= 200 && xhr.status < 300 && xhr.readystate === 4) {
            // 请求成功
            ...
        }
    }
    
    xhr.send(data)
}

判断是否存在cancelToken,如果存在的话,promise更改状态为fulfilled时,调用abort()方法,即可取消请求。

source原理

通过cancelToken.source()会得到一个对象source,其中有两个属性,一个cancel方法,一个token属性。

其中tokennew CancelToken()得到的是一样的
只是内部把new CancelToken(c => cancel = c)帮你放在了cancel中,并抛了出来,并没有不一样

let cancelToken = axios.CancelToken;
let source = cancelToken.source();

axios({
    ...
    cancelToken: source.token // source.token === new CancelToken()
})

source.cancel()

// 实现
class CancelToken {
    constructor(executor) {
        let resolvePromise;
        // 保存Promise的resolve方法
        this.promise = new Promise(resolve => {
            resolvePromise = resolve
        })
        
        executor(message => {
            resolvePromise(message)
        })
    }
    // sourc方法
    static source() {
        let cancel
        let token = new CancelToken(c => {
            cancel = c
        })
        
        return {
            cancel,
            token
        }
    }
}

我们可以看到,调用source方法只是帮你把cancel取出来了,只是一个语法糖


如果觉得对你有帮助,帮忙点个赞吧!
你们的点赞是我码字的动力!!!!