<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          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)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜夜撸日日操 | 国精品无码一区二区三区在线秋菊 | 9.1成长蘑菇视频 | 亚洲黄色电影在线播放 | 欧美大香蕉性爱 |