前端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就是为了解决回调地狱问题,它不是新的语法功能,而是一种新的写法,是异步编程的一种解决方案,允许将回调函数的嵌套改成链式调用。
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 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 result = new HashMap<>(8);
empService.saveEmp(emp);
result.put("status", "0");
result.put("message", "保存成功");
return result;
}
}
本文部分内容引用了阮一峰的《ECMAScript 6 入门》