1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Axios 源碼解析-完整篇

        共 19348字,需瀏覽 39分鐘

         ·

        2021-07-06 18:00

        背景

        日常開發(fā)中我們經(jīng)常跟接口打交道,而在現(xiàn)代標(biāo)準(zhǔn)前端框架(Vue/React)開發(fā)中,離不開的是 axios,出于好奇閱讀了一下源碼。

        閱讀源碼免不了枯燥無味,容易被上下文互相依賴的關(guān)系搞得一頭露水,我們可以抓住主要矛盾,忽略次要矛盾,可結(jié)合 debugger 調(diào)試模式,先把主干流程梳理清楚,在慢慢啃細(xì)節(jié)比較好,以下是對(duì)源碼和背后的設(shè)計(jì)思想進(jìn)行解讀,不足之處請(qǐng)多多指正。

        axios 是什么

        1. 基于 promise 封裝的 http 請(qǐng)求庫(避免回調(diào)地獄)

        2. 支持瀏覽器端和 node

        3. 豐富的配置項(xiàng):數(shù)據(jù)轉(zhuǎn)換器,攔截器等等

        4. 客戶端支持防御 XSRF

        5. 生態(tài)完善(支持 Vue/React,周邊插件等等)

        另外兩條數(shù)據(jù)證明 axios 使用之廣泛

        1.截至 2021 年 6月底,githubstar 數(shù)高達(dá) 85.4k

        2.npm 的周下載量達(dá)到千萬級(jí)別

        Axios 的基本使用

        源碼目錄結(jié)構(gòu)

        先看看目錄說明,如下

        執(zhí)行流程

        先看看整體執(zhí)行流程,有大體的概念,后面會(huì)細(xì)說
        整體流程有以下幾點(diǎn):

        1. axios.create 創(chuàng)建單獨(dú)實(shí)例,或直接使用 axios 實(shí)例(axios/axios.get…)

        2. request 方法是入口,axios/axios.get 等調(diào)用都會(huì)走進(jìn) request 進(jìn)行處理

        3. 請(qǐng)求攔截器

        4. 請(qǐng)求數(shù)據(jù)轉(zhuǎn)換器,對(duì)傳入的參數(shù) dataheader 做數(shù)據(jù)處理,比如 JSON.stringify(data)

        5. 適配器,判斷是瀏覽器端還是 node 端,執(zhí)行不同的方法

        6. 響應(yīng)數(shù)據(jù)轉(zhuǎn)換器,對(duì)服務(wù)端的數(shù)據(jù)進(jìn)行處理,比如 JSON.parse(data)

        7. 響應(yīng)攔截器,對(duì)服務(wù)端數(shù)據(jù)做處理,比如 token 失效退出登陸,報(bào)錯(cuò) dialog 提示

        8. 返回?cái)?shù)據(jù)給開發(fā)者

        入口文件(lib/axios.js)

        從下面這段代碼可以得出,導(dǎo)出的 axios 就是實(shí)例化后的對(duì)象,還在其上掛載 create 方法,以供創(chuàng)建獨(dú)立實(shí)例,從而達(dá)到實(shí)例之間互不影響,互相隔離。

        ...
        // 創(chuàng)建實(shí)例過程的方法
        function createInstance(defaultConfig{
          return instance;
        }
        // 實(shí)例化
        var axios = createInstance(defaults);

        // 創(chuàng)建獨(dú)立的實(shí)例,隔離作用域
        axios.create = function create(instanceConfig{
          return createInstance(mergeConfig(axios.defaults, instanceConfig));
        };
        ...
        // 導(dǎo)出實(shí)例
        module.exports = axios;

        可能大家對(duì) createInstance 方法感到好奇,下面一探究竟。

        function createInstance(defaultConfig{
          // 實(shí)例化,創(chuàng)建一個(gè)上下文
          var context = new Axios(defaultConfig);

          // 平時(shí)調(diào)用的 get/post 等等請(qǐng)求,底層都是調(diào)用 request 方法
          // 將 request 方法的 this 指向 context(上下文),形成新的實(shí)例
          var instance = bind(Axios.prototype.request, context);

          // Axios.prototype 上的方法 (get/post...)掛載到新的實(shí)例 instance 上,
          // 并且將原型方法中 this 指向 context
          utils.extend(instance, Axios.prototype, context);

          // Axios 屬性值掛載到新的實(shí)例 instance 上
          // 開發(fā)中才能使用 axios.default/interceptors
          utils.extend(instance, context);

          return instance;
        }

        從上面代碼可以看得出,Axios 不是簡單的創(chuàng)建實(shí)例 context,而且進(jìn)行一系列的上下文綁定和屬性方法掛載,從而去支持 axios(),也支持 axios.get() 等等用法;

        createInstance 函數(shù)是一個(gè)核心入口,我們?cè)诎焉厦媪鞒淌崂硪幌拢?/p>

        1. 通過構(gòu)造函數(shù) Axios 創(chuàng)建實(shí)例 context,作為下面 request 方法的上下文(this 指向)

        2. Axios.prototype.request 方法作為實(shí)例使用,并把 this 指向 context,形成新的實(shí)例 instance

        3. 將構(gòu)造函數(shù) Axios.prototype 上的方法掛載到新的實(shí)例 instance 上,然后將原型各個(gè)方法中的 this 指向 context,開發(fā)中才能使用 axios.get/post… 等等

        4. 將構(gòu)造函數(shù) Axios 的實(shí)例屬性掛載到新的實(shí)例 instance 上,我們開發(fā)中才能使用下面屬性
          axios.default.baseUrl = 'https://…'
          axios.interceptors.request.use(resolve,reject)

        大家可能對(duì)上面第 2 點(diǎn) request 方法感到好奇,createInstance 方法明明可以寫一行代碼 return new Axios() 即可,為什么大費(fèi)周章使用 request 方法綁定新實(shí)例,其實(shí)就只是為了支持 axios() 寫法,開發(fā)者可以寫少幾行代碼。。。

        默認(rèn)配置(lib/defaults.js)

        createInstance 方法調(diào)用發(fā)現(xiàn)有個(gè)默認(rèn)配置,主要是內(nèi)置的屬性和方法,可對(duì)其進(jìn)行覆蓋

        var defaults = {
          ...
          // 請(qǐng)求超時(shí)時(shí)間,默認(rèn)不超時(shí)
          timeout: 0,
          // 請(qǐng)求數(shù)據(jù)轉(zhuǎn)換器
          transformRequest: [function transformRequest(data, headers{...}],
          // 響應(yīng)數(shù)據(jù)轉(zhuǎn)換器
          transformResponse: [function transformResponse(data{...}],
          ...
        };
        ...
        module.exports = defaults;

        構(gòu)造函數(shù) Axios(lib/core/Axios.js)

        主要有兩點(diǎn):

        1. 配置:外部傳入,可覆蓋內(nèi)部默認(rèn)配置

        2. 攔截器:實(shí)例后,開發(fā)者可通過 use 方法注冊(cè)成功和失敗的鉤子函數(shù),比如 axios.interceptors.request.use((config)=&gt;config,(error)=&gt;error);

        function Axios(instanceConfig{
          // 配置
          this.defaults = instanceConfig;
          // 攔截器實(shí)例
          this.interceptors = {
            requestnew InterceptorManager(),
            responsenew InterceptorManager()
          };
        }

        在看看原型方法 request 做了什么

        1. 支持多類型傳參

        2. 配置優(yōu)先級(jí)定義

        3. 通過 promise 鏈?zhǔn)秸{(diào)用,依次順序執(zhí)行

        // 偽代碼
        Axios.prototype.request = function request(config{
          // 為了支持 request(url, {...}), request({url, ...})
          if (typeof config === 'string') {
            config = arguments[1] || {};
            config.url = arguments[0];
          } else {
            config = config || {};
          }
          // 配置優(yōu)先級(jí): 調(diào)用方法的配置 > 實(shí)例化axios的配置 > 默認(rèn)配置
          // 舉個(gè)例子,類似:axios.get(url, {}) > axios.create(url, {}) > 內(nèi)部默認(rèn)設(shè)置
          config = mergeConfig(this.defaults, config);
          // 攔截器(請(qǐng)求和響應(yīng))
          var requestInterceptorChain = [{
            fulfilled: interceptor.request.fulfilled,
            rejected: interceptor.request.rejected
          }];
          var responseInterceptorChain = [{
            fulfilled: interceptor.response.fulfilled,
            rejected: interceptor.response.rejected
          }];
          var promise;
          // 形成一個(gè) promise 鏈條的數(shù)組
          var chain = [].concat(requestInterceptorChain, chain, responseInterceptorChain);
          // 傳入配置
          promise = Promise.resolve(config);
          // 形成 promise 鏈條調(diào)用
          while (chain.length) {
            promise = promise.then(chain.shift(), chain.shift());
          }
          ...
          return promise;
        };

        通過對(duì)數(shù)組的遍歷,形成一條異步的 promise 調(diào)用鏈,是 axios 對(duì) promise 的巧妙運(yùn)用,用一張圖表示

        攔截器 (lib/core/InterceptorManager.js)

        上面說到的 promise 調(diào)用鏈,里面涉及到攔截器,攔截器比較簡單,掛載一個(gè)屬性和三個(gè)原型方法

        • handler: 存放 use 注冊(cè)的回調(diào)函數(shù)

        • use: 注冊(cè)成功和失敗的回調(diào)函數(shù)

        • eject: 刪除注冊(cè)過的函數(shù)

        • forEach: 遍歷回調(diào)函數(shù),一般內(nèi)部使用多,比如:promise 調(diào)用鏈那個(gè)方法里,循環(huán)遍歷回調(diào)函數(shù),存放到 promise 調(diào)用鏈的數(shù)組中

        function InterceptorManager({
          // 存放 use 注冊(cè)的回調(diào)函數(shù)
          this.handlers = [];
        }
        InterceptorManager.prototype.use = function use(fulfilled, rejected, options{
          // 注冊(cè)成功和失敗的回調(diào)函數(shù)
          this.handlers.push({
            fulfilled: fulfilled,
            rejected: rejected,
            ...
          });
          return this.handlers.length - 1;
        };
        InterceptorManager.prototype.eject = function eject(id{
          // 刪除注冊(cè)過的函數(shù)
          if (this.handlers[id]) {
            this.handlers[id] = null;
          }
        };
        InterceptorManager.prototype.forEach = function forEach(fn{
          // 遍歷回調(diào)函數(shù),一般內(nèi)部使用多
          utils.forEach(this.handlers, function forEachHandler(h{
            if (h !== null) {
              fn(h);
            }
          });
        };

        dispatchRequest(lib/core/dispatchRequest.js)

        上面說到的 promise 調(diào)用鏈中的 dispatchRequest 方法,主要做了以下操作:

        1. transformRequest: 對(duì) config 中的 data 進(jìn)行加工,比如對(duì) post 請(qǐng)求的 data 進(jìn)行字符串化 (JSON.stringify(data))

        2. adapter:適配器,包含瀏覽器端 xhrnode 端的 http

        3. transformResponse: 對(duì)服務(wù)端響應(yīng)的數(shù)據(jù)進(jìn)行加工,比如 JSON.parse(data)

        dispatchRequest 局部圖

        module.exports = function dispatchRequest(config{
          ...
          // transformRequest 方法,上下文綁定 config,對(duì) data 和 headers 進(jìn)行加工
          config.data = transformData.call(
            config, // 上下文環(huán)境,即 this 指向
            config.data, // 請(qǐng)求 body 參數(shù)
            config.headers, // 請(qǐng)求頭
            config.transformRequest // 轉(zhuǎn)換數(shù)據(jù)方法
          );
          // adapter 是一個(gè)適配器,包含瀏覽器端 xhr 和 node 端的 http
          // 內(nèi)置有 adapter,也可外部自定義去發(fā)起 ajax 請(qǐng)求
          var adapter = config.adapter || defaults.adapter;

          return adapter(config).then(function onAdapterResolution(response{
            // transformResponse 方法,上下文綁定 config,對(duì) data 和 headers 進(jìn)行加工
            response.data = transformData.call(
              config, // 上下文環(huán)境,即 this 指向
              response.data, // 服務(wù)端響應(yīng)的 data
              response.headers, // 服務(wù)端響應(yīng)的 headers
              config.transformResponse // 轉(zhuǎn)換數(shù)據(jù)方法
            );
            return response;
          }, function onAdapterRejection(reason{
            ...
            return Promise.reject(reason);
          });
        };

        數(shù)據(jù)轉(zhuǎn)換器(lib/core/transformData.js)

        上面說到的數(shù)據(jù)轉(zhuǎn)換器,比較好理解,源碼如下

        module.exports = function transformData(data, headers, fns{
          var context = this || defaults;
          // fns:一個(gè)數(shù)組,包含一個(gè)或多個(gè)方法轉(zhuǎn)換器方法
          utils.forEach(fns, function transform(fn{
            // 綁定上下文 context,傳入 data 和 headers 參數(shù)進(jìn)行加工
            data = fn.call(context, data, headers);
          });
          return data;
        };

        fns 方法即(請(qǐng)求或響應(yīng))數(shù)據(jù)轉(zhuǎn)換器方法,在剛開始 defaults 文件里定義的默認(rèn)配置,也可外部自定義方法,源碼如下:

        Axios(lib/defaults.js)

        var defaults = {
          ...
          transformRequest: [function transformRequest(data, headers{
            // 對(duì)外部傳入的 headers 進(jìn)行規(guī)范糾正,比如 (accept | ACCEPT) => Accept
            normalizeHeaderName(headers, 'Accept');
            normalizeHeaderName(headers, 'Content-Type');
            ...
            if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
              // post/put/patch 請(qǐng)求攜帶 data,需要設(shè)置頭部 Content-Type
              setContentTypeIfUnset(headers, 'application/json');
              // 字符串化
              return JSON.stringify(data);
            }
            return data;
          }],
          transformResponse: [function transformResponse(data{
            ...
            try {
              // 字符串解析為 json
              return JSON.parse(data);
            } catch (e) {
              ...
            }
            return data;
          }],
        }

        可以看得出,(請(qǐng)求或響應(yīng))數(shù)據(jù)轉(zhuǎn)換器方法是存放在數(shù)組里,可定義多個(gè)方法,各司其職,通過遍歷器對(duì)數(shù)據(jù)進(jìn)行多次加工,有點(diǎn)類似于 node 的管道傳輸 src.pipe(dest1).pipe(dest2)

        適配器(lib/defaults.js)

        主要包含兩部分源碼,即瀏覽器端 xhr 和 node 端的 http 請(qǐng)求,通過判斷環(huán)境,執(zhí)行不同端的 api。

        function getDefaultAdapter({
          var adapter;
          if (typeof XMLHttpRequest !== 'undefined') {
            // 瀏覽器
            adapter = require('./adapters/xhr');
          } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
            // node
            adapter = require('./adapters/http');
          }
          return adapter;
        }

        對(duì)外提供統(tǒng)一 api,但底層兼容瀏覽器端和 node 端,類似 sdk,底層更改不影響上層 api,保持向后兼容

        發(fā)起請(qǐng)求(lib/adapters/xhr.js)

        平時(shí)用得比較多的是瀏覽器端,這里只講 XMLHttpRequest 的封裝,node 端有興趣的同學(xué)自行查看源碼(lib/adapters/http.js)

        簡易版流程圖表示大致內(nèi)容:

        源碼比較長,使用偽代碼表示重點(diǎn)部分

        module.exports = function xhrAdapter(config{
          return new Promise(function dispatchXhrRequest(resolve, reject{
            ...
            // 初始化一個(gè) XMLHttpRequest 實(shí)例對(duì)象
            var request = new XMLHttpRequest();
            // 拼接url,例如:https://www.baidu,com + /api/test
            var fullPath = buildFullPath(config.baseURL, config.url);
            // 初始化一個(gè)請(qǐng)求,拼接url,例如:https://www.baidu,com/api/test + ?a=10&b=20
            request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
            // 超時(shí)斷開,默認(rèn) 0 永不超時(shí)
            request.timeout = config.timeout;
            // 當(dāng) readyState 屬性發(fā)生變化時(shí)觸發(fā),readyState = 4 代表請(qǐng)求完成
            request.onreadystatechange = resolve;
            // 取消請(qǐng)求觸發(fā)該事件
            request.onabort = reject;
            // 一般是網(wǎng)絡(luò)問題觸發(fā)該事件
            request.onerror = reject;
            // 超時(shí)觸發(fā)該事件
            request.ontimeout = reject;
            // 標(biāo)準(zhǔn)瀏覽器(有 window 和 document 對(duì)象)
            if (utils.isStandardBrowserEnv()) {
              // 非同源請(qǐng)求,需要設(shè)置 withCredentials = true,才會(huì)帶上 cookie
              var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
                cookies.read(config.xsrfCookieName) :
                undefined;
              if (xsrfValue) {
                requestHeaders[config.xsrfHeaderName] = xsrfValue;
              }
            }
            // request對(duì)象攜帶 headers 去請(qǐng)求
            if ('setRequestHeader' in request) {
              utils.forEach(requestHeaders, function setRequestHeader(val, key{
                if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
                  // data 為 undefined 時(shí),移除 content-type,即不是 post/put/patch 等請(qǐng)求
                  delete requestHeaders[key];
                } else {
                  request.setRequestHeader(key, val);
                }
              });
            }
            // 取消請(qǐng)求,cancelToken 從外部傳入
            if (config.cancelToken) {
              // 等待一個(gè) promise 響應(yīng),外部取消請(qǐng)求即執(zhí)行
              config.cancelToken.promise.then(function onCanceled(cancel
                request.abort();
                reject(cancel);
                // Clean up request
                request = null;
              });
            }
            // 發(fā)送請(qǐng)求
            request.send(requestData);
          });
        };

        取消請(qǐng)求(lib/cancel/CancelToken.js)

        先看看 axios 中文文檔使用

        var CancelToken = axios.CancelToken;
        var source = CancelToken.source();
        axios.get('/user/12345', {
          cancelToken: source.token
        }).catch(function(thrown{
          if (axios.isCancel(thrown)) {
            console.log('Request canceled', thrown.message);
          } else {
            // 處理錯(cuò)誤
          }
        });
        // 取消請(qǐng)求(message 參數(shù)是可選的)
        source.cancel('Operation canceled by the user.');

        可以猜想,CancelToken 對(duì)象掛載有 source 方法,調(diào)用 source 方法返回 {token, cancel},調(diào)用函數(shù) cancel 可取消請(qǐng)求,但 axios 內(nèi)部怎么知道取消請(qǐng)求,只能通過 { cancelToken: token } ,那 token  跟 cancel 必然有某種聯(lián)系

        看看源碼這段話

        1. CancelToken 掛載 source 方法用于創(chuàng)建自身實(shí)例,并且返回 {token, cancel}

        2. token 是構(gòu)造函數(shù) CancelToken 的實(shí)例,cancel 方法接收構(gòu)造函數(shù) CancelToken 內(nèi)部的一個(gè) cancel 函數(shù),用于取消請(qǐng)求

        3. 創(chuàng)建實(shí)例中,有一步是創(chuàng)建處于 pengding 狀態(tài)的 promise,并掛在實(shí)例方法上,外部通過參數(shù) cancelToken 將實(shí)例傳遞進(jìn) axios 內(nèi)部,內(nèi)部調(diào)用 cancelToken.promise.then 等待狀態(tài)改變

        4. 當(dāng)外部調(diào)用方法 cancel 取消請(qǐng)求,pendding 狀態(tài)就變?yōu)?resolve,即取消請(qǐng)求并且拋出 reject(message)

        function CancelToken(executor{
          var resolvePromise;
          /**
           * 創(chuàng)建處于 pengding 狀態(tài)的 promise,將 resolve 存放在外部變量 resolvePromise
           * 外部通過參數(shù) { cancelToken: new CancelToken(...) } 傳遞進(jìn) axios 內(nèi)部,
           * 內(nèi)部調(diào)用 cancelToken.promise.then 等待狀態(tài)改變,當(dāng)外部調(diào)用方法 cancel 取消請(qǐng)求,
           * pendding 狀態(tài)就變?yōu)?nbsp;resolve,即取消請(qǐng)求并且拋出 reject(message)
           */

          this.promise = new Promise(function promiseExecutor(resolve{
            resolvePromise = resolve;
          });
          // 保留 this 指向,內(nèi)部可調(diào)用
          var token = this;
          executor(function cancel(message{
            if (token.reason) {
              // 取消過的直接返回
              return;
            }
            // 外部調(diào)用 cancel 取消請(qǐng)求方法,Cancel 實(shí)例化,保存 message 并增加已取消請(qǐng)求標(biāo)示
            //  new Cancel(message) 后等于 { message,  __CANCEL__ : true}
            token.reason = new Cancel(message);
            // 上面的 promise 從 pedding 轉(zhuǎn)變?yōu)?nbsp;resolve,并攜帶 message 傳遞給 then
            resolvePromise(token.reason);
          });
        }
        // 掛載靜態(tài)方法
        CancelToken.source = function source({
          var cancel;
          /**
           * 構(gòu)造函數(shù) CancelToken 實(shí)例化,用回調(diào)函數(shù)做參數(shù),并且回調(diào)函數(shù)
           * 接收 CancelToken 內(nèi)部的函數(shù) c,保存在變量 cancel 中,
           * 后面調(diào)用 cancel 即取消請(qǐng)求
          */

          var token = new CancelToken(function executor(c{
            cancel = c;
          });
          return {
            token: token,
            cancel: cancel
          };
        };

        module.exports = CancelToken;

        總結(jié)

        上述分析概括成以下幾點(diǎn):

        1. 為了支持 axios() 簡潔寫法,內(nèi)部使用 request 函數(shù)作為新實(shí)例

        2. 使用 promsie 鏈?zhǔn)秸{(diào)用的巧妙方法,解決順序調(diào)用問題

        3. 數(shù)據(jù)轉(zhuǎn)換器方法使用數(shù)組存放,支持?jǐn)?shù)據(jù)的多次傳輸與加工

        4. 適配器通過兼容瀏覽器端和 node 端,對(duì)外提供統(tǒng)一 api

        5. 取消請(qǐng)求這塊,通過外部保留 pendding 狀態(tài),控制 promise 的執(zhí)行時(shí)機(jī)

        參考文獻(xiàn)

        Github Axios 源碼(https://github.com/axios/axios)

        Axios 文檔說明(http://www.axios-js.com/zh-cn/docs)

        一步一步解析Axios源碼,從入門到原理(https://blog.csdn.net/qq_27053493/article/details/97462300

        點(diǎn)個(gè)『在看』支持下 

        瀏覽 61
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            丰满白嫩大尺度尤物私房照 | 免费绳艺捆绑紧缚av网站入口 | 成人无码区免费视频观看在线 | 色色色天堂 | 青榴社区视频在线观看ios特色 | 丰满老妇高潮一级A片 | 操国产骚逼视频 | 操逼视频黄色 | 91禁看片 | 九九精品在线 |