一、回調地獄
前端js代碼中,爲了實現某些特殊需求代碼邏輯經常會寫成層層嵌套的異步回調函數(一個函數作爲參數需要依賴另一個函數執行調用),如果嵌套過多,會極大影響代碼可讀性和邏輯,這種情況也被稱作回調地獄(函數作爲參數層層嵌套)
假設有這樣一個需求:新增用戶保存之前要先驗證用戶名稱,再驗證手機號碼是否存在,可能的代碼如下:
前端代碼如下:
/**
* 根據地址和參數 異步獲取服務器結果
* @param url 請求的地址
* @param data 請求的參數
* @param successCall 成功時的回調函數
* @param errorCall 失敗時的回調函數
*/
function getRequest(url, data, successCall, errorCall) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = function () {
//只要XHR對象的readyState屬性值發生改變,都會觸發一次readystatechange事件。
if (this.readyState !== 4) {
//readyState等於4 表示 請求已完成(可以訪問服務器響應並使用它)
return;
}
if (this.status === 200) {
//請求成功 調用
successCall(this.response);
} else {
//失敗時 調用
errorCall(new Error(this.statusText));
}
};
xhr.responseType = "json";
xhr.send(data);
}
// 先驗證用戶是否存在,再驗證手機號碼是否重複,再提交保存
getRequest("http://localhost:8888/api/vusername?username=smith", null, function (data) {
console.info(data);
//賬號驗證成功之後 驗證手機號
getRequest("http://localhost:8888/api/vphone?phone=18046056459", null, function (data) {
console.info(data);
//手機號驗證成功之後 提交保存
getRequest("http://localhost:8888/api/save", null, function (data) {
console.info(data);
//TODO 如果還需要其他服務端驗證,則還需要繼續嵌套.....
});
});
})
後端代碼模擬:
@RestController
@CrossOrigin
public class CommonController {
/**
* 模擬驗證用戶名是否存在
*
* @param request
* @return
*/
@GetMapping("/api/vusername")
public Map validateUserName(HttpServletRequest request) {
String username = request.getParameter("username");
Map result = new HashMap();
result.put("data",username);
result.put("exists", true);
return result;
}
/**
* 模擬驗證手機號碼是否已經存在
*
* @param request
* @return
*/
@GetMapping("/api/vphone")
public Map validatePhone(HttpServletRequest request) {
String phone = request.getParameter("phone");
Map result = new HashMap();
result.put("data",phone);
result.put("exists", true);
return result;
}
/**
* 模擬保存
*
* @param request
* @return
*/
@GetMapping("/api/save")
public Map save(HttpServletRequest request) {
Map result = new HashMap();
result.put("msg", "保存成功");
return result;
}
}
//運行結果如下:
{data: "smith", exists: true}
{data: "18046056459", exists: true}
{msg: "保存成功"}
js中的這種寫法就是回調地獄。
二、解決回調嵌套問題(ES6 Promise 對象)
ES6的 Promise就是爲了解決回調地獄問題,它不是新的語法功能,而是一種新的寫法,是異步編程的一種解決方案,允許將回調函數的嵌套改成鏈式調用。
1.用console.dir列出promise對象所有的屬性方法如下:
可以看到 Promise是一個構造函數,可以通過 new Promise() 得到一個 Promise 的實例。
參照 阮一峯 出版的 《ECMAScript 6 入門》 介紹如下:
Promise簡單說就是一個容器,裏面保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。
Promise提供統一的API,各種異步操作都可以用同樣的方法進行處理。有了Promise對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。
2.Promise對象的狀態
Promise創建的實例是一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功 resolved)和rejected(已失敗)。
只有這個異步操作的結果可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
這個異步操作的最終結果只有2種可能:
(1)異步執行成功了(pending變爲fulfilled),需要在內部調用 成功的回調函數 resolve 把結果返回給調用者
(2) 異步執行失敗了(pending變爲rejected),需要在內部調用 失敗的回調函數 reject 把結果返回給調用者
3.Promise基本使用:創建一個promise實例如下:
var myPromise = new Promise(function (resolve, reject) {
//這裏寫具體的異步操作代碼
// ......
if (異步執行成功) {
//調用resolve 回調函數
resolve(成功結果);
} else {
//異步執行失敗 調用reject回調函數
reject(error);
}
});
Promise構造函數接收一個函數作爲參數,這個函數裏面寫的是異步操作代碼,這個函數的2個參數分別是resolve和reject。
在異步操作執行成功之後調用resolve回調函數,並將成功結果作爲參數傳遞出去。
在異步操作執行失敗之後調用reject回調函數,並將失敗報的錯誤作爲參數傳遞出去。
Promise創建實例之後,可以用then方法分別指定已成功狀態的回調函數 和 已失敗狀態 的回調函數,
其中reject回調函數可選,不一定要提供,這2個函數都接受Promise對象傳出的值作爲參數。
myPromise.then(function (data) {
//異步執行成功之後 執行的代碼
}, function (error) {
//異步執行失敗之後 執行的代碼
})
Promise 創建實例後就會立即執行
var myPromise = new Promise(function (resolve, reject) {
console.info("立即執行promise裏面的代碼")
resolve();
});
myPromise.then(function (data) {
//異步執行成功之後 執行的代碼
console.info("執行resolve回調函數代碼")
});
console.info("執行主程序代碼");
//結果爲:
立即執行promise裏面的代碼
執行主程序代碼
執行resolve回調函數代碼
爲了能夠控制promise對象的執行和使用Promise提供的API,將promise操作簡單封裝一下:
function doPromise(param) {
return new Promise(function (resolve, reject) {
console.info("立即執行promise裏面的代碼")
resolve();
});
}
console.info("執行主程序代碼");
doPromise("xxx").then(function (data) {
console.info("執行resolve回調函數代碼")
});
//結果爲:
執行主程序代碼
立即執行promise裏面的代碼
執行resolve回調函數代碼
4.then()方法介紹
從上面的截圖中可以看出prototype屬性上,有一個then()方法,所以只要是Promise構造函數創建的實例都可以調用then()方法。這個方法的作用是爲了promise實例添加狀態改變時的回調函數(預先指定回調)。
then()方法返回的是一個新的Promise實例,因此可以採用鏈式寫法,即then方法後面再調用另一個then方法(避免了回調地獄寫法)。
console.info("執行主程序代碼");
doPromise("xxx").then(function (data) {
console.info("執行resolve回調函數代碼")
// 第一個回調函數完成以後,會將返回結果作爲參數,傳入第二個回調函數
return "返回值";
}).then(function(data){
console.info(data);
console.info("執行第二個then的resolve方法")
})
//結果爲:
執行主程序代碼
立即執行promise裏面的代碼
執行resolve回調函數代碼
返回值
執行第二個then的resolve方法
如果前一個回調函數返回的還是一個Promise對象,這時後一個回調函數就會等待該promise對象的狀態發生改變,纔會被調用。用promise改寫開頭的需求如下:
function getRequest(url, data) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = function () {
//只要XHR對象的readyState屬性值發生改變,都會觸發一次readystatechange事件。
if (this.readyState !== 4) {
//readyState等於4 表示 請求已完成(可以訪問服務器響應並使用它)
return;
}
if (this.status === 200) {
//請求成功 調用
resolve(this.response);
} else {
//失敗時 調用
reject(new Error(this.statusText));
}
};
xhr.responseType = "json";
xhr.send(data);
});
}
getRequest("http://localhost:8888/api/vusername?username=smith", null)
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8888/api/vphone?phone=18046056459", null);
})
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8888/api/save", null);
})
.then(function (data) {
console.info(data);
});
//成功的執行結果爲:
{data: "smith", exists: true}
{data: "18046056459", exists: true}
{msg: "保存成功"}
5.catch()方法
從上面的截圖中可以看到prototype屬性上,有一個catch方法,用於指定發生錯誤時的回調函數。
如果前面的Promise執行失敗,但是希望不影響後續的Promise的正常執行,這時候可以單獨爲每個promise的.then方法指定一下失敗的回調函數。
如果後面的Promise執行依賴於前面的Promise執行的結果,前面失敗了,後面的promise就不需要繼續執行了,這時候一旦有promise操作報錯,就需要立即終止所有promise的執行,可以採用catch方法實現。
Promise 對象的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤總是會被下一個catch語句捕獲。建議總是使用catch方法,而不使用then方法的第二個參數。
第二個驗證地址改成不存在的地址:
getRequest("http://localhost:8888/api/vusername?username=smith", null)
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8080/api/vphone?phone=18046056459", null);
})
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8888/api/save", null);
})
.then(function (data) {
console.info(data);
})
.catch(function (error) {
console.info("報錯了!!", error);
});
//結果爲:
{data: "smith", exists: true}
報錯了!! Error
6.finally()方法
從上面的截圖中可以看到prototype屬性上,有一個finally方法,用於指定不管Promise對象最後狀態如何,都會執行的操作。這個是ES2018引入的新標準。
finally方法的回調函數不接受任何參數,不依賴於Promise的執行結果。
getRequest("http://localhost:8888/api/vusername?username=smith", null)
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8080/api/vphone?phone=18046056459", null);
})
.then(function (data) {
console.info(data);
return getRequest("http://localhost:8888/api/save", null);
})
.then(function (data) {
console.info(data);
})
.catch(function (error) {
console.info("報錯了!!", error);
})
.finally(function(){
console.info("不管前面執行的如何,這裏都會執行到!")
});
//執行結果:
{data: "smith", exists: true}
報錯了!! Error
不管前面執行的如何,這裏都會執行到!
7.基於Promise的ajax操作封裝
var http = {
get(url) {
return new Promise(function (resolve, reject) {
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
xhr.open("GET", url);
xhr.onreadystatechange = function () {
//只要XHR對象的readyState屬性值發生改變,都會觸發一次readystatechange事件。
if (this.readyState !== 4) {
//readyState等於4 表示 請求已完成(可以訪問服務器響應並使用它)
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
//失敗時 調用
reject(new Error(this.statusText));
}
};
xhr.responseType = "json";
xhr.send();
});
},
post(url, param) {
return new Promise(function (resolve, reject) {
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
xhr.open("POST", url);
xhr.onreadystatechange = function () {
//只要XHR對象的readyState屬性值發生改變,都會觸發一次readystatechange事件。
if (this.readyState !== 4) {
//readyState等於4 表示 請求已完成(可以訪問服務器響應並使用它)
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
//失敗時 調用
reject(new Error(this.statusText));
}
};
xhr.responseType = "json";
//TODO 在Chrome73版本中 設置這個Content-Type ,java後端 request.getParameter("empno"); 獲取不到數據,所以註釋掉
//不設置的話 默認是這個:Content-Type: multipart/form-data;
//xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(getFormData(param));
});
}
};
function getFormData(param) {
if (typeof param !== 'object') {
console.info("傳遞的參數不是個對象!")
return;
}
if (window.FormData) {
var formData = new FormData();
for (field in param) {
formData.append(field, param[field]);
}
return formData;
} else {
var paramArr = [];
var index = 0;
for (field in param) {
paramArr[index] = encodeURIComponent(field) + "=" + encodeURIComponent(param[field]);
index++;
}
return paramArr.join("&");
}
}
get調用如下:
http.get("http://localhost:8888/empno")
.then(response => {
console.info(response)
})
.catch(error => {
console.info(error);
});
// 成功調用結果爲:
{empno: 7369, ename: "SMITH", job: "CLERK", mgr: 7902, hiredate: "1980-12-17 00:00:00", …}
post調用如下:
http.post("http://localhost:8888/", {
empno: 6666,
ename: "測試名稱",
job: "測試崗位"
})
.then(response => {
console.info(response)
})
.catch(error => {
console.info(error);
});
//成功調用結果如下:
{message: "保存成功", status: "0"}
後端採用 SpringBoot2 如下:
* 僱員控制器
*
* @author David Lin
* @version: 1.0
* @date 2019-03-17 11:29
*/
@RestController
@CrossOrigin
public class EmpController {
/**
* 保存員工信息
*
* @param request
* @return
*/
@PostMapping("/")
public Map<String, Object> saveEmp(HttpServletRequest request) {
String empno = request.getParameter("empno");
String ename = request.getParameter("ename");
String job = request.getParameter("job");
Emp emp = new Emp();
emp.setEmpno(Integer.valueOf(empno));
emp.setEname(ename);
emp.setJob(job);
Map<String, Object> result = new HashMap<>(8);
empService.saveEmp(emp);
result.put("status", "0");
result.put("message", "保存成功");
return result;
}
}
本文部分內容引用了阮一峯的《ECMAScript 6 入門》