Récemment, afin de mieux comprendre le principe de fonctionnement de Redux Sagas, j'ai réappris les connaissances des générateurs JavaScript, j'ai condensé divers points de connaissances collectés sur Internet dans un article. L'article est à la fois facile à comprendre et suffisamment rigoureux pour servir de guide du débutant sur l'utilisation des générateurs.
Introduction
JavaScript a introduit les générateurs dans ES6. Les fonctions du générateur sont comme les fonctions normales, sauf qu'elles peuvent être mises en pause et reprises. Les générateurs sont également étroitement liés aux itérateurs, puisque les objets générateurs sont des itérateurs.
En JavaScript, vous ne pouvez généralement pas mettre en pause ou vous arrêter après un appel de fonction. (Oui, les fonctions asynchrones font une pause en attendant une instruction wait, mais les fonctions asynchrones n'ont été introduites que dans ES7. De plus, les fonctions asynchrones sont construites au-dessus des générateurs.) Une fonction normale ne se termine que lorsqu'elle renvoie ou génère une erreur.
function foo() { console.log('Starting'); const x = 42; console.log(x); console.log('Stop me if you can'); console.log('But you cannot'); }
En revanche, les générateurs nous permettent de suspendre l'exécution à un point d'arrêt arbitraire et de reprendre l'exécution à partir du même point d'arrêt.
Générateurs et itérateurs
De MDN :
En JavaScript, un itérateur est un objet qui définit une séquence et peut renvoyer une valeur de retour une fois terminé. >Plus précisément, un itérateur est tout objet qui implémente le protocole Iterator>en utilisant la méthode next(), qui renvoie un objet avec deux propriétés : value, qui est la valeur suivante dans la séquence et done, si elle est vraie ; si la dernière valeur de la séquence a été itérée. Si value et done sont présents ensemble, c’est la valeur de retour de l’itérateur.
Ainsi, l'essence d'un itérateur est :
- un objet qui définit une séquence
- a une
next()
méthode... - renvoie un objet avec deux propriétés : value et done
Un générateur est-il nécessaire pour créer un itérateur ? Non. En fait, on peut déjà créer une séquence de Fibonacci infinie en utilisant des fermetures pré-ES6, comme le montre l'exemple suivant :
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
Concernant les avantages des générateurs, je citerai à nouveau MDN :
Bien que les itérateurs personnalisés soient un outil utile, mais leur création nécessite une programmation minutieuse de notre part en raison de la nécessité de maintenir explicitement leur état interne. Les fonctions génératrices offrent une alternative puissante : elles permettent de définir des algorithmes itératifs en écrivant une fonction dont l'exécution n'est pas continue.
En d'autres termes, utiliser des générateurs pour créer des itérateurs est plus simple (aucune fermeture requise !), ce qui signifie moins de risques d'erreurs.
La relation entre les générateurs et les itérateurs est que l'objet générateur renvoyé par la fonction générateur est un itérateur.
Syntaxe
Les fonctions du générateur sont créées à l'aide de la syntaxe function* et mises en pause à l'aide du mot-clé rendement.
L'appel initial d'une fonction génératrice n'exécute aucun de son code, mais renvoie un objet générateur. Cette valeur est utilisée en appelant la méthode next() du générateur, qui exécute le code jusqu'à ce que le mot-clé rendement soit rencontré, puis s'arrête jusqu'à ce que next() soit à nouveau appelé.
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 }
Les appels répétés à g.next() après la dernière instruction ci-dessus renvoient simplement (ou plus précisément, produisent) le même objet de retour : {value : undefined, done : true }.
yield suspend l'exécution
Vous remarquerez peut-être quelque chose de spécial dans l'extrait de code ci-dessus. Le deuxième appel next() produit un objet avec la propriété done: false au lieu de done: true.
Puisque nous exécutons la dernière instruction de la fonction génératrice, l'attribut done ne devrait-il pas être vrai ? pas vraiment. Lorsqu'une instruction rendement est rencontrée, la valeur qui la suit (dans ce cas « Monde ») est générée et l'exécution est suspendue. Par conséquent, le deuxième appel next() s'arrête sur la deuxième instruction Yield, donc l'exécution n'est pas encore terminée - l'exécution n'est pas terminée tant que l'exécution ne reprend pas après la deuxième instruction Yield (c'est-à-dire done: true), et n'exécute pas à nouveau le code.
Nous pouvons considérer l'appel next() comme indiquant au programme d'exécuter l'instruction rendement suivante (en supposant qu'elle existe), de générer une valeur et de faire une pause. Le programme ne saura pas qu'il n'y a rien après l'instruction rendement jusqu'à ce qu'il reprenne l'exécution, et l'exécution ne peut être reprise qu'avec un autre appel next().
rendement et retour
Dans l'exemple ci-dessus, nous utilisons le rendement pour transmettre la valeur en dehors du générateur. Nous pouvons également utiliser return (tout comme dans une fonction normale, cependant, l'utilisation de return termine l'exécution et définit done: true) ;
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 }
Parce que l'exécution ne s'arrête pas sur l'instruction return et que, par définition, aucun code ne peut être exécuté après l'instruction return, done est défini sur true.
yield : argument de la méthode next
Jusqu'à présent, nous avons utilisé rendement pour transmettre une valeur en dehors du générateur (et suspendre son exécution).
Cependant, le rendement est en réalité bidirectionnel et nous permet de transmettre des valeurs dans des fonctions génératrices.
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基础教程》