Created By JishuBao on 2019-03-22 15:22:22
Recently revised in 2019-03-22 16:38:22
目錄
- 前端網路請求發展史
- 手寫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;
});
複製程式碼