在之前的文章介绍了传统异步的实现方案,本文将介绍 ES6 中的一种全新的异步方案 –Generator 函数。
generator 简介
简单介绍一下 generator 的原理和语法,(更详细内容请看 ECMAScript 6 入门,本文只介绍和异步相关的核心内容)
基本语法
通过一个简单的例子来了解 generator
函数
function* MyGenerator() {
yield 'yield 的含义是: 执行此处时,暂停执行当前函数'
yield '暂停之后的函数可以用 next 方法继续执行'
return '遇到 return 之后会真正结束,done 会变成 true'
}
const gen = MyGenerator() // 执行后返回的是一个指针,可以利用该指针执行 next 方法
console.log(gen.next()) // {value: "yield 的含义是: 执行此处时,暂停执行当前函数“, done: false}
console.log(gen.next())// {value: "暂停之后的函数可以用 next 方法继续执行", done: false}
console.log(gen.next())// {value: "遇到 return 之后会真正结束,done 会变成 true", done: true}
数据交互
数据交互指的是 可以通过 yield 把当前执行的结果传出,也可以使用 next 把外部参数传入
function* MyGenerator(){
const result = yield '传出的数据'
return result
}
const gen = MyGenerator()
console.log(gen.next())// generatorAndAsync2.html:19 {value: "传出的数据", done: false}
console.log(gen.next('传入的数据'))// {value: "传入的数据", done: true}
交互的参数类型当然也可以是 函数:
function sayHi(){console.log('hi');
}
function* MyGenerator(){const result = yield sayHi()
return
}
const gen = MyGenerator()
const result = gen.next()// {value: sayHi, done: false}
result.value() // 正常输出 'hi'
具备以上最核心的两个特性之后,generator 就可以进行异步操作封装。
异步任务封装
首先,结合异步任务的特点以及前文提到的 genrator 函数的特性,提炼出使用 generator 封装异步操作的核心思路:
- 在异步任务执行时,使用
yield
交出执行权 - 在异步任务结束后,使用
next
交还执行权
起步
从一个最简单的例子开始:
// 1. 首先写一个异步任务, 在一秒后返回特定字符串
function asyncTask(callback){setTimeout(()=>{callback('Hello Leo')
}, 1000)
}
// 2. 接下来写出期望执行的顺序
function* runTask() {
let text = yield asyncTask
console.log(text) // 我们期望这里正常输出 Hello Leo
}
// 3. 按照期望值执行函数
const gen = runTask()// 此时执行权已经交出
gen.next().value(function (text) {// 执行 asyncTask 并传入 callback,关键点在于在 callback 里调用 next 交还执行权
gen.next(text)
})
首先,这段代码虽然很粗糙,但是已经反映了使用 generator
封装异步任务的核心思想。最直观的受益就是:runTask
的内容看起来很像同步代码,条理清晰,很适合阅读。
但是上面第 3 部分关于执行的代码很不灵活,我们不能每次都这么写一段,因此接下来的目标就是 实现一个任务执行器。
自动任务执行器
同样的,先思考核心的思路:要想让某个 generator
函数自动执行,无非就是一个 while
循环:
1. 如果当前 yield 返回值的 done 属性为 true, 说明任务已经执行完成;2. 如果当前 yield 返回值的 done 属性为 false, 说明任务还未执行完成,则继续执行 next,同时要注意参数传递
根据分析实现以下的执行器:
function autoExecute(task) {const gen = task()
let result = gen.next()
while(true){if(result.done){
break // 执行结束
return
}
console.log(result.value)// 为了便于观察 我们加上 console.log
result = gen.next(result.value) // 每次都应该重写 result 获取最新结果
}
}
function* simpleTask(){
yield 1
yield 2
yield 3
return
}
autoExecute(simpleTask)// 测试我们写的自动执行器 能够正确输出 123
上面的执行器已经有了雏形,但是对于前面例子中,result.value 为函数的情况 还没有处理,因此还需要稍微补充:
function isFunction(source){return Object.prototype.toString.call(source) === "[object Function]"
}
function autoExecute(task) {const gen = task()
let result = gen.next()
let isRuningAsync = false // 由于加入了异步处理,所以要增加一个标志位避免重复进入循环体
while (!isRuningAsync) {if (result.done) {return}
console.log(result.value)
/* start 补充的处理函数 */
if (isFunction(result.value)) {
isRuningAsync = true
const callback = (arg) => {result = gen.next(arg) // 核心代码
isRuningAsync = false
}
result.value(callback)
/* end 补充的处理函数 */
} else {result = gen.next(result.value)
}
}
}
autoExecute(runTask) // 试着用这个自动执行器执行之前的异步任务
上面这个自动执行器,就完成了 generator 对异步任务的封装。
总结
本文简要介绍了 generator 函数的一些特性,重点在于说明如何使用 generator
函数对异步任务进行封装,从而能够让异步代码编写的更加清晰。
再次强调:用 generator
函数对异步任务封装的思想是很明确的 –控制 Generator 函数的流程,在适当的时机接收和交还程序的执行权,但是具体的实现方式并不唯一,例如本文用的是最简单直接的回调函数方式,在阮一峰老师的《es6 入门》教程里,也有使用 thunk 思路来讲解的部分,可以自行查阅。