16.9、Generator与协程、Generator应用
/*
协程(coroutine)是一种程序运行方式,可以理解成 “协作的线程” 或 “协作的函数”。
协程既可以用单线程实现,也可以用多线程实现;前者是一种特殊的子例程,后者是一种特殊的线程。
*/
/*
协程与子例程的差异:
传统的 “子例程” 采用堆栈式 “后进先出” 的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态,线程(或函数)之间可以交换执行权。
也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行。
协程与普通线程的差异:
不难看出,协程适用于多任务运行的环境。
在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变。
它们的不同之处在于,同一时间可以有多个线程处于运行态,但是运行的协程只能有一个,其他协程都处于暂停态。
此外,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于 Javascript 是单线程的语言,只能保持一个调用栈。
引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。
不至于像异步操作的回调函数那样,一旦出错原始的调用栈早就结束。
Generator 函数是 es6 对协程的实现,但属于不完全实现。
Generator 函数被称为 “半协程”,意思是只有 Generator 函数的调用者才能将程序的执行权还给 Generator 函数,
如果是完全实现的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用 yield 语句交换控制权。
*/
/*
16.10 Generator 的应用
16.10.1 异步操作的同步化表达
Genarator 函数的暂停执行效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。
这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用 next 方法时再执行。
所以,Generator 函数的一个重要实际意义就是用于处理异步操作,改写回调函数。
*/
function* loadUI() {
showLoadingScreen()
yield loadUIDataAsynchronously()
hideLoadingScreen()
}
var loader = loadUI()
// 加载 UI
loader.next()
// 卸载 UI
loader.next()
/*
上面的代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。
下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。
等到数据加载完成,再一次使用 next 方法,则会隐藏 loading 界面。
可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。
AJAX 是典型的异步操作,通过 Generator 函数部署 AJAX 操作,可以用同步的方式表达。
*/
function* main() {
var result = yield request('http://some.url')
var response = JSON.parse(result)
console.log(response.value)
}
function request(url) {
makeAjaxCall(url, function(response) {
it.next(response)
})
}
var it = main()
it.next()
// 上面的 main 函数就是通过 AJAX 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。
// 注意:makeAjaxCall 函数中的 next 方法必须加上 response 参数,因为 yield 语句构成的表达式本身是没有值的,总是等于 undefined
// 下面是另一个例子,通过 Generator 函数逐行读取文本文件
function* numbers() {
let file = new FileReader('numbers.txt')
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10)
}
} finally {
file.close()
}
}
// 上面的代码打开文本文件,使用 yield 语句可以手动逐行读取文件
/*
16.10.2 控制流管理
如果有一个多步操作非常耗时,采用回调函数可能会写成下面这样。
*/
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
})
})
})
})
// 采用 Promise 改写上面的代码如下:
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done()
// 上面的代码已经把回调函数改成了直线执行的形式,但是加入了大量 Promise 的语法。
// Generator 函数可以进一步改善代码运行流程
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1)
var value3 = yield step2(value2)
var value4 = yield step3(value3)
var value5 = yield step4(value4)
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
// 然后,使用一个函数按次序自动执行所有步骤
scheduler(longRunningTask(initialValue))
function scheduler(task) {
var taskObj = task.next(task.value)
// 如果 Generator 函数未结束,就继续调用
if(!taskObj.done) {
task.value = taskObj.value
scheduler(task)
}
}
/*
注意:上面的这种做法只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。
因为这里的代码一得到返回值就继续往下执行,没有判断异步操作时完成。
*/