網路請求-手把手實現axios

安穩.發表於2019-03-22

Created By JishuBao on 2019-03-22 15:22:22
Recently revised in 2019-03-22 16:38:22

  目錄

  1. 前端網路請求發展史
  2. 手寫axios前提-瞭解promise

一、前端網路請求發展史

ajax的誕生

  1994年可以看做前端歷史的起點,這一年10月13日網景推出了第一版Navigator;這一年,Tim Berners-Lee建立了W3C;這一年,Tim的基友釋出了CSS。還是這一年,為動態web網頁設計的服務端指令碼PHP以及筆者本人誕生。

   1994年一個叫Rasmus Lerdorf的加拿大人為了維護個人網站而建立了PHP。PHP原意是Personal Home Page,宣傳語是Hypertext Preprocessor(超文字處理者)。PHP實現了與資料庫的互動以及用於生產動態頁面的模板引擎,是Web領域中最主流的服務端語言。

  1995年網景推出了JavaScript,實現了客戶端的計算任務(如表單驗證)。

  1996年微軟推出了iframe標籤,實現了非同步的區域性載入。

  1999年W3C釋出第四代HTML標準,同年微軟推出用於非同步資料傳輸的ActiveX,隨即各大瀏覽器廠商模仿實現了XMLHttpRequest。這標識著Ajax的誕生.但是Ajax這個詞是在六年後問世的。

  2005年,Jesse James Garrett發表了一篇線上文章,題為“Ajax:A new Approach to Web”介紹了一種技術,Asynchronous JavaScript+XML,即Ajax,這一技術可以向伺服器請求額外的資料而無須解除安裝頁面,會帶來更好的使用者體驗,特別是在谷歌使用Ajax技術打造了Gmail和谷歌地圖之後,Ajax獲得了巨大的關注。Ajax是Web網頁邁向Web應用的關鍵技術,它標識著Web2.0時代的到來。

  jQuery 是一個JavaScript庫,是一個由John Resig建立於2006年1月的開源專案(開源:意為對外開放)。jQuery 憑藉著簡潔的語法和跨平臺的相容性,極大簡化了JavaScript 開發人員遍歷HTML文件、操作DOM、處理事件、執行動畫和開發ajax的操作,其獨特而又優雅的程式碼風格改變JavaScript 程式設計師的設計思路和編寫程式的方式。即封裝了ajax的功能又被叫做jquery Ajax

fetch的誕生

  fetch號稱是ajax的替代品,它的API是基於Promise設計的,舊版本的瀏覽器不支援Promise,需要使用polyfill es6-promise(文章不多,不再詳細介紹)

axios的誕生

  axios是比fetch更好的非同步請求方案,具體有以下幾點好處

  • 從 node.js 建立 http 請求
  • 支援 Promise API
  • 客戶端支援防止CSRF
  • 提供了一些併發請求的介面(重要,方便了很多的操作)

各自寫法

1.原生Ajax

//原生Ajax
var Ajax={
    get:function(url,fn){
        //XMLHttpRequest物件用於在後臺伺服器互動資料
        var xhr=new XMLHttpRequest();
        xhr.open('GET',url,true);
        xhr.onreadystatechange=function(){
            //readyState==4說明請求已完成
            if(xhr.readyState == 4&&xhr.statue==200 || xhr.status==304){
                //從伺服器獲得資料
                fn.call(this,xhr.responseText);
            }
        };
        xhr.send();
    },
    //data應為'a=a1&b=b1'這種字串格式,在jq裡如果data為物件會自動將隊形轉成這種字串格式
    post:function(url,data,fn){
        var xhr=new XMLHttpRequest();
        xhr.open("POST",url,true);
        //新增http頭 傳送資訊至伺服器時內容編碼型別
        xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        xhr.onreadystatechange=function(){
            if(xhr.readyState==4&&(xhr.status==200||xhr.status==304)){
                fn.call(this,xhr.responseText);
            }
        };
        xhr.send(data);
    }
}
複製程式碼

