理解Generator函数与async函数
最近看见了一个async函数在event loop中的执行顺序问题,突然发现我对于Generator函数与async的掌握十分有限,惊出一身冷汗,赶忙恶补一番。
参考
JavaScript中的异步编程
JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数
回调函数本身并没有问题,它的问题出现在:如果需要连续处理多个异步任务,就会编程多个回调函数嵌套,形成著名的“回调地狱”
Promise就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,修改为通过promise.then
的链式调用。
Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
Generator函数
Generator生成器函数在执行时能暂停,后面又能从暂停处继续执行。
调用一个生成器函数并不会马上执行它里面的语句,,而是返回一个这个生成器的 迭代器**(iterator )对象(可以理解为一个指针)。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段(后续)),指向第一个(后续)遇到的 yield 语句。
next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
下面是一个使用Generator函数切换按钮状态的例子,注意这个函数可以一直执行下去,但会while(true)
并不会造成死循环,因为每次循环都需要调用g.next()
触发
<button id="btn">false</button>
<script type="text/javascript">
function *toggleBtnStatus(){
let status = false;
while(true) {
status = !status;
btn.innerText = status;
yield status;
}
}
let g = toggleBtnStatus();
btn.onclick = function(){
g.next()
}
</script>
Generator函数的一个问题是需要手动一直调用next,直至状态done为true为止, Generator 函数自动执行有两种方案
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
- Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权
thunk函数
在JavaScript中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
var readFile = Thunk(fs.readFile);
// 转换后的函数只接收callback作为参数
readFile(fileA)(callback);
下面是读取多个文件的处理方案
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
根据MDN文档描述,调用 next()方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值,那么手动情况下,执行上面gen函数的步骤为
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
可见上面都是通过 g.next(data)
传入数据,因此可以通过递归自动实现
function run(fn) {
var gen = fn();
// 这个next函数实际上就是上面readFile 的thunk函数的callback参数
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
这样就实现了Generator函数的自动执行,需要保证每个yeild后面的返回值是thunk函数
使用Promise
参考co库的实现,使用promise与thunk的方式类似,通过在promise.then中调用gen.next
来自动执行函数
先将readFile修改成Promise形式
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');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
手动调用方式为
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
})
同thunk函数,这里可以改写为
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);
同样也达到了自动执行gen函数的目的,这里需要保证每个yeild后面的返回值是一个Promise对象
async 和 await
上面整理了Generator 函数的使用和自动执行Generator 函数的实现原理。
由于自动执行生成器函数需要额外进行处理,因此在ES7引入了async和await,使用一对关键字async
和await
来实现异步代码的同步书写方式。实际上,async 函数就是 Generator 函数的语法糖,包括内置执行器、await后可跟任意数据类型等方面。
基本方式
async函数本身返回一个promise
async function testAsync(){
return "hello";
}
const result = testAsync();
// console.log(result); // Promise { hello }
result.then(res=>{
console.log(res)
})
await后跟一个表达式。await可以阻塞当前async函数中的代码,并等待其后面的表达式执行完成,然后取消阻塞并继续执行后续代码。
function timer(){
return new Promise((res, rej)=>{
setTimeout(()=>{
res("hello");
}, 500)
})
}
async function getTimer(){
let data = await timer(); // 阻塞 500ms
console.log(data);
}
getTimer();
理解await十分重要,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
- 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
- 如果它等到的是一个 Promise 对象,那么它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果
想想一个Promise对象是如何resolve的:异步等待Promise的状态修改为FulFilled时调用resolve
new Promise((resolve, reject)=>{
setTimeout(() => {
resolve(123)
}, 0);
})
换句话说,async 函数调用本身不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行(awiati后面的代码会被阻塞)。
async function t(){
console.log('async start')
return 'async result'
}
t().then(res=>{
console.log(res)
})
console.log('script start')
上面代码依次输出 async start、script start、async result
错误处理
由于await后面的promise可能被reject,所以最好将await代码放在try-catch语句中。 不过也可以直接在promise上使用catch完成异常捕获
async function getTimer(){
let data = await timer().catch(e=>{
console.log(e);
});
console.log(data);
}
从这里可以看出,只需要把异步的操作放在await后面的表达式即可,因此Promise的一些特性比如Promise.all
也就可以使用了
小结
这里整理了generator函数的基本使用和自动执行方法,然后介绍了作为自带执行器的generator函数语法糖——async函数。理解这些基础的语法是非常有必要的。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。