Mastering Reactive JavaScript
上QQ阅读APP看书,第一时间看更新

Transforming events using bacon.js

One important thing when learning functional reactive programming is how you can transform the events emitted by an observable. This way, you can use successive function calls to create new objects from the original input. This also improves the reuse of your code; every transformation of an observable creates a new observable, and each observable can have several listeners subscribed to it.

Before applying any transformations to our observables, let's implement an observable to generate and print the current date. To do this, let's use the Bacon.interval() method. So, the following code will emit an empty object every second:

var eventSource = Bacon 
.interval(1000);
Remember,  Bacon.interval() emits an empty object every x milliseconds, where x is the argument passed to the function–in our example, every 1000 milliseconds, which is the same as every second.

Now we can just subscribe a function to print the current date to the console. We can do this using the onValue() function by adding the following line:

eventSource 
.onValue(()=>{
console.log(new Date());
});

This will keep printing an output as follows:

    2016-11-23T21:21:43.291Z
2016-11-23T21:21:44.315Z
2016-11-23T21:21:45.319Z

It works fine and solves our problem, but remember the problem–We wanted an observable to generate and print the current date to the console. Unfortunately, bacon.js doesn't have any observable that could generate the current date every given second, but we can use an operator to change the empty object to the current date. The return of this operator will be a new observable that will emit the current date every second, which is exactly what we wanted from the beginning. Then, we can use the onValue() method to subscribe to this operator in order to print the current date.

Luckily, such an operator exists and it is one of the most commonly used operators as well. It's called the map() operator. It lets you add a function to map an event emitted in another object. It has the following signature:

eventStream.map(handler); 

The handler in the signature is a function that receives the emitted event and returns a new object to substitute the original event (so basically, we map an input to an output). You can see this illustrated in the following diagram:

This diagram describes exactly how we want our observable to work. We use the Bacon.inverval() method to generate a new observable that will emit the current date using the map() function. This function is executed every time an object is emitted.

Looking at this diagram makes it a lot easier to understand how we can use the map() function to implement the desired behavior. We will need to do some minor changes in our original code to create a new EventStream using the map() operator, and change our subscription to only log the emitted event. So, the final implementation of the described diagram using the map() operator is as follows:

Bacon 
.interval(1000)
.map(
(i)=> new Date()
)
.onValue((date)=>console.log(date));

This gives you the same kind of output as from the previous code:

    2016-11-23T21:21:43.291Z
2016-11-23T21:21:44.315Z
2016-11-23T21:21:45.319Z

In this section, I want to show you an example of reusing an eventStream. To do this, we will slightly change our original problem. Now, we want to print only those dates where the seconds are even. To do this, we will need a new operator: the filter() operator. This operator lets you create a new observable by omitting some events that you are not interested in. It has the following signature:

eventStream.filter(handler); 

Here, handler is a function that receives the emitted event as an input and returns true or false (actually, any truthy or falsy value). So, because we want to print only the dates where the seconds are even, we will pass the following handler as an argument to the filter operator:

(date)=>date.getSeconds() % 2 == 0 

As you can see, this function receives the current date as an argument and gets the seconds part of this date. Also, it uses the mod operator to decide whether it is an even number or not.

Notice that one of the advantages of this approach is the creation of small functions to implement each part of our code. This makes our code more testable, as we can easily create unit tests for each of these functions.

Now, to implement this new observable that emits only even dates, all we need to do is chain the filter() function to our previous observable in order to create a new observable that we can subscribe to finally print what we want. You can see the use of this filter() operator in the following diagram:

As you can see in this diagram, on the created observable, we don't emit the dates where the seconds are odd numbers. For this reason, we don't have their circles. Now, let's change our code using the map() function to use the filter() operator as well. This can be easily done with the following code:

Bacon 
.interval(1000)
.map(
(i)=> new Date()
)
.filter(
(date)=>date.getSeconds() % 2 == 0
)
.onValue((date)=>console.log(date));

If you run this code in a node program, you will see a result like this:

    2016-11-23T22:08:56.677Z
2016-11-23T22:08:58.715Z
2016-11-23T22:09:00.723Z
Notice that our new output contains only dates with even seconds.

This output is a little confusing. It might be hard to see that it prints only dates with even seconds (because the last part is milliseconds and not seconds). So, let's make a last change in our code. As you might expect, we can use the same operator we used before. To show this, let's use the map() operator again to generate a string from the date. Now we want to change our output to something like this:

    The number in the second part of the date XXX is YYY which is as       even number 

To do this, let's chain our filtered observable with a map() operator to generate this string for each event in the observable. The new observable can be represented by the following diagram:

The handler function will be used to generate the final string from the date. As the result string is big, I decided to omit it in the result circle, but it will follow the pattern I've described. So, first our handler function on the map() function should be as follows:

(date)=> 'The number in the second part of the date ' +  
date.toISOString() + ' is ' + date.getSeconds() +
' which is as even number'

Now, all we have to do is change our last observable with the map() operator. We can do this as follows:

Bacon 
.interval(1000)
.map(
(i)=> new Date()
)
.filter(
(date)=>date.getSeconds() % 2 == 0
)
.map(
(date)=> 'The number in the second part of the date ' +
date.toISOString() + ' is ' + date.getSeconds() +
' which is as even number'
)
.onValue((date)=>console.log(date));

Running this code will display the following output, which accentuates the seconds part, making it easier for us to see that it really is an even number:

    The number in the second part of the date 2016-11-23T22:29:42.694Z
is 42 which is as even number

The number in the second part of the date 2016-11-23T22:29:44.749Z
is 44 which is as even number

The number in the second part of the date 2016-11-23T22:29:46.757Z
is 46 which is as even number
The bacon.js has a lot of built-in operators. You can see their descriptions in their APIs. In this section, we only wanted to show how you can use operators to create new observables. We will see a few more operators in depth in the chapters which follows, using RxJS.