2.jquery ajax寫法

$.ajax({
    type: 'POST',
    url: url,
    data: data,
    dataType: dataType,
    success: function() {},
    error: function() {}
})
複製程式碼

3.fetch寫法

// fetch
fetch(url)
    .then(response => {
        if (response.ok) {
            response.json()
        }
    })
    .then(data => console.log(data))
    .catch(err => console.log(err))
複製程式碼

4.axios寫法

axios({
    method: 'GET',
    url: url,
})
.then(res => {console.log(res)})
.catch(err => {console.log(err)})
複製程式碼

二、手寫axios前提-瞭解promise

  我們都知道axios是基於promise封裝的,所以我們既然要自己手動封裝一個axios我們肯定要自己實現一個promise啊,不然怎麼能一步步慢慢從底層瞭解axios呢?

1.什麼是promise?

  Promise可能大家都不陌生,因為Promise規範已經出來好一段時間了,同時Promise也已經納入了ES6,而且高版本的chrome、firefox瀏覽器都已經原生實現了Promise,只不過和現如今流行的類Promise類庫相比少些API。

Promise是JS非同步程式設計中的重要概念,非同步抽象處理物件,是目前比較流行Javascript非同步程式設計解決方案之一

  所謂Promise,字面上可以理解為“承諾”,就是說A呼叫B,B返回一個“承諾”給A,然後A就可以在寫計劃的時候這麼寫:當B返回結果給我的時候,A執行方案S1,反之如果B因為什麼原因沒有給到A想要的結果,那麼A執行應急方案S2,這樣一來,所有的潛在風險都在A的可控範圍之內了。

2.Promise/A+規範

為實現者提供一個健全的、可互操作的 JavaScript promise 的開放標準。

術語

  • 解決 (fulfill) : 指一個 promise 成功時進行的一系列操作,如狀態的改變、回撥的執行。雖然規範中用 fulfill 來表示解決,但在後世的 promise 實現多以 resolve 來指代之。
  • 拒絕(reject) : 指一個 promise 失敗時進行的一系列操作。
  • 拒因 (reason) : 也就是拒絕原因,指在 promise 被拒絕時傳遞給拒絕回撥的值。
  • 終值(eventual value) : 所謂終值,指的是 promise 被解決時傳遞給解決回撥的值,由於 promise 有一次性的特徵,因此當這個值被傳遞時,標誌著 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)。
  • Promise : promise 是一個擁有 then 方法的物件或函式,其行為符合本規範。
  • thenable : 是一個定義了 then 方法的物件或函式,文中譯作“擁有 then 方法”。
  • 異常(exception) : 是使用 throw 語句丟擲的一個值。

基本要求

下面我們先來講述Promise/A+ 規範的幾個基本要求。

1. Promise的狀態

  一個Promise的當前狀態必須是以下三種狀態中的一種: 等待狀態(Pending) 執行狀態(Fulfilled)拒絕狀態(Rejected)

const PENDING = 'pending';

const FULFILLED = 'fulfilled';

const REJECTED = 'rejected';
複製程式碼

等待狀態(Pending)

處於等待態時,promise 需滿足以下條件:

  • 可以遷移至執行態或拒絕態
 if (this.state === PENDING) {
     this.state = FULFILLED || REJECTED ;
 }
複製程式碼

執行狀態(Fulfilled)

處於執行態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的終值
 this.value = value;
複製程式碼

拒絕狀態(Rejected)

處於拒絕態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的據因
 this.reason = reason;
複製程式碼

這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變(譯者注:蓋指當 value 或 reason 不是基本值時,只要求其引用地址相等,但屬性值可被更改)

2.Then 方法

  一個 promise 必須提供一個 then 方法以訪問其當前值、終值和據因。 promise 的 then 方法接受兩個引數:

promise.then(onFulfilled, onRejected)
複製程式碼

