Generator函数的异步应用
JavaScript 是单线程语言,如果没有异步编程,则会造成卡死,因此需要异步编程。
传统方法
在之前,异步编程的实现主要通过以下几个方法:
- 回调函数
- 事件监听
- 发布/订阅
Promise
对象
基本概念
异步
异步就是不连续的执行一个任务,一个任务被分为几段,一开始先执行一段,然后暂停去执行其他任务,最后再回来执行剩下的任务。
常见的示例有读取文件、网络请求等。第一段任务都是发送请求(读取文件权限请求和向服务器发送网络请求),等到请求通过后才能开始执行第二段。
这种不连续执行的任务就叫异步任务。相反的,任务连续执行的就是同步任务。
回调函数
JavaScript 中,异步任务是通过回调函数来实现的。回调函数就是将第二段任务写进一个函数内,在第一段任务执行完毕后通过回调函数执行第二段任务。回调函数英文名为 callback
,意为“被调用的函数”。
window.addEventListener('load', () => {
// some code...
});
上面的代码示例中,addEventListener
第二个参数就是回调函数,等到窗口加载完毕 load
事件触发后,执行该回调函数。
Promise
回调函数本身没问题,但是如果回调函数嵌套太多,会造成回调地狱问题,代码会变得难以阅读和维护。
window.addEventListener('load', () => {
input.addEventListener('click', () => {
axios({
url: 'xxx',
method: 'get',
success: (res) => {
img.onload = () => {
img.src = res.data;
}
}
})
})
});
可以看出,代码层层嵌套,耦合性很高,后续只要有一个操作发生改变,会牵扯它上层回调和下层回调。Promise
就是为了解决这个问题,Promise
允许将回调函数写成链式调用。
window.addEventListener('load', () => {
input.addEventListener('click', () => {
axios({
url: 'xxx',
method: 'get'
})
.then(res => {
img.onload = () => {
img.src = res.data;
}
})
})
});
虽然这样改了之后看起来相对更清楚了,但是如果 then
链式太多,一眼看出都是 then
的堆积,原语义变得很不清楚。
Generator函数
协程
传统的编程语言早已有异步编程(多任务)的解决方案,其中一种叫协程,多个线程协作完成异步任务。运行思路如下:
- 协程A开始执行
- 协程A执行到一半,暂停执行,执行权交给协程B
- 协程B执行到一半,暂停执行,执行权交给协程A
- 协程A恢复执行
可以看出,协程A就是分为两段的异步任务。
function * fn () {
// some code
yield readFile('xxx');
// some code
}
上方示例代码协程遇到 yield
暂停执行去读取文件。读取完毕后再返回继续执行。
协程Cenerator函数的实现
Generator函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(即暂停执行)。Generator函数内部可以暂停执行(yield
),也可以恢复执行(next
)。
function * fn () {
yield 1;
yield 2;
yield 3;
}
let g = fn();
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: undefined, done: true}
next
方法的作用是分阶段执行 Generator
函数。每次调用 next
方法都会返回 个对象,表示当前阶段的信息(value
属性和 done
属性)。value
属性是 yield
语句后面表达式的值,表示当前阶段的值;done
属性是一个布尔值,表示 Generator
函数是否执行完毕,即是否还有下个阶段。
Generator函数的数据交换和错误处理
Generator
函数可以暂停执行,也可以恢复执行,这意味着可以在暂停执行的地方交出数据,也可以在恢复执行的地方接收数据。除此之外,Generator
函数还能函数体内外数据交换和错误机制处理。
数据交换
jsfunction * fn (x) { let y = yield x + 2; return y } let g = fn(1); g.next(); // {value: 3, done: false} g.next('a'); // {value: 'a', done: true} g.next('b'); // {value: undefined, done: true}
错误处理
jsfunction * fn () { try { yield; } catch (e) { console.log(e); } } let g = fn(); g.next(); g.throw('出错了'); // 出错了
异步任务封装
下面先写一段异步读取文件的代码:
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile('/etc/fstab'); // 读取文件1
var f2 = yield readFile('/etc/shells'); // 读取文件2
};
let result1 = gen().next()
let result2 = gen().next()
result1.value.then(data => {
console.log(data);
// some code
})
result2.value.then(data => {
console.log(data);
// some code
})
可以看到,整体代码和同步任务的写法非常类似,除了增加 yield
命令之外。使用 next
方法,依次执行每个异步任务。虽然这样异步操作很简洁,但是流程管理很不方便,即何时执行、何时返回、何时抛出错误,以及异常处理等。
Thunk函数
Thunk
函数是自动执行 Generator
函数的一种方法。
参数的求值策略
编程语言中,函数的参数什么时候求值比较好一直是一个争论点。
let x = 1
function f(n) {
return n + 1
}
f(x + 5)
目前有两个流派:
参数早求值,又称传值调用(call by value):函数参数在传入函数体之前就计算出来
以上方代码为例,传值调用是进入函数体前先计算
x + 5
,然后再传入函数f
。实际上等同于下方代码。jslet x = 1 function f(n) { return n + 1 } f(6)
参数惰性求值,又称传名调用(call by name):函数参数在函数体内部求值
传名调用是把参数传入函数体,用到的时候再求值。等同于下方代码。
jslet x = 1 function f(m) { return x + 5 + 1 } f(x + 5)
二者各有利弊,传值调用比较简单,但是有可能函数内根本用不到,造成性能损失。传名调用在函数体内部,可以方便的引用任何函数,但是实现起来比较复杂。
Thunk函数的含义
编译器的 “传名调用” 实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk
函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
Thunk
函数就是传名调用的一种策略,可用来替换某个表达式。
JavaScript的Thunk函数
JavaScript 语言是传值调用,它的 Thunk
函数含义有所不同。在 JavaScript 中, Thunk
函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
经过转换器处理,它变成了单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk
函数。任何函数只要参数有回调函数,都可以写成 Thunk
函数的形式。
Thunkify模块
生产环境的转换器,建议使用 Thunkify
模块。首先 npm i thunkify
下载依赖。,使用方式如。
import thunkify from 'thunkify';
import fs from 'fs';
var readFile = thunkify(fs.readFile);
readFile('/etc/fstab')(function (err, data) {
if (err) throw err;
console.log(data.toString());
});
Thunkify 源码
function thunkify (it) {
return function () {
let args = new Array(arguments.length);
let self = this;
for(let i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return function (done) {
let called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
it.apply(self, args);
} catch (ex) {
done(ex);
}
}
}
}
Generator函数的流程管理
有了 Generator
函数后,Thunk
函数便有了用武之地,自动流程管理 Generator
函数。
自动流程管理 代码
import thunkify from 'thunkify';
import fs from 'fs';
var readFile = thunkify(fs.readFile);
var gen = function* () {
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
let g = gen();
let r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
let r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
上面的代码中,函数封装了一个异步的读取文件操作,只要执行 run
函数,这些操作就 会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且只需要一行代码就可以执行。 Thunk
函数井不是 Generator
函数自动执行的唯一方案,因为自动执行的关键是,必须有种机制自动控制 Generator
函数的流程,接收和交还程序的执行权,回调函数可以做到这点,Promise
对象也可以做到。
co模块
co
模块可以让你不用编写 Generator
函数的执行器。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen).then(function () {
console.log('Generator 函数执行完成');
});
co
模块其实就是将两种自动执行器(Thunk
函数和 Promise
对象)包装成一个模块。使用 co
的前提条件是,Generator
函数的 yield
命令后面,只能是 Thunk
函数或 Promise
对象。
如果数组或对象的成员,全部都是 Promise
对象,也可以使用 co
函数。
总结
JavaScript 是单线程语言,需要异步编程来避免阻塞。传统的异步编程方法包括回调函数、事件监听、发布/订阅和 Promise
对象。
Generator
函数是 ES6 中实现协程的方式,允许函数执行权的交出和恢复,通过 yield
和 next
实现。
Generator
函数可以用于异步任务封装,使得异步代码的写法更接近同步代码。 Thunk
函数是将多参数函数转换为单参数版本的函数,常用于自动执行 Generator
函数。
co
模块是一个自动执行器,可以处理 Generator
函数中的 Thunk
函数和 Promise
对象,简化异步操作的流程管理。