第十七章 异步操作和 async 函数
1. 基本概念
1.1 异步
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
1.2 回调函数
回调函数(callback
)就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
【2018-05-21|2018-05-21 Es6 笔记】回调函数的第一个参数,必须是错误对象err
(如果没有错误,该参数就是null
)?
原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。
1.3 Promise
Promise 的写法是回调函数的改进,使用then
方法以后,异步任务的两段执行看得更清楚了,Promise 的最大问题是代码冗余,太多的then
使语义不清。
var promise = new Promise(function(resolve, reject) {
// ... some codeif (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then((value)=>{
// 做正确处理
},(err)=>{
// 做错误处理
});
2. Generator 函数 2.1 协程的 Generator 函数实现
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用
yield
语句注明。每次调用
next
方法,会返回一个对象,表示当前阶段的信息(value
属性和done
属性)。value
属性是yield
语句后面表达式的值,表示当前阶段的值;done
属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。function* gen(x) {
var y = yield x + 2;
return y;
}var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
2.2 Generator 函数的数据交换和错误处理
Generator 函数向外输出数据,体现在
next
返回值的 value 属性上;next
方法还可以接受参数,向 Generator 函数体内输入数据。function* gen(x){
var y = yield x + 2;
return y;
}var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
2.2 异步任务的封装
Generator 函数的的缺点:流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
// How people build software.
3. Thunk 函数 3.1 参数的求值策略
"传值调用":即在进入函数体之前,就计算参数表达式的值,再将这个值传入函数。C 语言就采用这种策略。
“传名调用”:即直接将参数表达式传入函数体,只在用到它的时候求值。
传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。
3.2 Thunk 函数的含义
注意: react native不能识别Thunk关键字。将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
3.3 JavaScript 语言的 Thunk 函数
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
function f(a, cb) {
cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
3.4 Thunkify 模块
注意: 在win环境下install失败,在mac环境下未测试。生产环境的转换器,建议使用 Thunkify 模块。它主要多了一个检查机制,确保回调函数只运行一次。
function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3
3.5 Generator 函数的流程管理
yield
命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。3.6 Thunk 函数的自动流程管理
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}next();
}function* g() {
// ...
}run(g);
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入
run
函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield
命令后面的必须是 Thunk 函数。4. co 模块 4.1 基本用法
co 模块可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
上面代码中,Generator 函数只要传入
co
函数,就会自动执行。co
函数返回一个Promise
对象,因此可以用then
方法添加回调函数。co(gen).then(function (){
console.log('Generator 函数执行完成');
});
4.2 co 模块的原理
Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用
then
方法交回执行权。co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的
yield
命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co4.3 基于 Promise 对象的自动执行
注意:react-native不能直接引入fs模块,可以安装react-native-fs插件代替
var fs = require('react-native-fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/a.txt');
var f2 = yield readFile('./b.txt');
console.log(f1.toString());
console.log(f2.toString());
};
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}run(gen);
4.4 co 模块的源码
首先,co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。
在返回的 Promise 对象里面,co 先检查参数
gen
是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为resolved
。接着,co 将 Generator 函数的内部指针对象的
next
方法,包装成onFulfilled
函数。这主要是为了能够捕捉抛出的错误。最后,就是关键的
next
函数,它会反复调用自身。4.5 处理并发的异步操作
co 允许某些操作同时进行,等到它们全部完成,才进行下一步。这时,要把并发的操作都放在数组或对象里面,跟在
yield
语句后面。co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch((err)=>console.log(err));
// [ 1, 2 ]