引數可選

onFulfilled 和 onRejected 都是可選引數。

  • 如果 onFulfilled 不是函式,其必須被忽略
  • 如果 onRejected 不是函式,其必須被忽略

onFulfilled 特性

如果 onFulfilled 是函式:

  • 當 promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值
  • 在 promise 執行結束前其不可被呼叫
  • 其呼叫次數不可超過一次

onRejected 特性

如果 onRejected 是函式:

  • 當 promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因
  • 在 promise 被拒絕執行前其不可被呼叫
  • 其呼叫次數不可超過一次

3.Promise程式碼基本結構

  例項化Promise物件時傳入一個函式作為執行器,有兩個引數(resolve和reject)分別將結果變為成功態和失敗態。我們可以寫出基本結構

function JsBaoPromise(executor){
    var _this=this;//快取this,避免因為this指向造成不必要的錯誤
    this.status='pending';//等待狀態
    this.value=undefined;
    this.reason=undefined;
    
    function resolve(value){

    }

    function reject(reason){

    }

    executor(resolve,reject);

}
複製程式碼

當例項化Promise時會立即執行

  其中status儲存了Promise物件的狀態,規範中指明,一個Promise只有三種狀態:等待(pending)、成功態(resolved)、失敗態(rejected)。當一個Promise物件執行成功了要有一個結果,它使用value屬性儲存,也可能由於某種原因失敗了,這個失敗原因放在reason屬性中儲存。executor函式立馬執行。

4.then方法定義在原型上

  每一個Promise例項都有一個then方法,它用來處理非同步返回的結果,它是定義在原型上的方法,我們先寫一個空方法做好準備:

JsBaoPromise.prototype.then=function(onFulfill,onRejected){
    
}
複製程式碼

5.已是成功態或是失敗態不可再更新狀態

  規範中規定,當Promise物件已經由pending狀態改變為了成功態(resolved)或是失敗態(rejected)就不能再次更改狀態了。因此我們在更新狀態時要判斷,如果當前狀態是pending(等待態)才可更新。

function JsBaoPromise(executor){
    var _this=this;//快取this,避免因為this指向造成不必要的錯誤
    this.status='pending';//等待狀態
    this.value=undefined;
    this.reason=undefined;
    
    function resolve(value){
        //當狀態為pending時再做更新
        if(_this.status==='pending'){
            _this.value=value'//儲存成功結果
            _this.status='resolved';
        }
    }

    function reject(reason){
        if(_this.status==='pending'){
            _this.reason=reason;//儲存失敗原因
            _this.status='rejected';
        }
    }

    executor(resolve,reject);

}
複製程式碼

  以上可以看到,在resolve和reject函式中分別加入了判斷,只有當前狀態是pending才可進行操作,同時將成功的結果和失敗的原因都儲存到對應的屬性上。之後將state屬性置為更新後的狀態。

6.then方法的基本實現

  當Promise的狀態發生了改變,不論是成功或是失敗都會呼叫then方法,所以,then方法的實現也很簡單,根據state狀態來呼叫不同的回撥函式即可:

JsBaoPromise.prototype.then=function(onFulfill,onRejected){
    var _this=this;
    if(_this.status==='resolved'){
        //判斷引數型別,是函式執行之
        if(typeof onFulfill==='function'){
            if(typeof onFulfilled==='function'){
                onFulfilled(_this.value);
            }
        }
    }
    if(_this.status==="rejected"){
        if(typeof onRejected === 'function'){
            onRejected(_this.reason);
        }
    }
}
複製程式碼

  需要一點注意,規範中說明了,onFulfilled 和 onRejected 都是可選引數,也就是說可以傳也可以不傳。傳入的回撥函式也不是一個函式型別,那怎麼辦?規範中說忽略它就好了。因此需要判斷一下回撥函式的型別,如果明確是個函式再執行它。

試驗一下

