使用Generator函数进行异步编程
Generator函数在工作中还没有用到过,一直在使用async,最近在看async的原理,发现它只是Generator的语法糖。
Generator的基础知识之前写过文章介绍过(https://www.cnblogs.com/wangtingnoblog/p/js_Generator.html),这里主要讨论一下怎么使用Generator函数来进行异步编程。
先来看下面的代码:
1 function* g() { 2 console.log('g start') 3 yield setTimeout(() => console.log('setTimeout'), 3000) 4 console.log('g end') 5 } 6 const useg = g(); 7 useg.next(); 8 useg.next();
运行结果:
分析:
- 第7步时,yield后面的异步操作已经开始,但是还没有结束,会在3秒后打印出setTimeout,
- 因为没有等到异步操作结束就调用了next方法,造成先打印出 g end,然后才打印出setTimeout
- 我们想要的是按照正常的顺序进行打印,比如在得知yield完成之后才能结束函数,这个要怎么实现呢?
- 如果在yield表达式执行完成后能执行一个回调函数,我们在回调函数中再通知Generator函数执行下一步就可以了。
看下面的代码:
1 function* g() { 2 console.log('g start') 3 yield function(callBack) { 4 setTimeout(() => { 5 console.log('setTimeout') 6 callBack() 7 }, 3000) 8 } 9 console.log('g end') 10 } 11 const useg = g(); 12 const first = useg.next(); 13 first.value(() => useg.next());
运行结果:
分析:
- 可以看到,我们把异步操作封装在一个函数中,这个函数只接受一个回调函数作为参数,当异步操作执行完成会,它会调用回调函数。
- 这样就保证Generator函数的下部分的代码一定在上部分的代码执行完成之后才执行。
- 有一点需要注意的是,这样修改代码使得异步操作变成了惰性执行了。
- 之前的代码是只要调用useg.next方法,异步操作就会开始执行,修改后的代码则变成就算调用了useg.next方法,如果没有调用value(callback),异步操作也不会执行。
- 这种惰性执行和Observable很像,如果没有订阅Observable,Observable的逻辑就不会执行
- Promise是立即执行的
我们再来看一下实际的例子,使用Generator函数封装ajax:
1 function* g() { 2 console.log('g start') 3 const userName = yield function(callBack) { 4 const xhr = new XMLHttpRequest(); 5 xhr.open('get', 'http://localhost:3002/users?id=1', true); 6 xhr.responseType = 'json'; 7 xhr.setRequestHeader('Accept', 'application/json'); 8 xhr.onload = () => { 9 const data = xhr.response; 10 callBack(data[0].name); 11 } 12 xhr.send(null); 13 } 14 const families = yield function(callBack) { 15 const xhr = new XMLHttpRequest(); 16 xhr.open('get', `http://localhost:3002/family?name=${userName}`, true); 17 xhr.responseType = 'json'; 18 xhr.setRequestHeader('Accept', 'application/json'); 19 xhr.onload = () => { 20 const data = xhr.response; 21 callBack(data[0].families); 22 } 23 xhr.send(null); 24 } 25 console.log(families) 26 console.log('g end') 27 } 28 const useg = g(); 29 const first = useg.next(); 30 first.value( 31 (name) => { 32 const second = useg.next(name) 33 second.value( 34 (families) => useg.next(families) 35 ) 36 } 37 );
上面的代码执行的是:先通过id取得用户名,再通过用户名取得家庭成员。只看第1行到第27行,步骤清晰,很像是同步的写法。
我们需要关注的是,第28行到第37行对Generator函数的流程管理:
- 调用Generator函数取得迭代器对象
- 调用next方法取得value属性
- 调用value属性的值,给它传入回调函数
- 在回调函数中执行第2步,第3步
- 判断此时函数已经执行完成
注意到这里的12345步是可以用迭代来进行的,修改流程管理的代码如下:
1 function run(g) { 2 // 取得迭代器 3 const useg = g(); 4 // 定义循环调用的函数 5 function loop(data) { 6 // 取得当前的值 7 const result = useg.next(data); 8 // 判断是否已经执行完成,执行完成会退出 9 if (result.done) return; 10 // 没有执行完成则继续执行回调 11 result.value(loop); 12 } 13 // 开始迭代 14 loop(); 15 } 16 run(g);
可以看到,使用迭代来管理流程时完全没有必要知道生成器的内部结构。而且这种方式也不管生成器中有多少个yield。
有一点需要注意的是,使用这种发生来管理流程要求生成器中yield后面的表达式必须是一个函数,且这个函数有唯一参数(回调函数)
其实,我们需要的只是在异步操作有了结果之后把执行权再交还给Generator函数继续执行。
问题是怎么知道异步操作有了结果?
- 回调函数
- Promise
上面已经介绍过使用回调函数来控制流程,它的限制是yield后面的表达式必须是一个以回调函数为参数的函数。
下面介绍使用Promise来控制流程:
ajax函数返回Promise
1 function ajax(url) { 2 return new Promise( 3 (resolve, reject) => { 4 const xhr = new XMLHttpRequest(); 5 xhr.open('get', url, true); 6 xhr.responseType = 'json'; 7 xhr.setRequestHeader('Accept', 'application/json'); 8 xhr.onload = () => { 9 const data = xhr.response; 10 resolve(data); 11 } 12 xhr.send(null); 13 } 14 ) 15 }
1 function* g() { 2 console.log('g start') 3 const userNameResponse = yield ajax('http://localhost:3002/users?id=1') 4 const familiesResponse = yield ajax('http://localhost:3002/family?name=' + userNameResponse[0].name) 5 console.log(familiesResponse[0].families) 6 console.log('g end') 7 } 8 const useg = g(); 9 useg.next().value.then( 10 (userNameResponse) => useg.next(userNameResponse).value.then( 11 (familiesResponse) => useg.next(familiesResponse) 12 ) 13 );
可以看到:
- yield后面必须是Promise对象
- 流程控制中同样可以使用迭代来执行
Promise迭代版本的流程控制:
function run(g) { // 取得迭代器 const useg = g(); // 定义循环调用的函数 function loop(data) { // 当前的值 const result = useg.next(data); // 判断是否已经执行完成,执行完成会退出 if (result.done) return; // 没有执行完成则继续执行回调 result.value.then(loop); } // 开始迭代 loop(); } run(g);
对比之前的使用回调函数来控制流程的代码,你会发现和使用Promise来控制流程的代码如此的类似。
使用Promise来控制流程需要注意的是yield后面必须是一个Promise对象
对比使用回调函数控制流程和使用Promise对象来控制流程
- 使用回调函数控制流程需要yield后面是一个以回调函数为参数的函数,使用Promise对象来控制流程需要yield后面必须是Promise对象。
- 使用回调函数控制流程时,异步操作是惰性执行的,使用Promise对象来控制流程时,异步操作是立即执行的
总结:
Generator函数本身使得异步操作看起来非常像同步操作,麻烦的是它的流程控制需要我们手动调用。(之后要讨论的async就是对Generator的进一步封装,不用开发者总结来流程控制)
另外上面的例子为了简化,都没有做异常处理,实际开发中,异常处理还是很有必要的。
作成:2019-02-10
修改:
- 2019-02-10 23:23:57 添加分类
参考:《ES6标准入门》、《Learning TypeScript》、《深入理解ES6》