JavaScript生成器函数Generator解决异步操作问题
作者:Tqing
为什么使用Generator?
在JavaScript使用异步操作时,在async和await还没有被JavaScript官方正式推出时,那么异步操作解决方案就只有回调函数和Promise。
回调函数
所谓回调函数,就是把需要执行的动作以函数的方式包装起来,再将这个函数以参数的方式传递给其他的函数,当时机到来时再进行调用。
// 需在浏览器中运行 function loadImage(imgUrl, callback) { const img = document.createElement("img"); img.onload = function () { callback(this); }; img.src = imgUrl; } loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg", (img) => { document.body.appendChild(img); loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg", (img) => { document.body.appendChild(img); loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg", (img) => { document.body.appendChild(img); } ); } ); } );
Promise
promise是为了解决回调函数产生的回调地狱问题而产生的。
function loadImage(imgUrl) { return new Promise((resolve) => { const img = document.createElement("img"); img.onload = function () { resolve(this); }; img.src = imgUrl; }); } loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg" ) .then((img) => { document.body.appendChild(img); return loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg" ); }) .then((img) => { document.body.appendChild(img); return loadImage( "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg" ); }) .then((img) => { document.body.appendChild(img); });
虽然解决了回调地狱的问题,异步任务执行步骤也更加清晰了,但是相比于现在async和await的异步操作同步化的表达方式还是略逊一筹,在async和await还没有推出时,社区就已经利用生成器函数实现了社区版的async和await,当时生成器的出现就是为了服务于异步编程。
使用方式
创建方式
function* getValue() { yield 1; yield 2; return 3; } const generator = getValue() // 获取到生成器 console.log(Object.prototype.toString.call(generator)); // [object Generator]
next方法
获取返回值
生成器函数和普通函数的调用方式完全不同,上面代码虽然调用生成器函数产生了生成器,但是生成器函数内部的代码并为被执行。
那么需要怎样操作才能让生成器内部的代码执行?
生成器有一个next方法,当这个方法被调用时,会把yield后面的值返回回来,并且生成器函数内部的代码停止执行,当再次调用next方法后,生成器函数内部代码会从上次暂停处开始执行,到下一个yield语句处停止。
next()方法返回的对象包含两个属性:
- value:返回的值,也就是yield关键字后面的值。
done:表示生成器函数是否已经完成,true表示已经完成,false表示未完成。
function* getValue() { yield 1; yield 2; yield 3; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next()); // { value: 2, done: false } console.log(generator.next()); // { value: 3, done: false } console.log(generator.next()); // { value: undefined, done: true }
当生成器内最后的yield语句也执行过后,意味着整个生成器函数执行完毕,调用next()方法产出的结果中done的值为true了。
简单用图片演示下运行流程:
第一次调用next()方法,生成器函数会暂停在第一个yield语句
再次调用next()方法,生成器函数会暂停在第二个yield语句
再次调用next()方法,生成器函数会暂停在第三个yield语句
再次调用next()方法,生成器函数执行完毕
这里返回的value为什么是undefined,其实可以这么理解,因为在JavaScript中,函数不主动return,那么会默认返回undefined。
function* getValue() { yield 1; yield 2; yield 3; return undefined; }
如果生成器函数中有return语句,那么在执行return语句的时候就会把生成器的状态置为已完成,后面的yield语句将不再被执行,这点和普通函数是保持一致的。
function* getValue() { yield 1; yield 2; return 3; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next()); // { value: 2, done: false } console.log(generator.next()); // { value: 3, done: true }
传递参数
next函数其实也可以传递参数,这个参数将被yield语句消费。
function* getValue() { const a = yield 1; const b = yield 2 * a; return 3 * b; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next(5)); // { value: 10, done: false } console.log(generator.next(6)); // { value: 18, done: true }
还是使用图片来演示整个流程。
- 首先调用next(),语句在第一个yield语句处暂停
- 然后调用next(5),参数5会传递给yield语句等号左边的变量a,语句会停在第二个yield语句处
- 然后调用next(6),参数6会传递给yield语句等号左边的变量b,碰到return语句直接返回,整个生成器函数执行完毕。
throw函数
next函数可以往生成器里面传递函数,而throw方法可以往生成器函数里面抛出异常,如果没有在生成器函数里面捕获异常,那么生成器函数会向其它函数一样抛出异常。
function* getValue() { yield 1; try { yield 2; } catch (error) { console.error(error); // 捕获异常, 打印 trhow error } yield 3; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next(5)); // { value: 2, done: false } console.log(generator.throw("throw error")); // {value: 3, done: false}
return函数
reutrn函数在被调用后,生成器函数会直接返回return函数传递的值,而且生成器函数整个函数执行完毕。
function* getValue() { yield 1; yield 2; yield 3; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.return(5)); // { value: 5, done: true } // 此时生成器函数执行完毕 console.log(generator.next()); // {value: undefined, done: true}
使用场景
实现Async和Await
function getValue(n, ms = 1000) { return new Promise((resolve) => { setTimeout(() => { resolve(n); }, ms); }); } function* generator() { console.log(1); let x = yield getValue(2); console.log(x); x = yield getValue(3, 2000); console.log(x); x = yield 4; console.log(x); } function asyncGenerator(generator) { return new Promise((resolve, reject) => { let iterable = generator(); let generated = iterable.next(); tick(); function tick() { if (generated.done === false) { Promise.resolve(generated.value).then( (value) => { try { generated = iterable.next(value); tick(); } catch (err) { reject(err); } }, (reason) => { try { generated = iterable.throw(reason); tick(); } catch (err) { reject(err); } } ); } else { resolve(generated.value); } } }); } asyncGenerator(generator);
借用上面实现asyncGenerator函数,我们可以再来优化下加载图片的代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> const imgUrlList = [ "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg", "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg", "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg", ]; function loadImage(imgUrl) { return new Promise((resolve) => { const img = document.createElement("img"); img.onload = function () { resolve(this); }; img.src = imgUrl; }); } function* loadImageList() { const img1 = yield loadImage(imgUrlList[0]); document.body.appendChild(img1); const img2 = yield loadImage(imgUrlList[1]); document.body.appendChild(img2); const img3 = yield loadImage(imgUrlList[2]); document.body.appendChild(img3); } function asyncGenerator(generator) { return new Promise((resolve, reject) => { let iterable = generator(); let generated = iterable.next(); tick(); function tick() { if (generated.done === false) { Promise.resolve(generated.value).then( (value) => { try { generated = iterable.next(value); tick(); } catch (err) { reject(err); } }, (reason) => { try { generated = iterable.throw(reason); tick(); } catch (err) { reject(err); } } ); } else { resolve(generated.value); } } }); } asyncGenerator(loadImageList).then(() => { console.log("load success"); }); </script> </html>
实现自定义迭代器
const list = { head: { value: 1, next: { value: 2, next: { value: 3, next: null, }, }, }, *[Symbol.iterator]() { let curNode = list.head; while (curNode) { yield curNode.value; curNode = curNode.next; } }, }; for (let val of list) { console.log(val); }
上面针对链表数据结构使用for...of的进行遍历,这得益于在list内部声明了迭代器,有两点是for...of所需要的
- 返回包含next方法的对象
- 调用返回的next方法返回的对象包含value和done这两个属性
而这两点生成器函数全部满足,无疑是天作之合。
以上就是JavaScript生成器函数Generator解决异步操作问题的详细内容,更多关于JavaScript生成器函数Generator的资料请关注脚本之家其它相关文章!