let promise=new JsBaoPromise((resolve,reject)=>{
    resolve('55');
});
promise.then(
    (data)=>{
        console.log(data);
    },
    (error)=>{
        console.log(error);
    }
)
複製程式碼

  可以看出來控制檯列印出來55。

7.讓promise支援非同步

  程式碼寫到這裡似乎基本功能都實現了,可是還有一個很大的問題,目前此Promise還不支援非同步程式碼,如果Promise中封裝的是非同步操作,then方法無能為力。

let p = new JsBaoPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    },500);
});

p.then(data => console.log(data)); //沒有任何結果
複製程式碼

  執行以上程式碼發現沒有任何結果,本意是等500毫秒後執行then方法,哪裡有問題呢?原因是setTimeout函式使得resolve是非同步執行的,有延遲,當呼叫then方法的時候,此時此刻的狀態還是等待態(pending),因此then方法即沒有呼叫onFulfilled也沒有呼叫onRejected。

  這個問題如何解決?我們可以參照釋出訂閱模式,在執行then方法時如果還在等待態(pending),就把回撥函式臨時寄存到一個陣列裡,當狀態發生改變時依次從陣列中取出執行就好了,清楚這個思路我們實現它,首先在類上新增兩個Array型別的陣列,用於存放回撥函式:

function JsBaoPromise(executor){
   var _this=this;//快取this,避免因為this指向造成不必要的錯誤
   this.status='pending';//等待狀態
   this.value=undefined;
   this.reason=undefined;
   this.onFulfilledCallFunc=[];//儲存成功回撥
   this.onRejectedCallFunc=[];//儲存失敗回撥
   
   function resolve(value){
       //當狀態為pending時再做更新
       if(_this.status==='pending'){
           _this.value=value;//儲存成功結果
           _this.status='resolved';
       }
   }

   function reject(reason){
       if(_this.status==='pending'){
           _this.reason=reason;//儲存失敗原因
           _this.status='rejected';
       }
   }

   executor(resolve,reject);

}
複製程式碼
JsBaoPromise.prototype.then=function(onFulfilled,onRejected){
   var _this=this;
   if(_this.status==='resolved'){
       //判斷引數型別,是函式執行之
       if(typeof onFulfilled==='function'){
           if(typeof onFulfilled==='function'){
               onFulfilled(_this.value);
           }
       }
   }
   if(_this.status==="rejected"){
       if(typeof onRejected === 'function'){
           onRejected(_this.reason);
       }
   }
   if(_this.status==='pending'){
       if(typeof onFulfilled==='function'){
           _this.onFulfilledCallFunc.push(onFulfilled);//儲存回撥
       }
       if(typeof onRejected==='function'){
           _this.onRejectedCallFunc.push(onRejected);//儲存回撥
       }
   }
}
複製程式碼

  這樣當then方法執行時,若狀態還在等待態(pending),將回撥函式依次放入陣列中。

  寄存好了回撥,接下來就是當狀態改變時執行就好了

function resolve(value){
        //當狀態為pending時再做更新
        if(_this.status==='pending'){
            _this.value=value;//儲存成功結果
            _this.onFulfilledCallFunc.forEach(fn=>fn(value))
            _this.status='resolved';
        }
    }

function reject(reason){
        if(_this.status==='pending'){
            _this.reason=reason;//儲存失敗原因
            _this.onRejectedCallFunc.forEach(fn=>fn(reason))
            _this.status='rejected';
        }
}
複製程式碼

   至此,Promise已經支援了非同步操作,setTimeout延遲後也可正確執行then方法返回結果。

8.鏈式呼叫

  Promise處理非同步程式碼最強大的地方就是支援鏈式呼叫,這塊也是最複雜的,我們先梳理一下規範中是怎麼定義的:

1.每個then方法都返回一個新的Promise物件 (原理的核心)

2.如果then方法中顯示地返回了一個Promise物件就以此物件為準,返回它的結果

