JavaScript中的异步编程
一、回调函数
因为在JS中函数是一等公民,所以它可以像其他变量一样作为参数进行传递。例如下方这段登录时的业务逻辑代码:
let key, token, userId;
$.ajax({
type: 'get',
url: 'http://localhost:3000/apiKey',
success: function (data) {
key = data;
$.ajax({
type: 'get',
url: 'http://localhost:3000/getToken',
data: {
key: key
},
success: function (data) {
token = data.token;
userId = data.userId;
$.ajax({
type: 'get',
url: 'http://localhost:3000/getData',
data: {
token: token,
userId: userId
},
success: function (data) {
console.log('业务数据:', data);
},
error: function (err) {
console.log(err);
}
});
},
error: function (err) {
console.log(err);
}
});
},
error: function (err) {
console.log(err);
}
});
整段代码充满了回调嵌套,代码不仅在纵向扩展,横向也在扩展,这就是我们常说的回调地狱(Callback Hell)
二、Promise
ES6的Promise也好,jQuery的Promise也好,不同的库有不同的实现,但是大家遵循的都是同一套规范,所以,Promise并不指特定的某个实现,它是一种规范,是一套处理JavaScript异步的机制。
用Promise的方式重构上面那段代码:
let getKeyPromise = function () {
return new Promise((resolve, reject) => {
$.ajax({
type: 'get',
url: 'http://localhost:3000/apiKey',
success: function (data) {
let key = data;
resolve(key);
},
error: function (err) {
console.log(err);
}
});
});
}
let getTokenPromise = function (key) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getToken',
data: {
key: key
},
success: function (data) {
resolve(data);
},
error: function (err) {
console.log(err);
}
});
});
}
let getDataPromise = function(data) {
let token = data.token;
let userId = data.userId;
return new Promise((resolve, reject) => {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getData',
data: {
token: token,
userId: userId
},
success: function (data) {
resolve(data);
},
error: function (err) {
console.log(err);
}
});
});
}
getKeyPromise()
.then(key => {
return getTokenPromise(key);
})
.then(data => {
return getDataPromise(data);
})
.then(data => {
console.log("业务数据" + data);
})
.catch(err => {
console,log(err);
});
Promise去除了横向扩展,无论有再多的业务依赖,通过多个then(…)来获取数据,再一点就是逻辑性更明显,层级比较清晰,Promise在一定程度上解决了回调函数的书写结构问题,但回调函数依然在主流程上存在,只不过都放到了then(…)里面。
首先明确一点,Promise可以保证以下情况,引用自JavaScript | MDN:
在JavaScript事件队列的当前运行完成之前,回调函数永远不会被调用
通过 .then 形式添加的回调函数,甚至都在异步操作完成之后才被添加的函数,都会被调用
通过多次调用 .then,可以添加多个回调函数,它们会按照插入顺序并且独立运行
三、生成器函数Generator
一种顺序、看似同步的异步流程控制表达风格,这就是ES6中的生成器(Gererator)。
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
yield表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
可迭代协议和迭代器协议
了解Generator之前,必须先了解ES6新增的两个协议:可迭代协议和迭代器协议。
可迭代协议
可迭代协议运行JavaScript对象去定义或定制它们的迭代行为,例如(定义)在一个for…of结构中什么值可以被循环(得到)。以下内置类型都是内置的可迭代对象并且有默认的迭代行为:
Array
Map
Set
String
TypedArray
函数的Arguments对象
NodeList对象
注意,Object不符合可迭代协议。
为了变成可迭代对象,一个对象必须实现@@iterator方法,意思是这个对象(或者它原型链prototype chain上的某个对象)必须有一个名字是Symbol.iterator的属性:
属性 | 值 |
---|---|
[Symbol.iterator] | 返回一个对象的无参函数,被返回对象符合迭代器协议 |
迭代器协议
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。
属性 | 值 |
---|---|
next | 返回一个对象的无参函数,被返回对象拥有两个属性 |
其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束,遍历结束时value为undefined,done为true。
再次重构上面打代码:
function getKey () {
$.ajax({
type: 'get',
url: 'http://localhost:3000/apiKey',
success: function (data) {
key = data;
it.next(key);
}
error: function (err) {
console.log(err);
}
});
}
function getToken (key) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getToken',
data: {
key: key
},
success: function (data) {
loginData = data;
it.next(loginData);
}
error: function (err) {
console.log(err);
}
});
}
function getData (loginData) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getData',
data: {
token: loginData.token,
userId: loginData.userId
},
success: function (busiData) {
it.next(busiData);
}
error: function (err) {
console.log(err);
}
});
}
function *main () {
let key = yield getKey();
let LoginData = yield getToken(key);
let busiData = yield getData(loginData);
console.log('业务数据:', busiData);
}
// 生成迭代器实例
var it = main();
// 运行第一步
it.next();
console.log('不影响主线程执行');
四、Async/Await
上面我们介绍了Promise和Generator,把这两者结合起来,就是Async/Await。
Generator的缺点是还需要我们手动控制next()执行,使用Async/Await的时候,只要await后面跟着一个Promise,它会自动等到Promise决议以后的返回值,resolve(…)或者reject(…)都可以
用Async/Await的方式改写上面的代码:
let getKeyPromise = function () {
return new Promsie(function (resolve, reject) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/apiKey',
success: function (data) {
let key = data;
resolve(key);
},
error: function (err) {
reject(err);
}
});
});
};
let getTokenPromise = function (key) {
return new Promsie(function (resolve, reject) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getToken',
data: {
key: key
},
success: function (data) {
resolve(data);
},
error: function (err) {
reject(err);
}
});
});
};
let getDataPromise = function (data) {
let token = data.token;
let userId = data.userId;
return new Promsie(function (resolve, reject) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getData',
data: {
token: token,
userId: userId
},
success: function (data) {
resolve(data);
},
error: function (err) {
reject(err);
}
});
});
};
async function main () {
let key = await getKeyPromise();
let loginData = await getTokenPromise(key);
let busiData = await getDataPromise(loginData);
console.log('业务数据:', busiData);
}
main();
console.log('不影响主线程执行');
Async/Await是Generator和Promise的组合,完全解决了基于回调的异步流程存在的两个问题,可能是现在最好的JavaScript处理异步的方式了。
总结
JavaScript异步编程的发展历程阶段:
1. 第一个阶段
回调函数,但会导致两个问题:
- 缺乏顺序性:回调地狱导致的调试困难,和大脑的思维方式不符
- 缺乏可信任性:控制反转导致的一系列信任问题
2. 第二个阶段
Promise,Promise是基于PromiseA+规范的实现,它很好的解决了控制反转导致的信任问题,将代码执行的主动权重新拿了回来。
3. 第三个阶段
生成器函数Generator,使用Generator,可以让我们用同步的方式来书写代码,解决了顺序性的问题,但是需要手动去控制next(…),将回调成功返回的数据送回JavaScript主流程中。
4. 第四个阶段
Async/Await,Async/Await结合了Promise和Generator,在await后面跟一个Promise,它会自动等待Promise的决议值,解决了Generator需要手动控制next(…)执行的问题,真正实现了用同步的方式书写异步代码。