Calling next with Schedulers

You’ve learned a heaping pile of different ways to create observables throughout this book. The secret you didn’t know was how Rx decides exactly when to send a new value down the chain. The tool Rx uses for this is called a scheduler, and Rx ships many options for you to choose from. By default, every observable does not use a scheduler to plan out events, instead they emit all values synchronously. Run the following code to see this in action:

 of​(​'Hello'​)
 .subscribe(next => console.log(next));
 
 console.log(​'World'​);

This snippet does not pass a scheduler into the of constructor, so it just runs synchronously. This example logs Hello before World. Now, we can add the asap scheduler to the mix, which delays any events in the observable pipeline until all synchronous code is executed.

Behind the Scenes

images/aside-icons/note.png

Specifically, the asap scheduler adds the next call to the microtask queue. This means that any observable using the asap scheduler behaves the same way as a promise.

 of​(​'Hello'​, Scheduler.asap)
 .subscribe(next => console.log(next));
 
 console.log(​'World'​);

When you run the updated code, you see World logged before Hello. The synchronous log at the end of the snippet runs, and then the observable, now running on an asynchronous schedule, emits an event, leading to World getting logged second. Most observable constructors take an optional scheduler as their final parameter. Several regular operators, like bufferTime, debounceTime, and delay also allow a user-defined scheduler.

Asynchronous schedulers

images/aside-icons/note.png

RxJS has two different types of asynchronous schedulers: asap and async. The key difference is that asap schedules the observable event to be run using the micro task queue (process.nextTick() in Node.js, or setTimeout in the browser). This means that the event runs after any synchronous code but before any other scheduled asynchronous tasks. The other major scheduler, async, schedules using setTimeout, and is appropriate for some time-based operations.

Using an asynchronous scheduler is neat, but what’s important for this chapter is the animationFrame scheduler, which runs every event through a call to the requestAnimationFrame API.

Requesting Repaints with requestAnimationFrame

In the world of the <canvas> API, we use requestAnimationFrame to inform the browser that we’re going to write a batch of pixels to the page. This way, we can update the location of everything at once, and the browser only needs to repaint one time, as opposed to the browser redrawing the page for each object we want to write. It takes a single parameter, a function, that is typically called recursively. Most usage of requestAnimationFrame looks something like this:

 function​ updateCanvas() {
  updateObjects();
  renderObjects();
  requestAnimationFrame(updateCanvas);
 }

In our case, we don’t need to muck around with manually calling requestAnimationFrame, we just need to set up our interval creation method with the proper scheduler. This results in more regular spacing between frame updates. The import is a bit wonky, so make sure you get it right:

 import​ { animationFrame } ​from​ ​'rxjs/internal/scheduler/animationFrame'​;
 
 function​ rafTween(element, endPoint, durationSec) {
 // Convert duration to 60 frames per second
 let​ durationInFrames = 60 * durationSec;
 let​ distancePerStep = endPoint / durationInFrames;
 
 // 60 frames per second
  interval(1000 / 60, animationFrame)
  .pipe(
  map(n => n * distancePerStep),
  take(durationInFrames)
  )
  .subscribe((location) => {
  element.style.left = location + ​'px'​;
  });
 }

We’re not quite there yet—blips in rendering time can still occur. Now that we added a scheduler to the mix, we have access to the now method on our scheduler. The now method works like the browser’s performance.now, but it’s integrated into the scheduler itself, keeping everything in one nice package. With one final update, we now have our tweening function ready for primetime. It will try to run at sixty frames per second, but will be able to handle a slower machine without problems by expanding how far the object moves each frame:

 function​ rafTween(element, pixelsToMove, durationSec) {
 let​ startTime = animationFrame.now();
 let​ endTime = animationFrame.now() + (durationSec * 1000);
 let​ durationMs = endTime - startTime;
 let​ startPoint = element.style.left;
 
  interval(1000 / 60, animationFrame)
  .pipe(
map(() => (animationFrame.now() - startTime) / durationMs),
takeWhile(percentDone => percentDone <= 1)
  )
  .subscribe(percentDone => {
  element.style.left = startPoint + (pixelsToMove * percentDone) + ​'px'​;
  });
 }

The first thing we do is ignore the value emitted from interval and instead, map the time elapsed since start as a percentage of total time the animation will take.

Now that we no longer can depend on a number of events (frames) to determine when we’re done, we add the takeWhile operator, which continues to pass events through until duration milliseconds have passed. Since it gets a value representing the percentage completion of the animation, we’re done when that percentage reaches 1.

Creating Easing Operators

Now that the observable stream emits progressive values, we can run them through a converter to create different styles of transitioning between two points (also known as easing). Each possible easing relies on an equation that answers the question, “Given that we are halfway through, where should the center be rendered?” For instance, the following starts slowly, and then accelerates to catch up at the end:

 function​ easingSquared(percentDone) {
 return​ percentDone * percentDone;
 }
 
 easingSquared(.25) ​// 0.0625
 easingSquared(.50) ​// 0.25
 easingSquared(.75) ​// 0.5625
 easingSquared(.9) ​// 0.81
 easingSquared(1) ​// 1

Many other easing functions exist, but it’s a lot more fun to see them in action than to drearily read code.

Let’s put everything together from this section to create a tool that allows moving elements with an arbitrary easing function to see a few of these in action. Open multiTween.ts and write the following code.

First, we’ll create a percentDone operator that takes a duration in milliseconds and returns a percentage of how far the animation has gone.

 function​ percentDone(durationSec) {
 return​ ​function​ ($obs) {
 let​ startTime = animationFrame.now();
 // let endTime = animationFrame.now() + (durationSec * 1000);
 // let durationMs = endTime - startTime;
 return​ $obs.pipe(
  map(() => (animationFrame.now() - startTime) / (durationSec * 1000))
  );
  };
 }

While this function creates a startTime variable, rest assured that the inner function won’t be called until something subscribes to the observable—another advantage of lazy observables. This means that startTime won’t be set until the animation begins.

The next operator uses a map that takes an easing function. Right now, this is just a wrapper around the map operator.

 function​ easingMap(easingFn) {
 return​ ​function​ ($obs) {
 return​ $obs.pipe(map(easingFn));
  };
 }

This snippet may seem superfluous (why not just use map?), but it helps communicate intent. Later, you can add more concrete types to your operator with TypeScript, so that it will allow only certain types of functions.

Finally, let’s create a new tween function that takes advantage of these two new operators you created.

 function​ finalTween(easingFn, element, endPoint, duration) {
 let​ startPoint = element.style.left;
 let​ pixelsToMove = endPoint - startPoint;
 
  interval(1000 / 60, animationFrame)
  .pipe(
  percentDone(duration),
  takeWhile(percentDone => percentDone <= 1),
  easingMap(easingFn)
  )
  .subscribe((movePercent: number) => {
  element.style.left = startPoint + (pixelsToMove * movePercent) + ​'px'​;
  });
 }

We can then compose these together to demonstrate what several different types of easing operators look like:

 // Easing Functions
 let​ linear = p => p;
 let​ quadraticIn = p => p * p;
 let​ quadraticOut = p => p * (2 - p);
 
 finalTween(linear, redSquare, 500, 5);
 finalTween(quadraticIn, blueSquare, 500, 5);
 finalTween(quadraticOut, greenSquare, 500, 5);
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.251.70