Baru-baru ini, untuk memahami dengan lebih baik cara Redux Sagas berfungsi, saya mempelajari semula pengetahuan tentang penjana JavaScript. Saya mengumpul Pelbagai mata pengetahuan dipekatkan menjadi satu artikel saya harap artikel ini mudah difahami dan cukup teliti untuk dijadikan panduan untuk pemula menggunakan generator.
Pengenalan
JavaScript memperkenalkan penjana dalam ES6. Fungsi penjana adalah seperti fungsi biasa kecuali ia boleh dijeda dan disambung semula. Penjana juga berkait rapat dengan iterator, kerana objek penjana adalah iterator.
Dalam JavaScript, anda biasanya tidak boleh menjeda atau berhenti selepas panggilan fungsi. (Ya, fungsi async dijeda sementara menunggu pernyataan menunggu, tetapi fungsi async hanya diperkenalkan dalam ES7. Selain itu, fungsi async dibina di atas penjana.) Fungsi biasa hanya tamat apabila ia mengembalikan atau membuang ralat .
function foo() { console.log('Starting'); const x = 42; console.log(x); console.log('Stop me if you can'); console.log('But you cannot'); }
Sebaliknya, penjana membenarkan kami menjeda pelaksanaan pada titik putus sewenang-wenangnya dan menyambung semula pelaksanaan dari titik putus yang sama.
Penjana dan Peulang
Dari MDN:
Dalam JavaScript, iterator ialah objek yang mentakrifkan jujukan dan mungkin mengembalikan nilai pulangan apabila ditamatkan . >Lebih khusus, iterator ialah sebarang objek yang melaksanakan protokol Iterator>dengan menggunakan kaedah next(), yang mengembalikan objek dengan dua sifat: nilai, iaitu nilai seterusnya dalam jujukan dan selesai, jika Ia benar jika nilai terakhir dalam jujukan telah diulang ke. Jika nilai dan selesai hadir bersama, ia adalah nilai pulangan lelaran.
Oleh itu, intipati iterator ialah:
- Objek yang mentakrifkan urutan
- mempunyai kaedah
next()
... - mengembalikan objek dengan dua Objek dengan atribut: nilai dan selesai
Adakah anda memerlukan penjana untuk mencipta iterator? Tidak. Malah, kita sudah boleh mencipta jujukan Fibonacci tak terhingga menggunakan penutupan pra-ES6, seperti yang ditunjukkan dalam contoh berikut:
var fibonacci = { next: (function () { var pre = 0, cur = 1; return function () { tmp = pre; pre = cur; cur += tmp; return cur; }; })() }; fibonacci.next(); // 1 fibonacci.next(); // 2 fibonacci.next(); // 3 fibonacci.next(); // 5 fibonacci.next(); // 8
Mengenai faedah penjana, saya akan memetik MDN sekali lagi:
Walaupun iterator tersuai adalah alat yang berguna, menciptanya memerlukan pengaturcaraan yang teliti kerana keperluan untuk mengekalkan keadaan dalaman mereka secara eksplisit. Fungsi penjana menyediakan alternatif yang berkuasa: ia membenarkan kami mentakrifkan algoritma lelaran dengan menulis fungsi yang pelaksanaannya tidak berterusan.
Dalam erti kata lain, adalah lebih mudah untuk mencipta iterator menggunakan penjana (tiada penutupan diperlukan!), yang bermakna lebih sedikit peluang ralat.
Hubungan antara penjana dan iterator ialah objek penjana yang dikembalikan oleh fungsi penjana ialah lelaran.
Sintaks
Fungsi penjana dicipta menggunakan sintaks fungsi* dan dijeda menggunakan kata kunci hasil.
Pada mulanya memanggil fungsi penjana tidak melaksanakan mana-mana kodnya sebaliknya, ia mengembalikan objek penjana. Nilai ini digunakan dengan memanggil kaedah next() penjana, yang melaksanakan kod sehingga kata kunci hasil ditemui, kemudian berhenti seketika sehingga next() dipanggil semula.
function * makeGen() { yield 'Hello'; yield 'World'; } const g = makeGen(); // g is a generator g.next(); // { value: 'Hello', done: false } g.next(); // { value: 'World', done: false } g.next(); // { value: undefined, done: true }
Panggilan berulang ke g.next() selepas pernyataan terakhir di atas hanya mengembalikan (atau, lebih tepat, menghasilkan) objek pulangan yang sama: { value: undefined, done: true }.
hasil menjeda pelaksanaan
Anda mungkin melihat sesuatu yang istimewa tentang coretan kod di atas. Panggilan kedua seterusnya() menghasilkan objek dengan harta done: false bukannya done: true.
Memandangkan kita sedang melaksanakan penyataan terakhir dalam fungsi penjana, bukankah atribut yang dilakukan adalah benar? tidak sangat. Apabila pernyataan hasil ditemui, nilai yang mengikutinya (dalam kes ini "Dunia") dijana dan pelaksanaan dijeda. Oleh itu, panggilan kedua seterusnya() berhenti seketika pada pernyataan hasil kedua, jadi pelaksanaan belum selesai lagi - pelaksanaan tidak lengkap sehingga pelaksanaan disambung semula selepas pernyataan hasil kedua (iaitu selesai: benar), dan tidak Jalankan kod itu semula.
Kita boleh menganggap panggilan seterusnya() sebagai memberitahu atur cara untuk menjalankan ke pernyataan hasil seterusnya (dengan mengandaikan ia wujud), menjana nilai dan jeda. Program ini tidak akan mengetahui bahawa tiada apa-apa selepas pernyataan hasil sehingga ia menyambung semula pelaksanaan, dan pelaksanaan hanya boleh disambung semula dengan panggilan seterusnya() yang lain.
hasil dan pulangan
Dalam contoh di atas, kami menggunakan hasil untuk menghantar nilai di luar penjana. Kami juga boleh menggunakan pulangan (sama seperti dalam fungsi biasa namun, menggunakan pulangan menamatkan pelaksanaan dan set selesai: benar.
function * makeGen() { yield 'Hello'; return 'Bye'; yield 'World'; } const g = makeGen(); // g is a generator g.next(); // { value: 'Hello', done: false } g.next(); // { value: 'Bye', done: true } g.next(); // { value: undefined, done: true }
Oleh kerana pelaksanaan tidak berhenti seketika pada pernyataan pulangan, dan mengikut definisi tiada kod boleh dilaksanakan selepas pernyataan pulangan, dilakukan ditetapkan kepada benar.
hasil: Hujah kepada kaedah seterusnya
Setakat ini kami telah menggunakan hasil untuk menghantar nilai di luar penjana (dan jeda pelaksanaannya).
Walau bagaimanapun, hasil sebenarnya adalah dua arah dan membolehkan kami menghantar nilai ke dalam fungsi penjana.
function * makeGen() { const foo = yield 'Hello world'; console.log(foo); } const g = makeGen(); g.next(1); // { value: 'Hello world', done: false } g.next(2); // logs 2, yields { value: undefined, done: true }
等一下。不应该是"1"打印到控制台,但是控制台打印的是"2"?起初,我发现这部分在概念上与直觉相反,因为我预期的赋值foo = 1。毕竟,我们将“1”传递到next()方法调用中,从而生成Hello world,对吗?
但事实并非如此。传递给第一个next(...)调用的值将被丢弃。除了这似乎是ES6规范之外,实际上没有其他原因.从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
我喜欢这样对程序的执行进行合理化:
- 在第一个next()调用时,它将一直运行,直到遇到yield 'Hello world',在此基础上生成{ value: 'Hello world', done: false }和暂停。就是这么回事。正如大家所看到的,传递给第一个next()调用的任何值都是不会被使用的(因此被丢弃)。
- 当再次调用next(...)时,执行将恢复。在这种情况下,执行需要为常量foo分配一些值(由yield语句决定)。因此,我们对next(2)的第二次调用赋值foo=2。程序不会在这里停止—它会一直运行,直到遇到下一个yield或return语句。在本例中,没有更多的yield,因此它记录2并返回undefined的done: true。在生成器使用异步因为yield是一个双向通道,允许信息在两个方向上流动,所以它允许我们以非常酷的方式使用生成器。到目前为止,我们主要使用yield在生成器之外传递值。但是我们也可以利用yield的双向特性以同步方式编写异步函数。
使用上面的概念,我们可以创建一个类似于同步代码但实际上执行异步函数的基本函数:
function request(url) { fetch(url).then(res => { it.next(res); // Resume iterator execution }); } function * main() { const rawResponse = yield request('https://some-url.com'); const returnValue = synchronouslyProcess(rawResponse); console.log(returnValue); } const it = main(); it.next(); // Remember, the first next() call doesn't accept input
这是它的工作原理。首先,我们声明一个request函数和main生成器函数。接下来,通过调用main()创建一个迭代器it。然后,我们从调用it.next()开始。
在第一行的function * main(),在yield request('https://some-url.com')之后执行暂停。request()隐式地返回undefined,因此我们实际上生成了undefined值,但这并不重要—我们没有使用该值。
当request()函数中的fetch()调用完成时,it.next(res)将会被调用并完成下列两件事:
it继续执行;和
it将res传递给生成器函数,该函数被分配给rawResponse
最后,main()的其余部分将同步完成。
这是一个非常基础的设置,应该与promise有一些相似之处。有关yield和异步性的更详细介绍,请参阅此文。
生成器是一次性
我们不能重复使用生成器,但可以从生成器函数创建新的生成器。
function * makeGen() { yield 42; } const g1 = makeGen(); const g2 = makeGen(); g1.next(); // { value: 42, done: false } g1.next(); // { value: undefined, done: true } g1.next(); // No way to reset this! g2.next(); // { value: 42, done: false } ... const g3 = makeGen(); // Create a new generator g3.next(); // { value: 42, done: false }
无限序列
迭代器表示序列,有点像数组。所以,我们应该能够将所有迭代器表示为数组,对吧?
然而,并不是的。数组在创建时需要立即分配,而迭代器是延迟使用的。数组是迫切需要的,因为创建一个包含n个元素的数组需要首先创建/计算所有n个元素,以便将它们存储在数组中。相反,迭代器是惰性的,因为序列中的下一个值只有在使用时才会创建/计算。
因此,表示无限序列的数组在物理上是不可能的(我们需要无限内存来存储无限项!),而迭代器可以轻松地表示(而不是存储)该序列。
让我们创建一个从1到正无穷数的无穷序列。与数组不同,这并不需要无限内存,因为序列中的每个值只有在使用时才会懒散地计算出来。
function * makeInfiniteSequence() { var curr = 1; while (true) { yield curr; curr += 1; } } const is = makeInfiniteSequence(); is.next(); { value: 1, done: false } is.next(); { value: 2, done: false } is.next(); { value: 3, done: false } ... // It will never end
有趣的事实:这类似于Python生成器表达式vs列表理解。虽然这两个表达式在功能上是相同的,但是生成器表达式提供了内存优势,因为值的计算是延迟的,而列表理解则是立即计算值并创建整个列表。
推荐学习:《javascript基础教程》