Skip to content

Commit f51ffd8

Browse files
committed
Merge branch 'master' of github.com:datavis-tech/reactive-function
2 parents 42c799c + 3955513 commit f51ffd8

File tree

1 file changed

+167
-30
lines changed

1 file changed

+167
-30
lines changed

README.md

Lines changed: 167 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
1-
# reactive-function [![Build Status](https://travis-ci.org/datavis-tech/reactive-function.svg?branch=master)](https://travis-ci.org/datavis-tech/reactive-function)
1+
# reactive-function
22

3-
[![NPM](https://nodei.co/npm/reactive-function.png)](https://npmjs.org/package/reactive-function)
4-
5-
* A library for managing data flows graphs and changing state.
6-
* Built on [reactive-property](https://github.com/datavis-tech/reactive-property).
7-
* The foundation for [reactive-model](https://github.com/datavis-tech/reactive-model).
3+
A library for managing reactive data flows.
84

9-
# Usage
5+
[![NPM](https://nodei.co/npm/reactive-function.png)](https://npmjs.org/package/reactive-function)
6+
[![NPM](https://nodei.co/npm-dl/reactive-function.png?months=3)](https://npmjs.org/package/reactive-function)
7+
[![Build Status](https://travis-ci.org/datavis-tech/reactive-function.svg?branch=master)](https://travis-ci.org/datavis-tech/reactive-function)
108

11-
If you are using NPM, install this package with:
9+
This library provides the ability to define reactive data flows by modeling application state as a directed graph and using [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting) to compute the order in which changes should be propagated.
1210

13-
`npm install reactive-function`
11+
<p align="center">
12+
<img src="https://cloud.githubusercontent.com/assets/68416/15476439/82b83244-212c-11e6-91d5-d975de8b6b8a.png">
13+
<br>
14+
This library is built on top of <a href="https://github.com/datavis-tech/reactive-property">reactive-property</a> and <a href="https://github.com/datavis-tech/graph-data-structure">graph-data-structure</a>
15+
</p>
1416

15-
Require it in your code like this:
17+
**Table of Contents**
1618

17-
```javascript
18-
var ReactiveFunction = require("reactive-function");
19-
```
19+
* [Examples](#examples)
20+
* [Full Name](#full-name)
21+
* [ABC](#abc)
22+
* [Tricky Case](#tricky-case)
23+
* [Installing](#installing)
24+
* [API Reference](#api-reference)
25+
* [Managing Reactive Functions](#managing-reactive-functions)
26+
* [Data Flow Execution](#data-flow-execution)
2027

21-
This library is designed to work with [reactive-property](https://github.com/datavis-tech/reactive-property), you'll need that too.
22-
23-
```javascript
24-
var ReactiveProperty = require("reactive-property");
25-
```
28+
## Examples
2629

30+
### Full Name
2731
Suppose you have two reactive properties to represent someone's first and last name.
2832

2933
```javascript
3034
var firstName = ReactiveProperty("Jane");
3135
var lastName = ReactiveProperty("Smith");
3236
```
3337

34-
Suppose you'd like to have another property that represents the full name of the person.
38+
Another reactive property can represent the full name of the person.
3539

3640
```javascript
3741
var fullName = ReactiveProperty();
@@ -43,12 +47,10 @@ You could set the full name value like this.
4347
fullName(firstName() + " " + lastName());
4448
```
4549

46-
However, this sets the value of `fullName` only once, and it does not get updated when `firstName` or `lastName` change.
47-
48-
Here's how you can define a reactive function that updates the full name whenever the first name or last name changes.
50+
However, the above code sets the value of `fullName` only once. Here's how you can define a **[ReactiveFunction](#constructor)** that automatically updates `fullName` whenever `firstName` or `lastName` change.
4951

5052
```javascript
51-
ReactiveFunction({
53+
var reactiveFunction = ReactiveFunction({
5254
inputs: [firstName, lastName],
5355
output: fullName,
5456
callback: function (first, last){
@@ -57,33 +59,168 @@ ReactiveFunction({
5759
});
5860
```
5961

60-
This defines a "reactive function" that will be invoked when its inputs (`firstName` and `lastName`) are both defined and whenever either one changes ([`null` is considered a defined value](https://github.com/datavis-tech/reactive-function/issues/1)). The function will be invoked on the next tick of the JavaScript event loop after it is defined and after any dependencies change.
62+
<p align="center">
63+
<img src="https://cloud.githubusercontent.com/assets/68416/15389922/cf3f24dc-1dd6-11e6-92d6-058051b752ea.png">
64+
<br>
65+
The data flow graph for the example code above.
66+
</p>
6167

62-
To force a synchronous evaluation of all reactive functions whose dependencies have updated, you can call
68+
Whenever `firstName` or `lastName` change, the callback defined above will be executed on the next animation frame. If you don't want to wait until the next animation frame, you can force a synchronous evaluation of the data flow graph by invoking **[digest](#digest)**.
6369

6470
```javascript
6571
ReactiveFunction.digest();
6672
```
6773

68-
Now you can access the computed value of the reactive function by invoking it as a getter.
74+
Now you can access the computed `fullName` value by invoking it as a getter.
6975

7076
```javascript
7177
console.log(fullName()); // Prints "Jane Smith"
7278
```
7379

80+
### ABC
81+
82+
The output of one reactive function can be used as an input to another.
83+
84+
<p align="center">
85+
<img src="https://cloud.githubusercontent.com/assets/68416/15385597/44a10522-1dc0-11e6-9054-2150f851db46.png">
86+
<br>
87+
Here, b is both an output and an input.
88+
</p>
89+
90+
```javascript
91+
var a = ReactiveProperty(5);
92+
var b = ReactiveProperty();
93+
var c = ReactiveProperty();
94+
95+
ReactiveFunction({
96+
inputs: [a],
97+
output: b,
98+
callback: function (a){ return a * 2; }
99+
});
100+
101+
ReactiveFunction({
102+
inputs: [b],
103+
output: c,
104+
callback: function (b){ return b / 2; }
105+
});
106+
107+
ReactiveFunction.digest();
108+
assert.equal(c(), 5);
109+
```
110+
111+
### Tricky Case
112+
113+
This is the case where [Model.js](https://github.com/curran/model) fails because it uses [Breadth-first Search](https://en.wikipedia.org/wiki/Breadth-first_search) to propagate changes. In this graph, propagation using breadth-first search would cause `e` to be set twice, and the first time it would be set with an *inconsistent state*. This fundamental flaw cropped up as flashes of inconstistent states in some interactive visualizations built on Model.js. For example, it happens when you change the X column in this [Magic Heat Map](http://bl.ocks.org/curran/a54fc3a6578efcdc19f4). This flaw in Model.js is the main inspiration for making this library and using [topological sort](https://en.wikipedia.org/wiki/Topological_sorting), which is the correct algorithm for propagating data flows and avoiding inconsistent states.
114+
115+
<p align="center">
116+
<img src="https://cloud.githubusercontent.com/assets/68416/15400254/7f779c9a-1e08-11e6-8992-9d2362bfba63.png">
117+
<br>
118+
The tricky case, where breadth-first propagation fails.
119+
</p>
120+
121+
```javascript
122+
var a = ReactiveProperty(5);
123+
var b = ReactiveProperty();
124+
var c = ReactiveProperty();
125+
var d = ReactiveProperty();
126+
var e = ReactiveProperty();
127+
128+
ReactiveFunction({ inputs: [a], output: b, callback: function (a){ return a * 2; } });
129+
ReactiveFunction({ inputs: [b], output: c, callback: function (b){ return b + 5; } });
130+
ReactiveFunction({ inputs: [a], output: d, callback: function (a){ return a * 3; } });
131+
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } });
132+
133+
ReactiveFunction.digest();
134+
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));
135+
136+
a(10);
137+
ReactiveFunction.digest();
138+
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));
139+
```
140+
74141
For more detailed example code, have a look at the [tests](https://github.com/datavis-tech/reactive-function/blob/master/test.js).
75142

76-
Related work:
143+
## Installing
144+
145+
If you are using [NPM](npmjs.com), install this package with:
146+
147+
`npm install reactive-function`
148+
149+
Require it in your code like this:
150+
151+
```javascript
152+
var ReactiveFunction = require("reactive-function");
153+
```
154+
155+
This library is designed to work with [reactive-property](https://github.com/datavis-tech/reactive-property), you'll need that too.
156+
157+
`npm install reactive-property`
158+
159+
```javascript
160+
var ReactiveProperty = require("reactive-property");
161+
```
162+
163+
## API Reference
164+
165+
* [Managing Reactive Functions](#managing-reactive-functions)
166+
* [Data Flow Execution](#data-flow-execution)
167+
168+
### Managing Reactive Functions
77169

170+
<a name="constructor" href="#constructor">#</a> <b>ReactiveFunction</b>(<i>options</i>)
171+
172+
Construct a new reactive function. The *options* argument should have the following properties.
173+
174+
* *inputs* - The input properties. An array of **[ReactiveProperty](https://github.com/datavis-tech/reactive-property#constructor)** instances.
175+
* *output* (optional) - The output property. An instance of **[ReactiveProperty](https://github.com/datavis-tech/reactive-property#constructor)**.
176+
* *callback* - The reactive function callback. Arguments are values of *inputs*. The return value will be assigned to *output*.
177+
178+
This constructor sets up a reactive function such that *callback* be invoked
179+
180+
* when all input properties are defined,
181+
* after any input properties change,
182+
* during a **[digest](#digest)**.
183+
184+
An input property is considered "defined" if it has any value other than `undefined`. The special value `null` is considered to be defined.
185+
186+
An input property is considered "changed" when
187+
188+
* the reactive function is initially set up, and
189+
* whenever its value is set.
190+
191+
Input properties for one reactive function may also be outputs of another.
192+
193+
<a name="destroy" href="#destroy">#</a> <i>reactiveFunction</i>.<b>destroy</b>()
194+
195+
Cleans up resources allocated to this reactive function and removes listeners from inputs.
196+
197+
### Data Flow Execution
198+
199+
<a name="digest" href="#digest">#</a> ReactiveFunction.<b>digest</b>()
200+
201+
Propagates changes from input properties through the data flow graph defined by all reactive properties using [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting). An edge in the data flow graph corresponds to a case where the output of one reactive function is used as an input to another.
202+
203+
Whenever any input properties for any reactive function change, **[digest](#digest)** is debounced (scheduled for invocation) on the **[nextFrame](#next-frame)**. Because it is debounced, multiple synchronous changes to input properties collapse into a single digest invocation.
204+
205+
Digests are debounced to the next animation frame rather than the next tick because browsers will render the page at most every animation frame (approximately 60 frames per second). This means that if DOM manipulations are triggered by reactive functions, and input properties are changed more frequently than 60 times per second (e.g. mouse or keyboard events), the DOM manipulations will only occur at most 60 times per second, not more than that.
206+
207+
<a name="next-frame" href="#next-frame">#</a> ReactiveFunction.<b>nextFrame</b>(<i>callback</i>)
208+
209+
Schedules the given function to execute on the next animation frame or next tick.
210+
211+
This is a simple polyfill for [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) that falls back to [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout). The main reason for having this is for use in the [tests](https://github.com/datavis-tech/reactive-function/blob/master/test.js), which run in a Node.js environment where `requestAnimationFrame` is not available. Automatic digests are debounced against this function.
212+
213+
## Related Work
214+
215+
* [Functional Reactive Animation (academic paper)](https://www.eecs.northwestern.edu/~robby/courses/395-495-2009-winter/fran.pdf)
78216
* [ReactiveJS](https://github.com/mattbaker/Reactive.js)
79217
* [vogievetsky/DVL](https://github.com/vogievetsky/DVL)
80218
* [EmberJS Computed Properties](https://guides.emberjs.com/v2.0.0/object-model/computed-properties/)
81219
* [AngularJS Digest](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest)
82220
* [ZJONSSON/clues](https://github.com/ZJONSSON/clues)
83221
* [Model.js](https://github.com/curran/model)
84-
85-
Thanks to Mike Bostock and Vadim Ogievetsky for suggesting to have the ability to digest within a single tick of the event loop (the main difference between this project and the original [model-js](https://github.com/curran/model)). This idea has inspired the construction of this library, which I hope will provide a solid foundation for complex interactive visualization systems.
86-
222+
* [RxJS - when](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/when.md)
223+
* [Bacon - When](https://github.com/baconjs/bacon.js/tree/master#bacon-when)
87224

88225
<p align="center">
89226
<a href="https://datavis.tech/">

0 commit comments

Comments
 (0)