异步编程
异步编程方式:
- 回调函数
- 事件监听
- Promise
- Generator
- async/swait
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
t.next(1); //第一次调用next函数时,传递的参数无效,故无打印结果
t.next(2); // a输出2;
t.next(3); // b输出3;
t.next(4); // c输出4;
t.next(5); // d输出5;
ES6 之后 ES7 中又提出了新的异步解决方案:async/await,async 是 Generator 函数的语法糖,async/await 的优点是代码清晰(不像使用 Promise 的时候需要写很多 then 的方法链),可以处理回调地狱的问题。async/await 写起来使得 JS 的异步代码看起来像同步代码,其实异步编程发展的目标就是让异步逻辑的代码看起来像同步一样容易理解
# Promise
# Promise.all()
- 当所有结果成功返回时按照请求顺序返回成功
- 当其中有一个失败方法时,则进入失败方法
# Promise.allsettled()
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function(results) {
console.log(results);
});
// 返回结果:
// [
// { status: 'fulfilled', value: 2 },
// { status: 'rejected', reason: -1 }
// ]
和 all 相比,allsetttled 执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功
Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方
# Promise.any()
any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态
- 所有的都返回 rejected,才会变成 rejected
- 只要有一个 fulflled,就会变成 fullfilled
看着和 all 正好相反
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const anyPromise = Promise.any([resolved, rejected]);
anyPromise.then(function(results) {
console.log(results);
});
// 返回结果:
// 2
只要其中一个 Promise 变成 fulfilled 状态,那么 any 最后就返回这个 Promise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2
# Promise.race()
race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数
我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断
//请求某个图片资源
function requestImg() {
var p = new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(img);
};
img.src = "http://www.baidu.com/img/flexible/logo/pc/result.png";
});
return p;
}
//延时函数,用于给请求计时
function timeout() {
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
reject("图片请求超时");
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results) {
console.log(results);
})
.catch(function(reason) {
console.log(reason);
});
# Generator
function* gen() {
console.log("enter");
let a = yield 1;
let b = yield (function() {
return 2;
})();
return 3;
}
var g = gen(); // 阻塞住,不会执行任何语句
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: true }
// { value: undefined, done: true }
# thunk 函数
let isString = (obj) => {
return Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
return Object.prototype.toString.call(obj) === '[object Function]';
};
let isArray = (obj) => {
return Object.prototype.toString.call(obj) === '[object Array]';
};
....
可以看到,其中出现了非常多重复的数据类型判断逻辑,平常业务开发中类似的重复逻辑的场景也同样会有很多。我们将它们做一下封装
let isType = (type) => {
return (obj) => {
return Object.prototype.toString.call(obj) === `[object ${type}]`;
};
};
let isString = isType("String");
let isArray = isType("Array");
isString("123"); // true
isArray([1, 2, 3]); // true
相应的 isString 和 isArray 是由 isType 方法生产出来的函数,通过上面的方式来改造代码,明显简洁了不少。像 isType 这样的函数我们称为 thunk 函数,它的基本思路都是接收一定的参数,会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能
# Generator 和 thunk
const readFileThunk = (filename) => {
return (callback) => {
fs.readFile(filename, callback);
};
};
const gen = function*() {
const data1 = yield readFileThunk("1.txt");
console.log(data1.toString());
const data2 = yield readFileThunk("2.txt");
console.log(data2.toString);
};
let g = gen();
g.next().value((err, data1) => {
g.next(data1).value((err, data2) => {
g.next(data2);
});
});
readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了。上面第三段代码执行起来嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可读性不强,因此我们有必要把执行的代码封装优化一下
function run(gen) {
const next = (err, data) => {
let res = gen.next(data);
if (res.done) return;
res.value(next);
};
next();
}
run(g);
改造完之后,我们可以看到 run 函数和上面的执行效果其实是一样的。代码虽然只有几行,但其包含了递归的过程,解决了多层嵌套的问题,并且完成了异步操作的一次性的执行效果。这就是通过 thunk 函数完成异步操作的情况
# Promise 和 thunk
// 最后包装成 Promise 对象进行返回
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then((res) => res);
};
let g = gen();
// 这块和上面 thunk 的方式一样
const gen = function*() {
const data1 = yield readFilePromise("1.txt");
console.log(data1.toString());
const data2 = yield readFilePromise("2.txt");
console.log(data2.toString);
};
// 这块和上面 thunk 的方式一样
function run(gen) {
const next = (err, data) => {
let res = gen.next(data);
if (res.done) return;
res.value.then(next);
};
next();
}
run(g);
# co 函数库
co 函数库是著名程序员 TJ 发布的一个小工具,用于处理 Generator 函数的自动执行。核心原理其实就是上面讲的通过和 thunk 函数以及 Promise 对象进行配合,包装成一个库
它使用起来非常简单,比如还是用上面那段代码,第三段代码就可以省略了,直接引用 co 函数,包装起来就可以使用了
const co = require("co");
let g = gen();
co(g).then((res) => {
console.log(res);
});
# 那么为什么 co 函数库可以自动执行 Generator 函数,它的处理原理是什么呢?
- 因为 Generator 函数就是一个异步操作的容器,它需要一种自动执行机制,co 函数接受 Generator 函数作为参数,并最后返回一个 Promise 对象
- 在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数;如果不是就返回,并将 Promise 对象的状态改为 resolved
- co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulfilled 函数。这主要是为了能够捕捉抛出的错误
- 关键的是 next 函数,它会反复调用自身
# async/await
JS 的异步编程从最开始的回调函数的方式,演化到使用 Promise 对象,再到 Generator+co 函数的方式,每次都有一些改变,但又让人觉得不彻底,都需要理解底层运行机制。
而 async/await 被称为 JS 中异步终极解决方案,它既能够像 co+Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无须借助任何第三方库
// readFilePromise 依旧返回 Promise 对象
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then((res) => res);
};
// 这里把 Generator的 * 换成 async,把 yield 换成 await
const gen = async function() {
const data1 = await readFilePromise("1.txt");
console.log(data1.toString());
const data2 = await readFilePromise("2.txt");
console.log(data2.toString);
};
从上面的代码中可以看到,虽然我们简单地将 Generator 的 * 号换成了 async,把 yield 换成了 await,但其实 async 的内部做了不少工作。我们根据 async 的原理详细拆解一下,看看它到底做了哪些工作。
总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点
- 内置执行器:Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行
- 适用性更好:co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束
- 可读性更好:async 和 await,比起使用 * 号和 yield,语义更清晰明了
async function func() {
return 100;
}
console.log(func());
// Promise {<fulfilled>: 100}
从执行的结果可以看出,async 函数 func 最后返回的结果直接是 Promise 对象,比较方便让开发者继续往后处理。而之前 Generator 并不会自动执行,需要通过 next 方法控制,最后返回的也并不是 Promise 对象,而是需要通过 co 函数库来实现最后返回 Promise 对象。
这样看来,ES7 加入的 async/await 的确解决了之前的问题,使开发者在编程过程中更容易理解,语法更清晰,并且也不用再单独引用 co 函数库了。因此用 async/await 写出的代码也更加优雅,相比于之前的 Promise 和 co+Generator 的方式更容易理解,上手成本也更低,不愧是 JS 异步的终极解决方案