JavaScript
是天生异步的,其可以先执行程序的主要逻辑,将耗时的操作推迟执行,相对同步来说,这样可以优化体验和提高性能。
1. 回调函数
回调函数我们接触过很多了,它非常简单、容易理解和部署,但确定也十分明显:不利于代码的阅读和维护,各个部分之间高度耦合,流程混乱。
function xxx(param){
console.log('我是高阶函数');
// 上面的代码执行完,去调用另一个函数
param();
}
function yyy() {
console.log('我是回调函数');
}
xxx(yyy);
回调函数是使用比较频繁的,比如:ajax、Node的文件操作等。其实回调函数大多数情况下并没有减少执行时间,只是将更耗时的放到后面执行了而已。
2. 事件监听(发布/订阅)
采用事件驱动模式,使任务的执行不取决代码的顺序,而取决于某一个事件是否发生。好处是比较容易理解,耦合度低。缺点就是使整个程序都要变成事件驱动型,运行流程会变得不清晰。
2.1 浏览器中的JavaScript的事件
element.onclick=function(){
//处理函数
}
2.2 Node中的events模块
// 简单使用
const events = require('events');
let e1 = new events.EventEmitter();
// 注册一个叫xxx的事件监听器
e1.on('xxx', function() {
console.log('监听器开始处理');
});
console.log('监听器已打开');
// 触发监听器执行
e1.emit('xxx');
3. Promise对象
我们前面介绍的回调函数和事件,其实是有很多问题的,比如:
- 函数的return无效了,为了完成一些相互依赖的逻辑,只能一层一层的再嵌套回调函数回调
- 多次嵌套,进入回调地狱,代码耦合度加深
- 代码可读性差,维护困难
- ……
为了解决上面的问题,方便地获取异步操作消息,并且为异步编程提供统一的API,社区很早就提出了Promise,最终在ES6中增加了Promise规范。
3.1 Promise的状态
Promise对象有3中状态:
- Pending:进行中 / 未完成 (初始就是这个状态)
- Resolved:已完成
- Rejected:已失败
3.2 基本语法
/**
* Promise对象传入的函数有两个参数,这两个参数都必须是函数对象
* 参数1:状态由“未完成” 变成 “已完成” 时要执行的函数,可以理解为成功时要执行的函数对象
* 参数2:状态由“未完成” 变成 “已失败” 时要执行的函数,可以理解为失败时要执行的函数对象
*/
let p = new Promise(function (resolve, reject) {
let data = {name: '嗨!小子'};
if(1 > 2){
// 调用成功时的回调函数
data.status = 'success';
resolve(data);
}else{
// 调用失败时的回调函数
data.status = 'error';
reject(data);
}
});
/**
* 使用实例的then方法分别指定resolved状态和rejected状态的回调函数
* 参数1(必选):Promise对象状态由“未完成” 变成 “已完成” 时执行的回调函数
* 参数2(可选):Promise对象状态由“未完成” 变成 “已失败” 时执行的回调函数
*/
p.then(
function (value) {
console.log('这是成功时要执行的内容');
console.log(value);
},
function (value) {
console.log('这是失败时要执行的内容');
console.log(value);
}
)
从语法中我们可以看到,我们的基本逻辑要写到Promise
对象中,我们的回调函数定义到then()
方法中了。和我们之前直接使用回调函数差不多。不过,then()
方法返回的是一个新的Promise对象,因此,then()
方法可以多次链式调用。
3.3 例子讲解
我们看一个例子:
需求:
从1.txt
中读取文本,然后在末尾加个*号,然后写到2.txt
中,如果上一步写入成功,则在log.txt
中记录写入成功,否则提示错误。
const fs = require('fs');
// 我们有一个需求:
// 从1.txt中读取文本,然后在末尾加个*号,然后写到2.txt中
// 如果上一步写入成功,则在log.txt中记录写入成功,否则提示错误
fs.readFile('./1.txt', 'utf-8', function (err, data) {
if(!err){
data += '*';
fs.writeFile('./2.txt', data, function (err) {
if(err){
console.log('文件写入失败');
}else{
fs.writeFile('./log.txt', `数据处理完成并写入成功,共写入${data.length}个字符`, function (err) {
if(err){
console.log('日志记录失败');
}else{
console.log('OK');
}
})
}
})
}else{
console.log('文件读取失败');
}
})
我们可以看出,上面的需求,每一步都依赖上一步的操作,使用回调函数是可以处理的,不过代码写的嵌套太多了,倘若一个需求有十来步,每一步都需要依赖前一步的结果,那这样写,能把人写疯。接下来我们尝试用Promise
去处理:
let p = new Promise(function (successCallback, errorCallback) {
fs.readFile('./1.txt', 'utf-8', function (err, data) {
if (err) {
// 执行错误的
errorCallback(err);
} else {
successCallback(data);
}
});
})
p.then(
function (data) {
// 使用return返回普通数据的话,返回的数据会被下一个then()接收
// 读取成功,处理数据,并将处理后的数据返回
return data += '*';
},
function (err) {
console.log(err);
}
)
.then(
// 这里可以只有一个参数
function (data) {
// 接收上一步的数据,并做下一步处理
// 如果这里的是一个Promise对象的话,会自动调用下一个then()
return new Promise(function (successFun, errorFun) {
fs.writeFile('./2.txt', data, function (err) {
if (err) {
errorFun(err);
} else {
successFun(data.length);
}
});
});
}
).then(
function (data) {
fs.writeFile('./log.txt', `数据处理完成并写入成功,共写入${data}个字符`, function (err) {
if (err) {
console.log('日志记录失败');
} else {
console.log('OK');
}
})
},
function (data) {
console.log(data);
console.log('文件写入失败');
}
)
我们可以看到,Promise可以将多层的回调函数改造成链式的操作,不过仍然需要写一些回调函数。
3.4 其它方法
3.4.1 实例方法:
.then()
:获取异步任务的正确结果(第二个参数可以获取异常结果)
.catch()
:获取异步任务的异常结果
.finally()
:无论结果成功或者失败都会执行(部分运行环境,可能不一定支持)
3.4.2 对象(静态)方法
Promise.all()
: 并发处理多个异步任务(一次性启动多个任务),所有任务执行完毕后得到结果。
Promise.race()
: 并发处理多个异步任务(一次性启动多个任务),只要一个任务执行完毕就得到结果。
这两个方法的使用方式一模一样,只是返回值返回的时机不同。
Promise.all([new Promise(function(x, y){}),
new Promise(function(x, y){}), ……])
.then(function(result){
});
Promise.race([new Promise(function(x, y){}),
new Promise(function(x, y){}), ……])
.then(function(result){
});
3.5 使用Promise简单改造ajax操作
function ajaxGet(url) {
return new Promise(function (success, error) {
let xhr = new XMLHttpRequest();
// 监听readyState属性值的变化
xhr.onreadystatechange = function () {
// 请求响应完毕
if (xhr.readyState === 4) {
if (xhr.status == 200) {
success(xhr.response);
} else {
error(xhr.status)
}
}
}
// 创建请求
xhr.open('GET', url);
xhr.responseType = 'json';
// 发送请求
xhr.send()
});
}
ajaxGet('http://127.0.0.1')
.then(
function(data){
return parseInt(data) + 7777;
}
)
.then(
function(x){
console.log(x);
}
)
4.async/await 语法糖
该语法是ES7新增的,async/await
是基于生成器和promise
实现的。是目前最简洁的编写异步代码的方式了。
// 基本语法
async function DoSomething() {
let data = await doFirst(); // 第1步操作 执行一个异步操作得到结果
let data2 = await doSecond(data); // 第2步操作 执行一个异步操作得到结果
let data3 = await odthird(data2); // 第3步操作 执行一个异步操作得到结果
return data3
}
上面的代码可以看出,如果都是异步操作,下一步的操作依赖上一步的结果,只要编写的时候加上async
和await
关键词,感觉就像写同步代码一样。
从语法上讲:async
关键字用来修饰函数,这种函数的返回值是一个promise对象,await
关键字用在 async function
函数内容,表示得到异步操作的结果。
async function xxx(){
}
// 是个Promise对象
console.log(xxx());
async function xxx(){
return '你长得真好看,向天边的花一样';
}
// 返回值依然是个Promise对象
console.log(xxx());
4.1 例子
看前面使用我们封装的ajaxGet函数进行的操作:我们先远程请求一个数据,然后用异步返回来的数据加上 8888, 得到结果打印出来,我们应该这样写代码:
ajaxGet('http://127.0.0.1')
.then(
// 使用回调函数进行后续操作
function (data) {
console.log(parseInt(data) + 7777);
}
)
我们发现,我们仍然要写一个回调函数,业务逻辑写到回调函数中。如果我们使用async
/await
语法糖的话,可以怎么处理呢?看代码:
async function xxx(){
// 获得异步操作的结果
let tmp = await ajaxGet('http://127.0.0.1');
console.log(tmp + 8888);
}
xxx();
我们发现,使用async
/await
语法糖,我们不用手动写回调函数了,是不是很方便。