3.如果then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise物件返回。

4.如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise物件

5.若then方法中出現異常,則呼叫失敗態方法(reject)跳轉到下一個then的onRejected

6.如果then方法沒有傳入任何回撥,則繼續向下傳遞(值的傳遞特性)。

規範中說的很抽像,我們可以把不好理解的點使用程式碼演示一下。

其中第3項,如果返回是個普通值就使用它包裝成Promise,我們用程式碼來演示:

let p =new Promise((resolve,reject)=>{
    resolve(1);
});

p.then(data=>{
    return 2; //返回一個普通值
}).then(data=>{
    console.log(data); //輸出2
});
複製程式碼

可見,當then返回了一個普通的值時,下一個then的成功態回撥中即可取到上一個then的返回結果,說明了上一個then正是使用2來包裝成的Promise,這符合規範中說的。

第4項,如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise物件

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => {
    //沒有return語句
}).then(data => {
    console.log(data); //undefined
});
複製程式碼

可以看到,當沒有返回任何值時不會報錯,沒有任何語句時實際上就是return undefined;即將undefined包裝成Promise物件傳給下一個then的成功態。

第6項,如果then方法沒有傳入任何回撥,則繼續向下傳遞,這是什麼意思呢?這就是Promise中值的穿透,還是用程式碼演示一下:

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => 2)
.then()
.then()
.then(data => {
    console.log(data); //2
});
複製程式碼

以上程式碼,在第一個then方法之後連續呼叫了兩個空的then方法 ,沒有傳入任何回撥函式,也沒有返回值,此時Promise會將值一直向下傳遞,直到你接收處理它,這就是所謂的值的穿透。

現在可以明白鏈式呼叫的原理,不論是何種情況then方法都會返回一個Promise物件,這樣才會有下個then方法。

搞清楚了這些點,我們就可以動手實現then方法的鏈式呼叫,一起來完善它。

首先,不論何種情況then都返回Promise物件,我們就例項化一個新promise2並返回。

JsBaoPromise.prototype.then=function(onFulfilled,onRejected){
    var _this=this;

    var promise2=new JsBaoPromise((resolve,reject)=>{
        if(_this.status==='resolved'){
            //判斷引數型別,是函式執行之
            if(typeof onFulfilled==='function'){
                if(typeof onFulfilled==='function'){
                    onFulfilled(_this.value);
                }
            }
        }
        if(_this.status==="rejected"){
            if(typeof onRejected === 'function'){
                onRejected(_this.reason);
            }
        }
        if(_this.status==='pending'){
            if(typeof onFulfilled==='function'){
                _this.onFulfilledCallFunc.push(onFulfilled);//儲存回撥
            }
            if(typeof onRejected==='function'){
                _this.onRejectedCallFunc.push(onRejected);//儲存回撥
            }
        }
    });

    return promise2;
    
}
複製程式碼

接下來就處理根據上一個then方法的返回值來生成新Promise物件,由於這塊邏輯較複雜且有很多處呼叫,我們抽離出一個方法來操作,這也是規範中說明的。

/**
 * 解析then返回值與新Promise物件
 * @param {Object} promise2 新的Promise物件 
 * @param {*} x 上一個then的返回值
 * @param {Function} resolve promise2的resolve
 * @param {Function} reject promise2的reject
 */
function resolvePromise(promise2, x, resolve, reject) {
    //...
}
複製程式碼

resolvePromise方法用來封裝鏈式呼叫產生的結果,下面我們分別一個個情況的寫出它的邏輯,首先規範中說明,如果promise2和 x 指向同一物件,就使用TypeError作為原因轉為失敗。原文如下:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

這是什麼意思?其實就是迴圈引用,當then的返回值與新生成的Promise物件為同一個(引用地址相同),則會丟擲TypeError錯誤:

let promise2 = p.then(data => {
    return promise2;
});
複製程式碼

相關文章