virtual-dom
Advanced tools
Comparing version 0.0.12 to 0.0.13
{ | ||
"name": "virtual-dom", | ||
"version": "0.0.12", | ||
"version": "0.0.13", | ||
"description": "A batched diff-based DOM rendering strategy", | ||
@@ -22,4 +22,4 @@ "keywords": [], | ||
"is-object": "0.1.2", | ||
"vdom": "0.0.5", | ||
"vtree": "0.0.4", | ||
"vdom": "0.0.6", | ||
"vtree": "0.0.5", | ||
"x-is-array": "0.1.0", | ||
@@ -29,2 +29,3 @@ "x-is-string": "0.1.0" | ||
"devDependencies": { | ||
"global": "^4.2.1", | ||
"min-document": "^2.6.1", | ||
@@ -31,0 +32,0 @@ "run-browser": "git://github.com/Raynos/run-browser", |
266
README.md
@@ -1,229 +0,121 @@ | ||
# Virtual DOM and diffing algorithm | ||
# virtual-dom | ||
(Please note this is currently work in progress) | ||
A JavaScript [DOM model](#dom-model) supporting [element creation](#element-creation), [diff computation](#diff-computation) and [patch operations](#patch-operations) for efficient re-rendering | ||
There was a [great article][1] about how react implements its | ||
virtual DOM. There are some really interesting ideas in there | ||
but they are deeply buried in the implementation of the React | ||
framework. | ||
However, it's possible to implement just the virtual DOM and | ||
diff algorithm on it's own as a set of independent modules. | ||
## Motivation | ||
The reason we want a diff engine is so that we can write our | ||
templates as plain javascript functions that take in our | ||
current application state and return a visual representation | ||
of the view for that state. | ||
Manual DOM manipulation is messy and keeping track of the previous DOM state is hard. A solution to this problem is to write your code as if you were recreating the entire DOM whenever state changes. Of course, if you actually recreated the entire DOM every time your application state changed, your app would be very slow and your input fields would lose focus. | ||
However, normally when you do this, you would have to re-create | ||
the entire DOM for that view each time the state changed and | ||
swap out the root node for your view. This is terrible for | ||
performance but also blows away temporary state like user input | ||
focus. | ||
`virtual-dom` is a collection of modules designed to provide a declarative way of representing the DOM for your app. So instead of updating the DOM when your application state changes, you simply create a virtual tree or `VTree`, which looks like the DOM state that you want. `virtual-dom` will then figure out how to make the DOM look like this efficiently without recreating all of the DOM nodes. | ||
A virtual DOM approach allows you to re-create a virtual DOM | ||
for the view each time the state changes. Creating a virtual | ||
DOM in JavaScript is cheap compared to DOM operations. You can | ||
then use the 60 fps batched DOM writer to apply differences | ||
between the current DOM state and the new virtual DOM state. | ||
`virtual-dom` allows you to update a view whenever state changes by creating a full `VTree` of the view and then patching the DOM efficiently to look exactly as you described it. This results in keeping manual DOM manipulation and previous state tracking out of your application code, promoting clean and maintainable rendering logic for web applications. | ||
One important part of the virtual DOM approach is that it is a | ||
**module** and it **should do one thing well**. The virtual DOM | ||
is only concerned with representing the virtual DOM. The `diff`, | ||
`batch` and `patch` functions are only concerned with the | ||
relevant algorithms for the virtual dom. | ||
The virtual DOM has nothing to do with events or representing | ||
application state. The below example demonstrates the usage | ||
of state with `observ` and events with `dom-delegator`. It | ||
could just as well have used `knockout` or `backbone` for state | ||
and used `jQuery` or `component/events` for events. | ||
## Example | ||
**Warning:** Vaporware. The `virtual-dom` is not implemented yet. | ||
```javascript | ||
var h = require('virtual-dom/h'); | ||
var diff = require('virtual-dom/diff'); | ||
var patch = require('virtual-dom/patch'); | ||
var createElement = require('virtual-dom/create-element'); | ||
```js | ||
var h = require("virtual-dom/h") | ||
var createElement = require("virtual-dom/create-element") | ||
var raf = require("raf").polyfill | ||
var Observ = require("observ") | ||
var ObservArray = require("observ-array") | ||
var computed = require("observ/computed") | ||
var Delegator = require("dom-delegator") | ||
var diff = require("virtual-dom-diff") | ||
var patch require("virtual-dom-patch") | ||
var batch = require("virtual-dom-batch") | ||
// logic that takes state and renders your view. | ||
function TodoList(items) { | ||
return h("ul", items.map(function (text) { | ||
return h("li", text) | ||
})) | ||
// 1: Create a function that declares what the DOM should look like | ||
function render(count) { | ||
return h('div', { | ||
style: { | ||
textAlign: 'center', | ||
verticalAlign: 'center', | ||
lineHeight: (100 + count) + 'px', | ||
border: '1px solid red', | ||
width: (100 + count) + 'px', | ||
height: (100 + count) + 'px' | ||
} | ||
}, [String(count)]); | ||
} | ||
function TodoApp(state) { | ||
return h("div", [ | ||
h("h3", "TODO"), | ||
{ render: TodoList, data: state.items }, | ||
h("div", { "data-submit": "addTodo" }, [ | ||
h("input", { value: state.text, name: "text" }), | ||
h("button", "Add # " + state.items.length + 1) | ||
]) | ||
]) | ||
} | ||
// 2: Initialise the document | ||
var count = 0; // We need some app data. Here we just store a count. | ||
// model the state of your app | ||
var state = { | ||
text: Observ(""), | ||
items: ObservArray([]) | ||
} | ||
var tree = render(count); // We need an initial tree | ||
var rootNode = createElement(tree); // Create an initial root DOM node ... | ||
document.body.appendChild(rootNode); // ... and it should be in the document | ||
// react to inputs and change state | ||
var delegator = Delegator(document.body) | ||
delegator.on("addTodo", function (ev) { | ||
state.items.push(ev.currentValue.text) | ||
state.text.set("") | ||
}) | ||
// 3: Wire up the update logic | ||
setInterval(function () { | ||
count++; | ||
// render initial state | ||
var currTree = TodoApp({ text: state.text(), items: state.items().value }) | ||
var elem = createElement(currTree) | ||
document.body.appendChild(elem) | ||
// when state changes diff the state | ||
var diffQueue = [] | ||
var applyUpdate = false | ||
computed([state.text, state.items], function () { | ||
// only call `update()` in next tick. | ||
// this allows for multiple synchronous changes to the state | ||
// in the current tick without re-rendering the virtual DOM | ||
if (applyUpdate === false) { | ||
applyUpdate = true | ||
setImmediate(function () { | ||
update() | ||
applyUpdate = false | ||
}) | ||
} | ||
}) | ||
function update() { | ||
var newTree = TodoApp({ text: state.text(), items: state.items().value }) | ||
// calculate the diff from currTree to newTree | ||
var patches = diff(currTree, newTree) | ||
diffQueue = diffQueue.concat(patches) | ||
currTree = newTree | ||
} | ||
// at 60 fps, batch all the patches and then apply them | ||
raf(function renderDOM() { | ||
var patches = batch(diffQueue) | ||
elem = patch(elem, patches) | ||
raf(renderDOM) | ||
}) | ||
var newTree = render(count); | ||
var patches = diff(tree, newTree); | ||
rootNode = patch(rootNode, patches); | ||
tree = newTree; | ||
}, 1000); | ||
``` | ||
[View on RequireBin](http://requirebin.com/?gist=5492847b9a9025e64bab) | ||
## Documentation | ||
## DOM model | ||
### `var virtualDOM = h(tagName, props?, children?)` | ||
`virtual-dom` exposes a set of objects designed for representing DOM nodes. A "Document Object Model Model" might seem like a strange term, but it is exactly that. It's a native JavaScript tree structure that represents a native DOM node tree. We call this a **VTree** | ||
`h` creates a virtual DOM tree. You can give it a `tagName` and | ||
optionally DOM properties & optionally an array of children. | ||
We can create a VTree using the objects directly in a verbose manner, or we can use the more terse virtual-hyperscript. | ||
### `var elem = createElement(virtualDOM)` | ||
### Example - creating a VTree using the objects directly | ||
`createElement` takes a virtual DOM tree and turns it into a DOM element | ||
that you can put in your DOM. Use this to render the initial | ||
tree. | ||
```javascript | ||
var VNode = require('vtree/vnode'); | ||
var VText = require('vtree/vtext'); | ||
### `var patches = diff(previousTree, currentTree)` | ||
function render(data) { | ||
return new VNode('div', { | ||
className: "greeting" | ||
}, [ | ||
new VText("Hello " + String(data.name)); | ||
]); | ||
} | ||
`diff` takes two virtual DOM tree and returns an array of virtual | ||
DOM patches that you would have to apply to the `previousTree` | ||
to create the `currentTree` | ||
module.exports = render; | ||
``` | ||
### Example - creating a VTree using virtual-hyperscript | ||
This function is used to determine what has changed in the | ||
virtual DOM tree so that we can later apply a minimal set of | ||
patches to the real DOM, since touching the real DOM is slow. | ||
```javascript | ||
var h = require('virtual-dom/h'); | ||
### `var patches = batch(patches)` | ||
function render(data) { | ||
return h('.greeting', ['Hello ' + data.name]); | ||
} | ||
`batch` can be used to take a large array of patches, generally | ||
more then what is returned by a single `diff` call and will | ||
then use a set of global heuristics to produce a smaller more | ||
optimal set of patches to apply to a DOM tree. | ||
module.exports = render; | ||
``` | ||
Generally you want to call `batch` 60 or 30 times per second to | ||
compute the optimal set of DOM mutations to apply. This is | ||
great if your application has large spikes of state changes | ||
that you want to condense into a smaller more optimal set of | ||
DOM mutations. | ||
The DOM model is designed to be efficient to create and read from. The reason why we don't just create a real DOM tree is that creating DOM nodes and reading the node properties is an expensive operation which is what we are trying to avoid. Reading some DOM node properties even causes side effects, so recreating the entire DOM structure with real DOM nodes simply isn't suitable for high performance rendering and it is not easy to reason about either. | ||
`batch` also does other useful things like re-ordering mutations | ||
to avoid reflows. | ||
A `VTree` is designed to be equivalent to an immutable data strucuture. While it's not actually immutable, you can reuse the nodes in multiple places and the functions we have exposed that take VTrees as arguments never mutate the trees. We could freeze the objects in the model but don't for efficiency. (The benefits of an immutable-equivalent data structure will be documented in vtree or blog post at some point) | ||
### `var elem = patch(elem, patches)` | ||
`patch` will take a real DOM element and apply the DOM mutations | ||
in order. This is the only part that actually does the | ||
expensive work of mutating the DOM. In case that the root node | ||
needs to be replaced, the root is returned from the operation | ||
We recommend you do this in a `requestAnimationFrame` handler. | ||
## Element creation | ||
## Concept | ||
```ocaml | ||
createElement(tree:VTree) -> DOMNode | ||
``` | ||
The goal is to represent your template as plain old javascript | ||
functions. Using actual `if` statements instead of | ||
`{{#if }} ... {{/if}}` and all other flow control build into | ||
javascript. | ||
Given that we have created a `VTree`, we need some way to translate this into a real DOM tree of some sort. This is provided by `create-element.js`. When rendering for the first time we would pass a complete `VTree` to create-element function to create the equivalent DOM node. | ||
One approach that works very well is [hyperscript][2] however | ||
that will re-create a DOM node each time you re-render your | ||
view which is expensive. | ||
## Diff computation | ||
A better solution is to have a `h` function that returns a | ||
virtual DOM tree. Creating a virtual DOM in JavaScript is | ||
cheap compared to manipulating the DOM directly. | ||
```ocaml | ||
diff(previous:VTree, current:VTree) -> PatchObject | ||
``` | ||
Once we have two virtual DOM trees. One for the current application | ||
state and one for the previous we can use the `diff` function | ||
to produce a minimal set of patches from the previous virtual | ||
DOM to the current virtual DOM. | ||
The primary motivation behind virtual-dom is to allow use to write code indepentent of previous state. So when our application state changes we will generate a new `VTree`. The `diff` function creates a set of DOM patches that, based on the difference between the previous `VTree` and the current `VTree` | ||
Once you have a set of patches, you could apply them immediately | ||
but it's better to queue them and flush this queue at a fixed | ||
interval like 60 times per second. Only doing our DOM | ||
manipulation with the callback to `requestAnimationFrame` will | ||
give us a performance boost and minimize the number of DOM | ||
operations we do. We also call `batch` in before we apply | ||
our patches to squash our list of diffs to the minimal set of | ||
operations. | ||
## Patch operations | ||
Another important thing to note is that our virtual DOM tree | ||
contains a notion of a `Component` which is | ||
`{ render: function (data) { return tree }, data: { ... } }`. | ||
```ocaml | ||
patch(rootNode:DOMNode, patches:PatchObject) -> DOMNode newRootNode | ||
``` | ||
This is an important part of making the virtual DOM fast. Calling | ||
`render()` is cheap because it only renders a single layer and | ||
embeds components for all it's child views. The `diff` engine | ||
then has the option to compare the `data` key of a component | ||
between the current and previous one, if the `data` hasn't | ||
changed then it doesn't have to re-render that component. | ||
Once we have computed the set of patches required to apply to the DOM, we need a function that can apply those patches. This is provided by the `patch` function. Given a DOM root node and a set of DOM patches, the `patch` function will update the DOM. After applying the patches to the DOM, the DOM should look like the new `VTree`. | ||
The `component` can also implement a `compare` function to | ||
compare the data between the previous and current to tell us | ||
whether or not the change requires a re-render. | ||
This means you only have to re-render the components that have | ||
changed instead of re-rendering the entire virtual DOM tree | ||
any time a piece of application state changes. | ||
## Original motivation | ||
[1]: http://calendar.perfplanet.com/2013/diff/ | ||
[2]: https://github.com/dominictarr/hyperscript | ||
virtual-dom is heavily inspired by the inner workings of React by facebook. This project originated as a gist of ideas, which [we have linked to provide some background context](https://gist.github.com/Raynos/8414846). |
@@ -12,21 +12,21 @@ var test = require("tape") | ||
var leftNode = h("div", [ | ||
h("div", { key: 1 }), | ||
h("div", { key: 2 }), | ||
h("div", { key: 3 }), | ||
h("div", { key: 4 }), | ||
h("div", { key: "test" }), | ||
h("div", { key: 6 }), | ||
h("div", { key: "good" }), | ||
h("div", { key: "7" }) | ||
h("div", { key: 1 }, "1"), | ||
h("div", { key: 2 }, "2"), | ||
h("div", { key: 3 }, "3"), | ||
h("div", { key: 4 }, "4"), | ||
h("div", { key: "test" }, "test"), | ||
h("div", { key: 6 }, "6"), | ||
h("div", { key: "good" }, "good"), | ||
h("div", { key: "7" }, "7") | ||
]) | ||
var rightNode = h("div", [ | ||
h("div", { key: "7" }), | ||
h("div", { key: 4 }), | ||
h("div", { key: 3 }), | ||
h("div", { key: 2 }), | ||
h("div", { key: 6 }), | ||
h("div", { key: "test" }), | ||
h("div", { key: "good" }), | ||
h("div", { key: 1 }) | ||
h("div", { key: "7" }, "7"), | ||
h("div", { key: 4 }, "4"), | ||
h("div", { key: 3 }, "3"), | ||
h("div", { key: 2 }, "2"), | ||
h("div", { key: 6 }, "6"), | ||
h("div", { key: "test" }, "test"), | ||
h("div", { key: "good" }, "good"), | ||
h("div", { key: 1 }, "1") | ||
]) | ||
@@ -105,3 +105,30 @@ | ||
test("avoid unnecessary reordering", function (assert) { | ||
var leftNode = h("div", [ | ||
h("div"), | ||
h("div", { key: 1 }), | ||
h("div") | ||
]) | ||
var rightNode = h("div", [ | ||
h("div"), | ||
h("div", { key: 1 }), | ||
h("div") | ||
]) | ||
var rootNode = render(leftNode) | ||
var childNodes = childNodesArray(rootNode) | ||
var patches = diff(leftNode, rightNode) | ||
assert.equal(patchCount(patches), 0) | ||
var newRoot = patch(rootNode, patches) | ||
assert.equal(newRoot, rootNode) | ||
assert.equal(newRoot.childNodes[0], childNodes[0]) | ||
assert.equal(newRoot.childNodes[1], childNodes[1]) | ||
assert.equal(newRoot.childNodes[2], childNodes[2]) | ||
assert.end() | ||
}) | ||
test("missing key gets replaced", function (assert) { | ||
@@ -108,0 +135,0 @@ var leftNode = h("div", [ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1730
67366
6
122
+ Addedvdom@0.0.6(transitive)
+ Addedvtree@0.0.5(transitive)
- Removedvdom@0.0.5(transitive)
- Removedvtree@0.0.4(transitive)
Updatedvdom@0.0.6
Updatedvtree@0.0.5