2018-05-21|2018-05-21 Es6 笔记

第十七章 异步操作和 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 对象,也可以使用 co
4.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 ]

    推荐阅读