JavaScript Async - Avoiding State With Yield |
Written by Ian Elliot | |||
Monday, 30 March 2020 | |||
Page 1 of 2 One way to keep your UI responsive is to break a computation up into small chunks. The problem is that you have to record the state of the computation so you can restart it. Yield can be used to do this automatically. This is an extract from Chapter 4 of my book on JavaScript Async. Now Available as a Book:JavaScript AsyncYou can buy it from: Amazon Contents
If you want to write JavaScript apps that are responsive, you have little choice but to master the art of creating your own non-blocking asynchronous functions. In this chapter we look at ways of working with the event queue to keep the UI working. To keep the UI responsive you have to let the UI thread do its job – which is to service the UI. There are only two ways to do this:
or
In most cases the simplest approach is to arrange to break the computation into small parts and give the thread back to the UI at regular intervals. The problem with this is keeping track of the state so that you can resume the computation. We have already looked at ways of doing this earlier in the chapter but there is another way that means you can effectively ignore the problem. Avoiding State – YieldThe big problem that we generally have to solve when splitting a computation up into small chunks is saving and restoring state. In general all you have to do is create a state object with all of the properties needed to restart the computation from where it left off. This is nearly always possible, but sometimes it requires a change to the code that isn't natural. For example, the Pi computation, introduced ealier in the chapter, is most naturally written: var pi = 0; var k; for (k = 1; k <= 100000; k++) { that is, with a single for loop repeating the iteration as many times as required. However, we can’t use a for loop like this if the calculation is to be broken up into chunks. There is no way of pausing the for loop and restarting it where it left of – or rather there didn't used to be. The yield keyword and generators were introduced in ES6 and are supported on all current browsers, but there is no legacy support – IE doesn't support it at all for example. Yield and generators were introduced to allow JavaScript to create iterations in a natural way. A generator is a function that can contain a yield instruction. When a yield is encountered the function is suspended and optionally a value can be returned. What is important for our particular use is that the function’s state is automatically preserved by the system and you can restart the function from the yield instruction as if nothing had happened. This clearly provides us with an automatic way of preserving the state of a function that we want to divide up into chunks. A simple example of generators and yield will be enough to explain its used in custom non-blocking functions. To use yield it has to be within a generator function which is declared in the same way as any function, but using a trailing asterisk: function* myGenerator(){ } You can write a generator function in the usual way, but with the added factor of being able to use yield to implement a return with the possibility of continuing. For example a very simple generator is: function* myGenerator(){ yield 1; yield 2; yield 3; } The generator looks like a function that you could call to return one of the values, but things are a tiny bit more complicated. The generator is in fact an object factory. When you call it, it returns an object which will carry out the computation that you have specified. That is: var myNums=myGenerator(); doesn't compute the numbers in the body of the generator, but returns an object which will do the job. This object is stored in myNums and it has a single method, next, which you can think of as a "resume" command for the generator. To get the first result of the generator we have to call the next method: console.log(myNums.next().value); The next method returns an object, an iteratorResult object, with two properties – value, which is the value passed by the yield, and done to indicate that the function has finished and cannot be resumed. You can see in the instruction above that the value 1 is printed to the console. That is, the next method runs the generator to the first yield which returns an iteratorResult object with value set to 1. The done property, were we to test it, is false. The function is now paused and can be resumed by another call to next: console.log(myNums.next().value); This starts the function off again at the instruction that follows the previous yield and so we see 2 printed in the console log. If you repeat this you would see 3 and then undefined, with done set to true. The important point is that the yield preserves the state of the function which the next restores. This means we can use a for loop for example and expect its state to be preserved between yields: function* myGenerator(){ var i; for(i=1;i<4;i++){ yield i; } } If you try this with the same console log statements then you will see 1,2,3, undefined. |
|||
Last Updated ( Monday, 30 March 2020 ) |