Skip to content

Commit f633b36

Browse files
committed
initial commit
0 parents  commit f633b36

13 files changed

+735
-0
lines changed

.gitignore

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.DS_Store
2+
.monitor
3+
.*.swp
4+
.nodemonignore
5+
releases
6+
*.log
7+
*.err
8+
fleet.json
9+
public/browserify
10+
bin/*.json
11+
.bin
12+
build
13+
compile
14+
.lock-wscript
15+
coverage
16+
node_modules

LICENCE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2014 Matt-Esch.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# Virtual DOM and diffing algorithm
2+
3+
There was a [great article][1] about how react implements it's
4+
virtual DOM. There are some really interesting ideas in there
5+
but they are deeply buried in the implementation of the React
6+
framework.
7+
8+
However, it's possible to implement just the virtual DOM and
9+
diff algorithm on it's own as a set of independent modules.
10+
11+
## Motivation
12+
13+
The reason we wan't a diff engine is so that we can write our
14+
templates as plain javascript functions that take in our
15+
current application state and returns a visual representation
16+
of the view for that state.
17+
18+
However normally when you do this, you would have to re-create
19+
the entire DOM for that view each time the state changed and
20+
swap out the root node for your view. This is terrible for
21+
performance but also blows away temporary state like user input
22+
focus.
23+
24+
A virtual DOM approach allows you to re-create a virtual DOM
25+
for the view each time the state changes. Creating a virtual
26+
DOM in JavaScript is cheap compared to DOM operations. You can
27+
then use the 60 fps batched DOM writer to apply differences
28+
between the current DOM state and the new virtual DOM state.
29+
30+
One important part of the virtual DOM approach is that it is a
31+
**module** and it **should do one thing well**. The virtual DOM
32+
is only concerned with representing the virtual DOM. The `diff`
33+
`batch` and `patch` functions are only concerned with the
34+
relevant algorithms for the virtual dom.
35+
36+
The virtual DOM has nothing to do with events or representing
37+
application state. The below example demonstrates the usage
38+
of state with `observ` and events with `dom-delegator`. It
39+
could just as well have used `knockout` or `backbone` for state
40+
and used `jQuery` or `component/events` for events.
41+
42+
## Example
43+
44+
**Warning:** Vaporware. The `virtual-dom` is not implemented yet.
45+
46+
```js
47+
var h = require("virtual-dom/h")
48+
var render = require("virtual-dom/render")
49+
var raf = require("raf").polyfill
50+
var Observ = require("observ")
51+
var ObservArray = require("observ-array")
52+
var computed = require("observ/computed")
53+
var Delegator = require("dom-delegator")
54+
var diff = require("virtual-dom-diff")
55+
var patch require("virtual-dom-patch")
56+
var batch = require("virtual-dom-batch")
57+
58+
// logic that takes state and renders your view.
59+
function TodoList(items) {
60+
return h("ul", items.map(function (text) {
61+
return h("li", text)
62+
}))
63+
}
64+
65+
function TodoApp(state) {
66+
return h("div", [
67+
h("h3", "TODO"),
68+
{ render: TodoList, data: state.items },
69+
h("div", { "data-submit": "addTodo" }, [
70+
h("input", { value: state.text, name: "text" }),
71+
h("button", "Add # " + state.items.length + 1)
72+
])
73+
])
74+
}
75+
76+
// model the state of your app
77+
var state = {
78+
text: Observ(""),
79+
items: ObservArray([])
80+
}
81+
82+
// react to inputs and change state
83+
var delegator = Delegator(document.body)
84+
delegator.on("addTodo", function (ev) {
85+
state.items.push(ev.currentValue.text)
86+
state.text.set("")
87+
})
88+
89+
// render initial state
90+
var currTree = TodoApp({ text: state.text(), items: state.items().value })
91+
var elem = render(currTree)
92+
93+
document.body.appendChild(elem)
94+
95+
// when state changes diff the state
96+
var diffQueue = []
97+
var applyUpdate = false
98+
computed([state.text, state.items], function () {
99+
// only call `update()` in next tick.
100+
// this allows for multiple synchronous changes to the state
101+
* // in the current tick without re-rendering the virtual DOM
102+
if (applyUpdate === false) {
103+
applyUpdate = true
104+
setImmediate(function () {
105+
update()
106+
applyUpdate = false
107+
})
108+
}
109+
})
110+
111+
function update() {
112+
var newTree = TodoApp({ text: state.text(), items: state.items().value })
113+
114+
// calculate the diff from currTree to newTree
115+
var patches = diff(currTree, newTree)
116+
diffQueue = diffQueue.concat(patches)
117+
currTree = newTree
118+
}
119+
120+
// at 60 fps, batch all the patches and then apply them
121+
raf(function renderDOM() {
122+
var patches = batch(diffQueue)
123+
patch(elem, patches)
124+
125+
raf(renderDOM)
126+
})
127+
```
128+
129+
## Documentation
130+
131+
### `var virtualDOM = h(tagName, props?, children?)`
132+
133+
`h` creates a virtual DOM tree. You can give it a `tagName` and
134+
optionally DOM properties & optionally an array of children.
135+
136+
### `var elem = render(virtualDOM)`
137+
138+
`render` takes a virtual DOM tree and turns it into a DOM element
139+
that you can put in your DOM. Use this to render the initial
140+
tree.
141+
142+
### `var patches = diff(previousTree, currentTree)`
143+
144+
`diff` takes two virtual DOM tree and returns an array of virtual
145+
DOM patches that you would have to apply to the `previousTree`
146+
to create the `currentTree`
147+
148+
This function is used to determine what has changed in the
149+
virtual DOM tree so that we can later apply a minimal set of
150+
patches to the real DOM, since touching the real DOM is slow.
151+
152+
### `var patches = batch(patches)`
153+
154+
`batch` can be used to take a large array of patches, generally
155+
more then what is returned by a single `diff` call and will
156+
then use a set of global heuristics to produce a smaller more
157+
optimal set of patches to apply to a DOM tree.
158+
159+
Generally you want to call `batch` 60 or 30 times per second to
160+
compute the optimal set of DOM mutations to apply. This is
161+
great if your application has large spikes of state changes
162+
that you want to condense into a smaller more optimal set of
163+
DOM mutations.
164+
165+
`batch` also does other useful things like re-ordering mutations
166+
to avoid reflows.
167+
168+
### `patch(elem, patches)`
169+
170+
`patch` will take a real DOM element and apply the DOM mutations
171+
in order. This is the only part that actually does the
172+
expensive work of mutating the DOM.
173+
174+
We recommend you do this in a `requestAnimationFrame` handler.
175+
176+
## Concept
177+
178+
The goal is to represent your template as plain old javascript
179+
functions. Using actual `if` statements instead of
180+
`{{#if }} ... {{/if}}` and all other flow control build into
181+
javascript.
182+
183+
One approach that works very well is [hyperscript][2] however
184+
that will re-create a DOM node each time you re-render your
185+
view which is expensive.
186+
187+
A better solution is to have a `h` function that returns a
188+
virtual DOM tree. Creating a virtual DOM in JavaScript is
189+
cheap compared to manipulating the DOM directly.
190+
191+
Once we have two virtual DOM trees. One for the current application
192+
state and one for the previous we can use the `diff` function
193+
to produce a minimal set of patches from the previous virtual
194+
DOM to the current virtual DOM.
195+
196+
Once you have a set of patches, you could apply them immediately
197+
but it's better to queue them and flush this queue at a fixed
198+
interval like 60 times per second. Only doing our DOM
199+
manipulation with the callback to `requestAnimationFrame` will
200+
give us a performance boost and minimize the number of DOM
201+
operations we do. We also call `batch` in before we apply
202+
our patches to squash our list of diffs to the minimal set of
203+
operations.
204+
205+
Another important thing to note is that our virtual DOM tree
206+
contains a notion of a `Component` which is
207+
`{ render: function (data) { return tree }, data: { ... } }`.
208+
209+
This is an important part of making the virtual DOM fast. Calling
210+
`render()` is cheap because it only renders a single layer and
211+
embeds components for all it's child views. The `diff` engine
212+
then has the option to compare the `data` key of a component
213+
between the current and previous one, if the `data` hasn't
214+
changed then it doesn't have to re-render that component.
215+
216+
The `component` can also implement a `compare` function to
217+
compare the data between the previous and current to tell us
218+
whether or not the change requires a re-render.
219+
220+
This means you only have to re-render the components that have
221+
changed instead of re-rendering the entire virtual DOM tree
222+
any time a piece of application state changes.
223+
224+
225+
[1]: http://calendar.perfplanet.com/2013/diff/
226+
[2]: https://github.com/dominictarr/hyperscript

h.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
var extend = require("extend")
2+
3+
var isArray = require("./lib/is-array")
4+
var isString = require("./lib/is-string")
5+
var parseTag = require("./lib/parse-tag")
6+
7+
var VirtualDOMNode = require("./virtual-dom-node.js")
8+
9+
module.exports = h
10+
11+
function h(tagName, properties, children) {
12+
var childNodes = []
13+
var tag, props
14+
15+
if (!children) {
16+
if (isChildren(properties)) {
17+
children = properties
18+
props = {}
19+
}
20+
}
21+
22+
props = props || extend({}, properties)
23+
tag = parseTag(tagName, props)
24+
25+
26+
if (children) {
27+
if (isArray(children)) {
28+
childNodes.push.apply(childNodes, children)
29+
} else {
30+
childNodes.push(children)
31+
}
32+
}
33+
34+
return new VirtualDOMNode(tag, props, childNodes)
35+
}
36+
37+
function isChild(x) {
38+
return isString(x) || (x instanceof VirtualDOMNode)
39+
}
40+
41+
function isChildren(x) {
42+
return isChild(x) || isArray(x)
43+
}

index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = virtualDom
2+
3+
function virtualDom() {
4+
5+
}

lib/is-array.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var nativeIsArray = Array.isArray
2+
var toString = Object.prototype.toString
3+
4+
module.exports = nativeIsArray || isArray
5+
6+
function isArray(obj) {
7+
return toString.call(obj) === "[object Array]"
8+
}

lib/is-object.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = isObject
2+
3+
function isObject(x) {
4+
return x === Object(x)
5+
}

lib/is-string.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var toString = Object.prototype.toString
2+
3+
module.exports = isString
4+
5+
function isString(obj) {
6+
return toString.call(obj) === "[object String]"
7+
}

lib/parse-tag.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
var split = require("browser-split")
2+
3+
var classIdSplit = /([\.#]?[a-zA-Z0-9_:-]+)/
4+
var notClassId = /^\.|#/
5+
6+
module.exports = parseTag
7+
8+
function parseTag(tag, props) {
9+
if (!tag) {
10+
return "div"
11+
}
12+
13+
var noId = !("id" in props)
14+
15+
var tagParts = split(tag, classIdSplit)
16+
var tagName = null
17+
18+
if(notClassId.test(tagParts[1])) {
19+
tagName = "div"
20+
}
21+
22+
var classes, part, type, i
23+
for (i = 0; i < tagParts.length; i++) {
24+
part = tagParts[i]
25+
26+
if (!part) {
27+
continue
28+
}
29+
30+
type = part.charAt(0)
31+
32+
if (!tagName) {
33+
tagName = part
34+
} else if (type === ".") {
35+
classes = classes || []
36+
classes.push(part.substring(1, part.length))
37+
} else if (type === "#" && noId) {
38+
props.id = part.substring(1, part.length)
39+
}
40+
}
41+
42+
if (classes) {
43+
if (props.className) {
44+
classes.push(props.className)
45+
}
46+
47+
props.className = classes.join(" ")
48+
}
49+
50+
return tagName ? tagName.toLowerCase() : "div"
51+
}

0 commit comments

Comments
 (0)