|
| 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 |
0 commit comments