FYI: This is one of three topics of our third Meteor Meetup on August 22th, 2016. The author is Khang Nguyen, a young talent member of Designveloper. This article is based on his writing, you can read the original one here.
Introduction
As a Meteor developer, I believe that everyone who has worked with Meteor all had experience with Tracker, or at least used it through some kinds of interfaces like getMeteordata or createContainer which come with the react-meteor-data package.
However, not all people know how Tracker works its magic. In this post, I’m going to dig into every facet of this case as well as bring to you a little bit of background knowledge.
What Is Tracker?
Tracker is a Meteor’s core package, it is a small but incredibly powerful library for transparent reactive programming in Meteor. Using Tracker you have much of the power of the Functional Reactive Programming FRP system without following FRP’s principles when implementing your application.
Combined with Tracker-aware libraries, like Session/ReactiveVar/ReactiveDict, this lets you build complex event-driven programs without writing a lot of boilerplate event-handling code.
What Makes It Great?
In a nutshell, it is reactivity. In my opinion, it is the strongest aspect of Meteor. Tracker helps us make our system work reactively both on client and server with ease. We do not have to learn any extra pieces stuff about reactive programming or functional programming to get started.
Please check out the list of "Meteor for Beginners - Easy and Practical" videos:
Just read the Tracker API and do the work then the magic happens.
Or even some Meteor-novice who do not know a thing about Tracker, their code still works reactively. Do you know what am I talking about? It is a good old Blaze (I say it’s old because I already moved to React for all new projects).
Blaze’s helpers are reactive natively because they use Tracker inside if you put inside them a reactive data source then whenever that source changes those helpers will be recomputed and you get new data on the screen.
Let’s read some code and behold what I am talking about, if you are already familiar with Tracker, skip this part and move to the next section to inspect the magic.
// Set up the reactive code const counter1 = new ReactiveVar(0); const counter2 = new ReactiveVar(0); const observeCounter = function() { Tracker.autorun(function() { const text = `Counter1 is now: ${counter1.get()}`; console.warn(text); }); console.warn(`Counter2 is now: ${counter2.get()}`); }; const computation = Tracker.autorun(observeCounter); /* Message on the console: Counter1 is now: 0 Counter2 is now: 0 */ // and now change the counter1's value counter1.set(1); /* Message on the console: Counter1 is now: 1 */ counter2.set(3); /* Message on the console: Counter1 is now: 1 Counter2 is now: 3 */ counter1.set(7); /* Message on the console: Counter1 is now: 7 */
How Does Tracker Work?
Basically, Tracker is a simple interface that lets reactive data sources (like the counter in the example above) talk to reactive data consumers (the observeCounter function). Below is the flow of Tracker (I ignore some good parts to make it as simple as possible)
- Call Tracker.autorun with function F
- If inside F, there is a reactive data source named R, then that source will add F to its dependence list
- Then whenever the value of R changes, R will retrieve all its dependence from the dependence list and run them.
Everything is easier said than done. So, let’s take a look at the code:
const counter = new ReactiveVar(1); const f = function() { console.log(counter.get()); }; Tracker.autorun(f);
In the above code: the counter is our reactive data source. It raises the question is how can it know what function is used inside to add that function as its dependence?
This is where the Meteor team does its trick to make this flow transparent. In fact, Tracker is an implementation of the Observer pattern or at least an Observer-liked pattern. An observer pattern can be used to create a reactive library like Tracker.
In a traditional implementation of Observer, we can think of F as an observer and R as a subject. R must have an interface to add F as its observer and notify/run F when its value changes. Something like this:
const counter = new Source(); const f = function(val) { console.log(val); }; counter.addObserver(f);
To imitate this, Tracker provides us with these interfaces:
- Tracker.autorun
- Tracker.Dependency
- Tracker.Dependency.prototype.depend
- Tracker.Dependency.prototype.changed
Tracker.Dependency is the implementation of the Subject in a traditional Observer. All of the reactive data sources to use with Tracker use this object inside. Let’s look at the basic implementation of ReactiveVar I use for the examples above:
ReactiveVar = function (initialValue, equalsFunc) { if (! (this instanceof ReactiveVar)) // called without `new` return new ReactiveVar(initialValue, equalsFunc); this.curValue = initialValue; this.equalsFunc = equalsFunc; this.dep = new Tracker.Dependency; }; ReactiveVar.prototype.get = function () { if (Tracker.active) this.dep.depend(); return this.curValue; }; ReactiveVar.prototype.set = function (newValue) { var oldValue = this.curValue; if ((this.equalsFunc || ReactiveVar._isEqual)(oldValue, newValue)) // value is same as last time return; this.curValue = newValue; this.dep.changed(); };
So when we create a new instance of ReactiveVar, a Tracker.Dependency objects will be created. This object will have two main functions: depend and changed with getting a call inside get and set function respectively.
This is the Tracker’s flow with more details:
Tracker.autorun will create a computation with the function (F) passed to it as the computation’s props.
// https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L569-L585 Tracker.autorun = function(f, options) { // ... var c = new Tracker.Computation(f, Tracker.currentComputation, options.onError); // ... return c; };
When being initiated, this computation is also set as the current computation inside a “global” variable named Tracker.currentComputation. And F will be run for the first time.
// https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L146-L208 Tracker.Computation = function(f, parent, onError) { // ... self._func = f; self._onError = onError; self._recomputing = false; var errored = true; try { self._compute(); errored = false; } finally { self.firstRun = false; if (errored) self.stop(); } }; // https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L302-L316 Tracker.Computation.prototype._compute = function() { var self = this; self.invalidated = false; var previous = Tracker.currentComputation; setCurrentComputation(self); var previousInCompute = inCompute; inCompute = true; try { withNoYieldsAllowed(self._func)(self); } finally { setCurrentComputation(previous); inCompute = previousInCompute; } }; // https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L29-L32 var setCurrentComputation = function(c) { Tracker.currentComputation = c; Tracker.active = !!c; };
If there is a .get operation (meaning .depend) inside the body of F. This function will be run and set the current computation stored in the global var named Tracker.currentComputation as its dependent.
// https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L403-L420 Tracker.Dependency.prototype.depend = function(computation) { if (!computation) { // ... computation = Tracker.currentComputation; } var self = this; var id = computation._id; if (!(id in self._dependentsById)) { self._dependentsById[id] = computation; // ... return true; } return false; };
Then whenever .set is called (meaning .changed), F will be rerun
// https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js#L428-L432 Tracker.Dependency.prototype.changed = function() { var self = this; for (var id in self._dependentsById) self._dependentsById[id].invalidate(); };
Yeah, so it is the basic idea. Beyond this basic flow actually, there are some other important things to take care of to have a complete production-ready Tracker library. I am not going to write about those things. Instead, I will just name them. And you can go and check yourself for a deeper understanding of Tracker. They are:
- Tracker inside Tracker (computation inside computation)
- Clear stopped computations
- Prevent infinite loop
Takeaways
Here’s something we’ve discovered so far:
- Tracker makes it possible to do reactive programming in Meteor
- It is an observer-liked implementation
- A good trick: use a “global currentComputation” to implement a transparent Observer
- Those guys who wrote Tracker are awesome 😉
So, did you find some great information on your own in this blog? I would love to know your ideas in the comments below.
Also, don’t forget to help this post spread by emailing it to a friend, or sharing it on Twitter or Facebook if you enjoyed it. Thank you!