├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs ├── index.js └── package.json ├── esm └── index.js ├── index.js ├── min.js ├── new.js ├── package.json ├── rollup ├── babel.config.js └── new.config.js ├── test ├── dommy.js ├── index.html ├── index.js ├── js-diff-benchmark.js ├── select.html ├── single.js ├── test.js ├── utils.js └── verify.js └── udomdiff-head.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | udomdiff-head.jpg 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µdomdiff 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/udomdiff.svg?branch=master)](https://travis-ci.com/WebReflection/udomdiff) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/udomdiff/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/udomdiff?branch=master) 4 | 5 | ![a different tree](./udomdiff-head.jpg) 6 | 7 | **Social Media Photo by [Christopher Rusev](https://unsplash.com/@ralics) on [Unsplash](https://unsplash.com/)** 8 | 9 | An essential diffing algorithm for [µhtml](https://github.com/WebReflection/uhtml#readme). 10 | 11 | 12 | ### Signature 13 | 14 | ```js 15 | futureNodes = udomdiff( 16 | parentNode, // where changes happen 17 | [...currentNodes], // Array of current items/nodes 18 | [...futureNodes], // Array of future items/nodes (returned) 19 | get(node, toDoWhat), // a callback to retrieve the node 20 | before // the anchored node to insertBefore 21 | ); 22 | ``` 23 | 24 | ### What is `get` and how does it work? 25 | 26 | You can find all info from [domdiff](https://github.com/WebReflection/domdiff#a-node-generic-info--node-callback-for-complex-data), as it's exactly the same concept: 27 | 28 | * `get(node, 1)` to retrieve the node that's being appended 29 | * `get(node, 0)` to get the node to use for an `insertBefore` operation 30 | * `get(node, -0)` to get the node to use for an `insertAfter` operation 31 | * `get(node, -1)` to retrieve the node that's being removed 32 | 33 | If you don't care about any of those second arguments values, `const get = o => o;` is a valid get too. 34 | 35 | 36 | ### How to import it: 37 | 38 | * via **CDN**, as global variable: `https://unpkg.com/udomdiff` 39 | * via **ESM**, as external module: `import udomdiff from 'https://unpkg.com/udomdiff/esm/index.js'` 40 | * via **CJS**: `const udomdiff = require('udomdiff');` ( or `require('udomdiff/cjs')` ) 41 | * via bundlers/transpilers: `import udomdiff from 'udomdiff';` ( or `from 'udomdiff/esm'` ) 42 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * ISC License 4 | * 5 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 13 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 15 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 16 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | * PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | /** 21 | * @param {Node} parentNode The container where children live 22 | * @param {Node[]} a The list of current/live children 23 | * @param {Node[]} b The list of future children 24 | * @param {(entry: Node, action: number) => Node} get 25 | * The callback invoked per each entry related DOM operation. 26 | * @param {Node} [before] The optional node used as anchor to insert before. 27 | * @returns {Node[]} The same list of future children. 28 | */ 29 | module.exports = (parentNode, a, b, get, before) => { 30 | const bLength = b.length; 31 | let aEnd = a.length; 32 | let bEnd = bLength; 33 | let aStart = 0; 34 | let bStart = 0; 35 | let map = null; 36 | while (aStart < aEnd || bStart < bEnd) { 37 | // append head, tail, or nodes in between: fast path 38 | if (aEnd === aStart) { 39 | // we could be in a situation where the rest of nodes that 40 | // need to be added are not at the end, and in such case 41 | // the node to `insertBefore`, if the index is more than 0 42 | // must be retrieved, otherwise it's gonna be the first item. 43 | const node = bEnd < bLength ? 44 | (bStart ? 45 | (get(b[bStart - 1], -0).nextSibling) : 46 | get(b[bEnd], 0)) : 47 | before; 48 | while (bStart < bEnd) 49 | parentNode.insertBefore(get(b[bStart++], 1), node); 50 | } 51 | // remove head or tail: fast path 52 | else if (bEnd === bStart) { 53 | while (aStart < aEnd) { 54 | // remove the node only if it's unknown or not live 55 | if (!map || !map.has(a[aStart])) 56 | parentNode.removeChild(get(a[aStart], -1)); 57 | aStart++; 58 | } 59 | } 60 | // same node: fast path 61 | else if (a[aStart] === b[bStart]) { 62 | aStart++; 63 | bStart++; 64 | } 65 | // same tail: fast path 66 | else if (a[aEnd - 1] === b[bEnd - 1]) { 67 | aEnd--; 68 | bEnd--; 69 | } 70 | // The once here single last swap "fast path" has been removed in v1.1.0 71 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 72 | // reverse swap: also fast path 73 | else if ( 74 | a[aStart] === b[bEnd - 1] && 75 | b[bStart] === a[aEnd - 1] 76 | ) { 77 | // this is a "shrink" operation that could happen in these cases: 78 | // [1, 2, 3, 4, 5] 79 | // [1, 4, 3, 2, 5] 80 | // or asymmetric too 81 | // [1, 2, 3, 4, 5] 82 | // [1, 2, 3, 5, 6, 4] 83 | const node = get(a[--aEnd], -0).nextSibling; 84 | parentNode.insertBefore( 85 | get(b[bStart++], 1), 86 | get(a[aStart++], -0).nextSibling 87 | ); 88 | parentNode.insertBefore(get(b[--bEnd], 1), node); 89 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 90 | // The main reason to do this, is that when a[aEnd] will be reached, 91 | // the loop will likely be on the fast path, as identical to b[bEnd]. 92 | // In the best case scenario, the next loop will skip the tail, 93 | // but in the worst one, this node will be considered as already 94 | // processed, bailing out pretty quickly from the map index check 95 | a[aEnd] = b[bEnd]; 96 | } 97 | // map based fallback, "slow" path 98 | else { 99 | // the map requires an O(bEnd - bStart) operation once 100 | // to store all future nodes indexes for later purposes. 101 | // In the worst case scenario, this is a full O(N) cost, 102 | // and such scenario happens at least when all nodes are different, 103 | // but also if both first and last items of the lists are different 104 | if (!map) { 105 | map = new Map; 106 | let i = bStart; 107 | while (i < bEnd) 108 | map.set(b[i], i++); 109 | } 110 | // if it's a future node, hence it needs some handling 111 | if (map.has(a[aStart])) { 112 | // grab the index of such node, 'cause it might have been processed 113 | const index = map.get(a[aStart]); 114 | // if it's not already processed, look on demand for the next LCS 115 | if (bStart < index && index < bEnd) { 116 | let i = aStart; 117 | // counts the amount of nodes that are the same in the future 118 | let sequence = 1; 119 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 120 | sequence++; 121 | // effort decision here: if the sequence is longer than replaces 122 | // needed to reach such sequence, which would brings again this loop 123 | // to the fast path, prepend the difference before a sequence, 124 | // and move only the future list index forward, so that aStart 125 | // and bStart will be aligned again, hence on the fast path. 126 | // An example considering aStart and bStart are both 0: 127 | // a: [1, 2, 3, 4] 128 | // b: [7, 1, 2, 3, 6] 129 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 130 | // will be processed at zero cost 131 | if (sequence > (index - bStart)) { 132 | const node = get(a[aStart], 0); 133 | while (bStart < index) 134 | parentNode.insertBefore(get(b[bStart++], 1), node); 135 | } 136 | // if the effort wasn't good enough, fallback to a replace, 137 | // moving both source and target indexes forward, hoping that some 138 | // similar node will be found later on, to go back to the fast path 139 | else { 140 | parentNode.replaceChild( 141 | get(b[bStart++], 1), 142 | get(a[aStart++], -1) 143 | ); 144 | } 145 | } 146 | // otherwise move the source forward, 'cause there's nothing to do 147 | else 148 | aStart++; 149 | } 150 | // this node has no meaning in the future list, so it's more than safe 151 | // to remove it, and check the next live node out instead, meaning 152 | // that only the live list index should be forwarded 153 | else 154 | parentNode.removeChild(get(a[aStart++], -1)); 155 | } 156 | } 157 | return b; 158 | }; 159 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ISC License 3 | * 4 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 15 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | /** 20 | * @param {Node} parentNode The container where children live 21 | * @param {Node[]} a The list of current/live children 22 | * @param {Node[]} b The list of future children 23 | * @param {(entry: Node, action: number) => Node} get 24 | * The callback invoked per each entry related DOM operation. 25 | * @param {Node} [before] The optional node used as anchor to insert before. 26 | * @returns {Node[]} The same list of future children. 27 | */ 28 | export default (parentNode, a, b, get, before) => { 29 | const bLength = b.length; 30 | let aEnd = a.length; 31 | let bEnd = bLength; 32 | let aStart = 0; 33 | let bStart = 0; 34 | let map = null; 35 | while (aStart < aEnd || bStart < bEnd) { 36 | // append head, tail, or nodes in between: fast path 37 | if (aEnd === aStart) { 38 | // we could be in a situation where the rest of nodes that 39 | // need to be added are not at the end, and in such case 40 | // the node to `insertBefore`, if the index is more than 0 41 | // must be retrieved, otherwise it's gonna be the first item. 42 | const node = bEnd < bLength ? 43 | (bStart ? 44 | (get(b[bStart - 1], -0).nextSibling) : 45 | get(b[bEnd], 0)) : 46 | before; 47 | while (bStart < bEnd) 48 | parentNode.insertBefore(get(b[bStart++], 1), node); 49 | } 50 | // remove head or tail: fast path 51 | else if (bEnd === bStart) { 52 | while (aStart < aEnd) { 53 | // remove the node only if it's unknown or not live 54 | if (!map || !map.has(a[aStart])) 55 | parentNode.removeChild(get(a[aStart], -1)); 56 | aStart++; 57 | } 58 | } 59 | // same node: fast path 60 | else if (a[aStart] === b[bStart]) { 61 | aStart++; 62 | bStart++; 63 | } 64 | // same tail: fast path 65 | else if (a[aEnd - 1] === b[bEnd - 1]) { 66 | aEnd--; 67 | bEnd--; 68 | } 69 | // The once here single last swap "fast path" has been removed in v1.1.0 70 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 71 | // reverse swap: also fast path 72 | else if ( 73 | a[aStart] === b[bEnd - 1] && 74 | b[bStart] === a[aEnd - 1] 75 | ) { 76 | // this is a "shrink" operation that could happen in these cases: 77 | // [1, 2, 3, 4, 5] 78 | // [1, 4, 3, 2, 5] 79 | // or asymmetric too 80 | // [1, 2, 3, 4, 5] 81 | // [1, 2, 3, 5, 6, 4] 82 | const node = get(a[--aEnd], -0).nextSibling; 83 | parentNode.insertBefore( 84 | get(b[bStart++], 1), 85 | get(a[aStart++], -0).nextSibling 86 | ); 87 | parentNode.insertBefore(get(b[--bEnd], 1), node); 88 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 89 | // The main reason to do this, is that when a[aEnd] will be reached, 90 | // the loop will likely be on the fast path, as identical to b[bEnd]. 91 | // In the best case scenario, the next loop will skip the tail, 92 | // but in the worst one, this node will be considered as already 93 | // processed, bailing out pretty quickly from the map index check 94 | a[aEnd] = b[bEnd]; 95 | } 96 | // map based fallback, "slow" path 97 | else { 98 | // the map requires an O(bEnd - bStart) operation once 99 | // to store all future nodes indexes for later purposes. 100 | // In the worst case scenario, this is a full O(N) cost, 101 | // and such scenario happens at least when all nodes are different, 102 | // but also if both first and last items of the lists are different 103 | if (!map) { 104 | map = new Map; 105 | let i = bStart; 106 | while (i < bEnd) 107 | map.set(b[i], i++); 108 | } 109 | // if it's a future node, hence it needs some handling 110 | if (map.has(a[aStart])) { 111 | // grab the index of such node, 'cause it might have been processed 112 | const index = map.get(a[aStart]); 113 | // if it's not already processed, look on demand for the next LCS 114 | if (bStart < index && index < bEnd) { 115 | let i = aStart; 116 | // counts the amount of nodes that are the same in the future 117 | let sequence = 1; 118 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 119 | sequence++; 120 | // effort decision here: if the sequence is longer than replaces 121 | // needed to reach such sequence, which would brings again this loop 122 | // to the fast path, prepend the difference before a sequence, 123 | // and move only the future list index forward, so that aStart 124 | // and bStart will be aligned again, hence on the fast path. 125 | // An example considering aStart and bStart are both 0: 126 | // a: [1, 2, 3, 4] 127 | // b: [7, 1, 2, 3, 6] 128 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 129 | // will be processed at zero cost 130 | if (sequence > (index - bStart)) { 131 | const node = get(a[aStart], 0); 132 | while (bStart < index) 133 | parentNode.insertBefore(get(b[bStart++], 1), node); 134 | } 135 | // if the effort wasn't good enough, fallback to a replace, 136 | // moving both source and target indexes forward, hoping that some 137 | // similar node will be found later on, to go back to the fast path 138 | else { 139 | parentNode.replaceChild( 140 | get(b[bStart++], 1), 141 | get(a[aStart++], -1) 142 | ); 143 | } 144 | } 145 | // otherwise move the source forward, 'cause there's nothing to do 146 | else 147 | aStart++; 148 | } 149 | // this node has no meaning in the future list, so it's more than safe 150 | // to remove it, and check the next live node out instead, meaning 151 | // that only the live list index should be forwarded 152 | else 153 | parentNode.removeChild(get(a[aStart++], -1)); 154 | } 155 | } 156 | return b; 157 | }; 158 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var udomdiff = (function (exports) { 2 | 'use strict'; 3 | 4 | /** 5 | * ISC License 6 | * 7 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 8 | * 9 | * Permission to use, copy, modify, and/or distribute this software for any 10 | * purpose with or without fee is hereby granted, provided that the above 11 | * copyright notice and this permission notice appear in all copies. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 15 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 18 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | * PERFORMANCE OF THIS SOFTWARE. 20 | */ 21 | 22 | /** 23 | * @param {Node} parentNode The container where children live 24 | * @param {Node[]} a The list of current/live children 25 | * @param {Node[]} b The list of future children 26 | * @param {(entry: Node, action: number) => Node} get 27 | * The callback invoked per each entry related DOM operation. 28 | * @param {Node} [before] The optional node used as anchor to insert before. 29 | * @returns {Node[]} The same list of future children. 30 | */ 31 | var index = (function (parentNode, a, b, get, before) { 32 | var bLength = b.length; 33 | var aEnd = a.length; 34 | var bEnd = bLength; 35 | var aStart = 0; 36 | var bStart = 0; 37 | var map = null; 38 | while (aStart < aEnd || bStart < bEnd) { 39 | // append head, tail, or nodes in between: fast path 40 | if (aEnd === aStart) { 41 | // we could be in a situation where the rest of nodes that 42 | // need to be added are not at the end, and in such case 43 | // the node to `insertBefore`, if the index is more than 0 44 | // must be retrieved, otherwise it's gonna be the first item. 45 | var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd], 0) : before; 46 | while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node); 47 | } 48 | // remove head or tail: fast path 49 | else if (bEnd === bStart) { 50 | while (aStart < aEnd) { 51 | // remove the node only if it's unknown or not live 52 | if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1)); 53 | aStart++; 54 | } 55 | } 56 | // same node: fast path 57 | else if (a[aStart] === b[bStart]) { 58 | aStart++; 59 | bStart++; 60 | } 61 | // same tail: fast path 62 | else if (a[aEnd - 1] === b[bEnd - 1]) { 63 | aEnd--; 64 | bEnd--; 65 | } 66 | // The once here single last swap "fast path" has been removed in v1.1.0 67 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 68 | // reverse swap: also fast path 69 | else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { 70 | // this is a "shrink" operation that could happen in these cases: 71 | // [1, 2, 3, 4, 5] 72 | // [1, 4, 3, 2, 5] 73 | // or asymmetric too 74 | // [1, 2, 3, 4, 5] 75 | // [1, 2, 3, 5, 6, 4] 76 | var _node = get(a[--aEnd], -0).nextSibling; 77 | parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -0).nextSibling); 78 | parentNode.insertBefore(get(b[--bEnd], 1), _node); 79 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 80 | // The main reason to do this, is that when a[aEnd] will be reached, 81 | // the loop will likely be on the fast path, as identical to b[bEnd]. 82 | // In the best case scenario, the next loop will skip the tail, 83 | // but in the worst one, this node will be considered as already 84 | // processed, bailing out pretty quickly from the map index check 85 | a[aEnd] = b[bEnd]; 86 | } 87 | // map based fallback, "slow" path 88 | else { 89 | // the map requires an O(bEnd - bStart) operation once 90 | // to store all future nodes indexes for later purposes. 91 | // In the worst case scenario, this is a full O(N) cost, 92 | // and such scenario happens at least when all nodes are different, 93 | // but also if both first and last items of the lists are different 94 | if (!map) { 95 | map = new Map(); 96 | var i = bStart; 97 | while (i < bEnd) map.set(b[i], i++); 98 | } 99 | // if it's a future node, hence it needs some handling 100 | if (map.has(a[aStart])) { 101 | // grab the index of such node, 'cause it might have been processed 102 | var index = map.get(a[aStart]); 103 | // if it's not already processed, look on demand for the next LCS 104 | if (bStart < index && index < bEnd) { 105 | var _i = aStart; 106 | // counts the amount of nodes that are the same in the future 107 | var sequence = 1; 108 | while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) sequence++; 109 | // effort decision here: if the sequence is longer than replaces 110 | // needed to reach such sequence, which would brings again this loop 111 | // to the fast path, prepend the difference before a sequence, 112 | // and move only the future list index forward, so that aStart 113 | // and bStart will be aligned again, hence on the fast path. 114 | // An example considering aStart and bStart are both 0: 115 | // a: [1, 2, 3, 4] 116 | // b: [7, 1, 2, 3, 6] 117 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 118 | // will be processed at zero cost 119 | if (sequence > index - bStart) { 120 | var _node2 = get(a[aStart], 0); 121 | while (bStart < index) parentNode.insertBefore(get(b[bStart++], 1), _node2); 122 | } 123 | // if the effort wasn't good enough, fallback to a replace, 124 | // moving both source and target indexes forward, hoping that some 125 | // similar node will be found later on, to go back to the fast path 126 | else { 127 | parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1)); 128 | } 129 | } 130 | // otherwise move the source forward, 'cause there's nothing to do 131 | else aStart++; 132 | } 133 | // this node has no meaning in the future list, so it's more than safe 134 | // to remove it, and check the next live node out instead, meaning 135 | // that only the live list index should be forwarded 136 | else parentNode.removeChild(get(a[aStart++], -1)); 137 | } 138 | } 139 | return b; 140 | }); 141 | 142 | exports["default"] = index; 143 | 144 | Object.defineProperty(exports, '__esModule', { value: true }); 145 | 146 | return exports; 147 | 148 | })({}).default; 149 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | var udomdiff=(e=>(e.default=function(e,r,i,f,l){for(var n=i.length,t=r.length,o=n,s=0,a=0,v=null;s{const f=i.length;let n=t.length,s=f,o=0,c=0,u=null;for(;or-c){const f=l(t[o],0);for(;c { 20 | const {parentNode} = node; 21 | node.parentNode = null; 22 | if (parentNode) { 23 | const {childNodes} = parentNode; 24 | const i = childNodes.indexOf(node); 25 | if (-1 < i) 26 | childNodes.splice(i, 1); 27 | } 28 | }; 29 | 30 | class Siblings { 31 | get nextSibling() { 32 | const {parentNode} = this; 33 | if (parentNode) { 34 | const {childNodes} = parentNode; 35 | const i = childNodes.indexOf(this) + 1; 36 | if (0 < i && i < childNodes.length) 37 | return childNodes[i]; 38 | } 39 | return null; 40 | } 41 | get previousSibling() { 42 | const {parentNode} = this; 43 | if (parentNode) { 44 | const {childNodes} = parentNode; 45 | const i = childNodes.indexOf(this) - 1; 46 | if (-1 < i) 47 | return childNodes[i]; 48 | } 49 | return null; 50 | } 51 | } 52 | 53 | class Nody extends Siblings { 54 | constructor(textContent) { 55 | super(); 56 | this.parentNode = null; 57 | this.textContent = textContent; 58 | } 59 | } 60 | 61 | class Dommy extends Siblings { 62 | constructor(tagName) { 63 | super(); 64 | this.parentNode = null; 65 | this.childNodes = []; 66 | this.tagName = tagName; 67 | } 68 | get firstChild() { 69 | return this.childNodes[0]; 70 | } 71 | get lastChild() { 72 | return this.childNodes[this.childNodes.length - 1]; 73 | } 74 | get textContent() { 75 | return this.childNodes.map(node => node.textContent).join(''); 76 | } 77 | set textContent(value) { 78 | this.childNodes.splice(0).forEach(remove); 79 | if (value) 80 | this.appendChild(document.createTextNode(value)); 81 | } 82 | appendChild(newNode) { 83 | if (!newNode) 84 | throw new Error('invalid appendChild'); 85 | remove(newNode); 86 | this.childNodes.push(newNode); 87 | newNode.parentNode = this; 88 | return newNode; 89 | } 90 | insertBefore(newNode, oldNode) { 91 | if (newNode !== oldNode) { 92 | remove(newNode); 93 | const {childNodes} = this; 94 | if (oldNode) { 95 | const i = childNodes.indexOf(oldNode); 96 | if (i < 0) 97 | throw new Error('invalid insertBefore'); 98 | childNodes.splice(i, 0, newNode); 99 | } 100 | else 101 | childNodes.push(newNode); 102 | newNode.parentNode = this; 103 | } 104 | return newNode; 105 | } 106 | removeChild(oldNode) { 107 | const {childNodes} = this; 108 | const i = childNodes.indexOf(oldNode); 109 | if (i < 0) 110 | throw new Error('invalid removeChild'); 111 | childNodes.splice(i, 1); 112 | oldNode.parentNode = null; 113 | return oldNode; 114 | } 115 | replaceChild(newNode, oldNode) { 116 | remove(newNode); 117 | const {childNodes} = this; 118 | const i = childNodes.indexOf(oldNode); 119 | if (i < 0) 120 | throw new Error('invalid replaceChild'); 121 | childNodes[i] = newNode; 122 | oldNode.parentNode = null; 123 | newNode.parentNode = this; 124 | return newNode; 125 | } 126 | } 127 | 128 | module.exports = { 129 | createElement: tagName => new Dommy(tagName), 130 | createTextNode: textContent => new Nody(textContent) 131 | }; 132 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | global.document = { 2 | createElement: function (tagName) { 3 | return {tagName: tagName, value: '\n'}; 4 | }, 5 | createTextNode: function (value) { 6 | return Object.defineProperty( 7 | {value: value}, 8 | 'nextSibling', 9 | {get: function () { 10 | var cn = document.body.childNodes; 11 | return cn[cn.indexOf(this) + 1]; 12 | }} 13 | ); 14 | }, 15 | importNode: function () {}, 16 | body: { 17 | get lastElementChild() { 18 | return this.childNodes[this.childNodes.length - 1]; 19 | }, 20 | get textContent() { 21 | return this.childNodes.map(node => node.value).join(''); 22 | }, 23 | appendChild: function (node) { 24 | this.removeChild(node); 25 | this.childNodes.push(node); 26 | node.parentNode = this; 27 | }, 28 | childNodes: [], 29 | insertBefore: function (before, after) { 30 | if (before !== after) { 31 | this.removeChild(before); 32 | var cn = this.childNodes; 33 | cn.splice(cn.indexOf(after), 0, before); 34 | before.parentNode = this; 35 | } 36 | }, 37 | removeChild: function (child) { 38 | delete child.parentNode; 39 | var cn = this.childNodes; 40 | var i = cn.indexOf(child); 41 | if (-1 < i) 42 | cn.splice(i, 1); 43 | }, 44 | replaceChild: function (newChild, oldChild) { 45 | this.removeChild(newChild); 46 | var cn = this.childNodes; 47 | cn.splice(cn.indexOf(oldChild), 1, newChild); 48 | newChild.parentNode = this; 49 | delete oldChild.parentNode; 50 | } 51 | } 52 | }; 53 | 54 | global.udomdiff = require('../cjs'); 55 | require('./test.js'); 56 | global.test(); 57 | -------------------------------------------------------------------------------- /test/js-diff-benchmark.js: -------------------------------------------------------------------------------- 1 | // source: https://github.com/luwes/js-diff-benchmark 2 | const fs = require('fs'); 3 | const c = require('ansi-colors'); 4 | var Terser = require('terser'); 5 | const gzipSize = require('gzip-size'); 6 | const Table = require('cli-table'); 7 | const microtime = require('microtime'); 8 | const document = require('./dommy.js'); 9 | const get = o => o; 10 | 11 | const libs = [ 12 | 'udomdiff w/out before', 13 | 'udomdiff with before' 14 | ]; 15 | 16 | const cols = [ 17 | '', 18 | '1k', 19 | 'Repl', 20 | 'Shufle', 21 | 'Invers', 22 | 'Clear', 23 | 'Append', 24 | 'Prepend', 25 | 'Swap2', 26 | 'Up10th', 27 | '10k', 28 | 'Swap2', 29 | 'Total', 30 | 'Size', 31 | ]; 32 | 33 | const table = new Table({ 34 | head: cols, 35 | colAligns: cols.map(() => 'middle'), 36 | style: { 37 | head: ['green'], 38 | }, 39 | }); 40 | 41 | let shuffleSeed; 42 | 43 | // in case we'd like to test "pinnability" of the differ 44 | let before;// = document.createTextNode(''); 45 | 46 | const parent = document.createElement('div'); 47 | 48 | const { 49 | clear, reset, verifyNodes, 50 | random, reverse, 51 | create1000, create10000, 52 | append1000, prepend1000, 53 | swapRows, updateEach10thRow 54 | } = require('./utils.js')(document, parent, () => before); 55 | 56 | libs.forEach((lib, i) => { 57 | if (i) 58 | before = document.createTextNode(''); 59 | 60 | const libResults = []; 61 | table.push({ [lib]: libResults }); 62 | 63 | const file = `../cjs/index.js`; 64 | const diff = require(file); 65 | 66 | var code = fs.readFileSync(require.resolve(file), 'utf8'); 67 | var gzip = gzipSize.sync(Terser.minify(code).code); 68 | 69 | // clean up the parent 70 | // clean up the parent 71 | parent.textContent = ''; 72 | if (before) 73 | parent.appendChild(before); 74 | 75 | //* warm up + checking everything works upfront 76 | let childNodes = create1000(diff, []); 77 | console.assert( 78 | verifyNodes(childNodes, 1000), 79 | '%s warmup create', 80 | lib 81 | ); 82 | 83 | childNodes = create1000(diff, childNodes); 84 | console.assert( 85 | verifyNodes(childNodes, 1000), 86 | '%s warmup replace', 87 | lib 88 | ); 89 | 90 | if (!shuffleSeed) { 91 | // create a fixed shuffled seed so each library does the same. 92 | const shuffle = childNodes.slice().sort( 93 | () => Math.random() - Math.random() 94 | ); 95 | shuffleSeed = shuffle.map((node) => childNodes.indexOf(node)); 96 | } 97 | 98 | childNodes = append1000(diff, childNodes); 99 | console.assert( 100 | verifyNodes(childNodes, 2000), 101 | '%s warmup append', 102 | lib 103 | ); 104 | childNodes = prepend1000(diff, childNodes); 105 | console.assert( 106 | verifyNodes(childNodes, 3000), 107 | '%s warmup prepend', 108 | lib 109 | ); 110 | childNodes = clear(diff, childNodes); 111 | console.assert( 112 | verifyNodes(childNodes, 0), 113 | '%s warmup clear', 114 | lib 115 | ); 116 | childNodes = create10000(diff, childNodes); 117 | console.assert( 118 | verifyNodes(childNodes, 10000), 119 | '%s warmup 10k', 120 | lib 121 | ); 122 | childNodes = clear(diff, childNodes); 123 | console.assert( 124 | verifyNodes(childNodes, 0), 125 | '%s warmup clear 10k', 126 | lib 127 | ); 128 | childNodes = create1000(diff, childNodes); 129 | childNodes = swapRows(diff, childNodes); 130 | console.assert(childNodes[1].textContent == 998, '%s warmup swap', lib); 131 | console.assert(childNodes[998].textContent == 1, '%s warmup swap', lib); 132 | childNodes = clear(diff, childNodes); 133 | childNodes = create1000(diff, childNodes); 134 | childNodes = updateEach10thRow(diff, childNodes); 135 | console.assert( 136 | /!$/.test(childNodes[0].textContent), 137 | '%s warmup update', 138 | lib 139 | ); 140 | console.assert( 141 | !/!$/.test(childNodes[1].textContent), 142 | '%s warmup update', 143 | lib 144 | ); 145 | console.assert( 146 | /!$/.test(childNodes[10].textContent), 147 | '%s warmup update', 148 | lib 149 | ); 150 | childNodes = clear(diff, childNodes); 151 | console.assert( 152 | verifyNodes(childNodes, 0), 153 | '%s warmup clear', 154 | lib 155 | ); 156 | //*/ 157 | 158 | // console.time(lib.toUpperCase()); 159 | 160 | const totalStart = microtime.now(); 161 | 162 | let begin; 163 | const start = () => { 164 | reset(); 165 | begin = microtime.now(); 166 | }; 167 | const stop = (count, operationMax) => { 168 | const end = microtime.now() - begin; 169 | const delta = count - operationMax; 170 | libResults.push(`${(end / 1000).toPrecision(2)}ms 171 | ${c.gray(count)}${ 172 | count > operationMax 173 | ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`) 174 | : '' 175 | }`.replace(/^\s+/m, '')); 176 | }; 177 | 178 | // actual benchmark 179 | 180 | start(); 181 | childNodes = create1000(diff, childNodes); 182 | stop(parent.count(), 1000); 183 | console.assert( 184 | verifyNodes(childNodes, 1000), 185 | '%s 1k', 186 | lib 187 | ); 188 | 189 | start(); 190 | childNodes = create1000(diff, childNodes); 191 | stop(parent.count(), 2000); 192 | console.assert( 193 | verifyNodes(childNodes, 1000), 194 | '%s replace', 195 | lib 196 | ); 197 | 198 | start(); 199 | childNodes = random(shuffleSeed, diff, childNodes); 200 | stop(parent.count(), 2000); 201 | console.assert( 202 | verifyNodes(childNodes, 1000), 203 | '%s random', 204 | lib 205 | ); 206 | 207 | start(); 208 | childNodes = reverse(diff, childNodes); 209 | stop(parent.count(), 2000); 210 | console.assert( 211 | verifyNodes(childNodes, 1000), 212 | '%s reverse', 213 | lib 214 | ); 215 | 216 | start(); 217 | childNodes = clear(diff, childNodes); 218 | stop(parent.count(), 1000); 219 | console.assert( 220 | verifyNodes(childNodes, 0), 221 | '%s clear', 222 | lib 223 | ); 224 | 225 | childNodes = create1000(diff, childNodes); 226 | 227 | start(); 228 | childNodes = append1000(diff, childNodes); 229 | stop(parent.count(), 2000); 230 | console.assert( 231 | verifyNodes(childNodes, 2000), 232 | '%s append 1k', 233 | lib 234 | ); 235 | 236 | start(); 237 | childNodes = prepend1000(diff, childNodes); 238 | stop(parent.count(), 1000); 239 | console.assert( 240 | verifyNodes(childNodes, 3000), 241 | '%s prepend 1k', 242 | lib 243 | ); 244 | 245 | childNodes = clear(diff, childNodes); 246 | childNodes = create1000(diff, childNodes); 247 | 248 | start(); 249 | childNodes = swapRows(diff, childNodes); 250 | stop(parent.count(), 4); 251 | console.assert( 252 | parent.childNodes[1].textContent == 998 && 253 | parent.childNodes[998].textContent == 1 && 254 | verifyNodes(childNodes, 1000), 255 | '%s swap2 1k', 256 | lib 257 | ); 258 | 259 | start(); 260 | childNodes = updateEach10thRow(diff, childNodes); 261 | stop(parent.count(), 200); 262 | console.assert( 263 | verifyNodes(childNodes, 1000), 264 | '%s update 10th', 265 | lib 266 | ); 267 | 268 | childNodes = clear(diff, childNodes); 269 | 270 | start(); 271 | childNodes = create10000(diff, childNodes); 272 | stop(parent.count(), 10000); 273 | console.assert( 274 | verifyNodes(childNodes, 10000), 275 | '%s 10k', 276 | lib 277 | ); 278 | 279 | start(); 280 | childNodes = swapRows(diff, childNodes); 281 | stop(parent.count(), 4); 282 | console.assert( 283 | parent.childNodes[1].textContent == 9998 && 284 | parent.childNodes[9998].textContent == 1 && 285 | verifyNodes(childNodes, 10000), 286 | '%s swap2 10k', 287 | lib 288 | ); 289 | 290 | childNodes = clear(diff, childNodes); 291 | reset(); 292 | 293 | //*/ 294 | 295 | libResults.push(`${((microtime.now() - totalStart) / 1000).toPrecision(3)}ms`); 296 | libResults.push(`${gzip}B`); 297 | 298 | // const used = process.memoryUsage().heapUsed / 1024 / 1024; 299 | // console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`); 300 | 301 | try { 302 | if (global.gc) { 303 | global.gc(); 304 | } 305 | } catch (e) { 306 | process.exit(); 307 | } 308 | }); 309 | 310 | table.sort((a, b) => { 311 | a = Object.values(a)[0]; 312 | b = Object.values(b)[0]; 313 | return parseInt(a[a.length - 2]) - parseInt(b[b.length - 2]); 314 | }); 315 | 316 | console.log(table.toString()); 317 | -------------------------------------------------------------------------------- /test/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 44 | 45 | 46 |
47 |
48 |

html

49 | 55 | 56 | -------------------------------------------------------------------------------- /test/single.js: -------------------------------------------------------------------------------- 1 | const udomdiff = require('../cjs'); 2 | 3 | const {Dommy, Nody, get} = require('./utils.js'); 4 | 5 | let parent = new Dommy(); 6 | 7 | parent.childNodes.push(new Nody('before')); 8 | 9 | for (let i = 0; i < 10; i++) 10 | udomdiff( 11 | parent, 12 | parent.childNodes, 13 | [new Nody('after')], 14 | get, 15 | parent.lastElementChild 16 | ); 17 | 18 | console.time('single'); 19 | udomdiff( 20 | parent, 21 | parent.childNodes, 22 | [new Nody('after')], 23 | get, 24 | parent.lastElementChild 25 | ); 26 | console.timeEnd('single'); 27 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | function test() { 2 | var before = document.body.appendChild( 3 | document.createElement('hr') 4 | ); 5 | var nodes = { 6 | 'a': document.createTextNode('a'), 7 | 'b': document.createTextNode('b'), 8 | 'c': document.createTextNode('c'), 9 | 'd': document.createTextNode('d'), 10 | 'e': document.createTextNode('e'), 11 | 'f': document.createTextNode('f'), 12 | 'g': document.createTextNode('g'), 13 | 'h': document.createTextNode('h'), 14 | 'i': document.createTextNode('i'), 15 | 'j': document.createTextNode('j'), 16 | 'k': document.createTextNode('k') 17 | }; 18 | var futureState = udomdiff( 19 | document.body, 20 | [], 21 | [nodes.b, nodes.c, nodes.d], 22 | get, 23 | before 24 | ); 25 | diff('bcd'); 26 | diff('bcd'); 27 | diff('abcd'); 28 | diff('dcba'); 29 | diff('abcd'); 30 | diff('abcdef'); 31 | diff('abcghidef'); 32 | diff('abcghide'); 33 | diff('cghide'); 34 | diff('cgde'); 35 | diff(''); 36 | diff('abcdef'); 37 | diff('abgidef'); 38 | diff('abcdef'); 39 | diff('jgabcdefhi'); 40 | diff('abcdef'); 41 | diff('agcdhi'); 42 | diff('igadhc'); 43 | diff('chdagi'); 44 | diff('dfg'); 45 | diff('abcdfg'); 46 | diff('abcdefg'); 47 | diff('gfedcba'); 48 | diff('fdbaeg'); 49 | diff('abcdef'); 50 | diff('abcdefhij'); 51 | diff('abcdehfij'); 52 | diff('abidehfcj'); 53 | diff('abcdefhij'); 54 | diff('abcdefghijk'); 55 | diff('ghi'); 56 | diff('abcd'); 57 | diff('bcad'); 58 | diff('abcde'); 59 | diff('dabcf'); 60 | diff('ade'); 61 | diff('df'); 62 | diff('bdck'); 63 | diff('ckbd'); 64 | diff(''); 65 | diff('abcd'); 66 | diff('abdec'); 67 | diff('abc'); 68 | diff('cab'); 69 | console.assert( 70 | /^hr$/i.test(document.body.lastElementChild.tagName), 71 | '
preserved' 72 | ); 73 | log('%cthousand nodes', 'font-weight:bold;'); 74 | futureState = udomdiff( 75 | document.body, 76 | futureState, 77 | Array(1000).join('.').split('.').map(function (v, i) { 78 | return document.createTextNode('' + i); 79 | }), 80 | get, 81 | before 82 | ); 83 | console.time('reverse'); 84 | futureState = udomdiff( 85 | document.body, 86 | futureState, 87 | futureState.slice().reverse(), 88 | get, 89 | before 90 | ); 91 | console.timeEnd('reverse'); 92 | console.time('random'); 93 | futureState = udomdiff( 94 | document.body, 95 | futureState, 96 | futureState.slice().sort(function () { 97 | return Math.random() < .5 ? 1 : -1; 98 | }), 99 | get, 100 | before 101 | ); 102 | console.timeEnd('random'); 103 | function diff(text) { 104 | log('%c' + (text ? text : '""'), 'font-weight:bold;'); 105 | futureState = udomdiff( 106 | document.body, 107 | futureState, 108 | text.split('').map(content, nodes), 109 | get, 110 | before 111 | ); 112 | compare(text); 113 | } 114 | } 115 | 116 | var log = document.importNode.length === 1 ? 117 | console.log : 118 | function (info) { 119 | console.log(info.slice(2)); 120 | }; 121 | 122 | function content(c) { 123 | return this[c]; 124 | } 125 | 126 | function compare(text) { 127 | const body = document.body.textContent.replace(/\n/g, ''); 128 | console.assert(body === text, 'expected: ' + text + ' but it is ' + body); 129 | } 130 | 131 | function get(node) { 132 | return node; 133 | } 134 | 135 | if (typeof window === 'object') 136 | window.test = test; 137 | else 138 | global.test = test; 139 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const get = o => o; 2 | module.exports = (document, container, before) => { 3 | const mutations = []; 4 | const { 5 | appendChild, 6 | insertBefore, 7 | removeChild, 8 | replaceChild 9 | } = container; 10 | container.count = () => mutations.length; 11 | container.appendChild = function (newNode) { 12 | const {textContent} = newNode; 13 | if (newNode.parentNode) 14 | mutations.push(`append: drop(${textContent})`); 15 | mutations.push(`append: add(${textContent})`); 16 | return appendChild.call(this, newNode); 17 | }; 18 | container.insertBefore = function (newNode, oldNode) { 19 | const {textContent} = newNode; 20 | if (newNode.parentNode) 21 | mutations.push(`insert: drop(${textContent})`); 22 | mutations.push( 23 | oldNode ? 24 | `insert: put(${textContent}) before (${oldNode.textContent})` : 25 | `insert: add(${textContent})` 26 | ); 27 | return insertBefore.call(this, newNode, oldNode); 28 | }; 29 | container.removeChild = function (oldNode) { 30 | mutations.push(`remove: drop(${oldNode.textContent})`); 31 | return removeChild.call(this, oldNode); 32 | }; 33 | container.replaceChild = function (newNode, oldNode) { 34 | const {textContent} = newNode; 35 | mutations.push(`replace: drop(${oldNode.textContent})`); 36 | if (newNode.parentNode) 37 | mutations.push(`replace: drop(${textContent})`); 38 | mutations.push(`replace: put(${textContent})`); 39 | return replaceChild.call(this, newNode, oldNode); 40 | }; 41 | const createNode = text => { 42 | const node = document.createElement('p'); 43 | node.appendChild(document.createTextNode(text)); 44 | return node; 45 | }; 46 | return { 47 | // Benchnmark Utilities 48 | reset() { 49 | mutations.splice(0); 50 | }, 51 | verifyNodes(childNodes, expected) { 52 | return childNodes.length === expected && 53 | childNodes.every((row, i) => row === container.childNodes[i]) && 54 | container.childNodes.length === expected + (before() ? 1 : 0) && 55 | (!before || container.childNodes[expected] === before()); 56 | }, 57 | // Benchnmark Functions 58 | random(shuffleSeed, diff, oldNodes) { 59 | return diff( 60 | container, 61 | oldNodes, 62 | shuffleSeed.map((newIdx) => oldNodes[newIdx]), 63 | get, 64 | before() 65 | ); 66 | }, 67 | reverse(diff, oldNodes) { 68 | return diff(container, oldNodes, oldNodes.slice().reverse(), get, before()); 69 | }, 70 | append1000(diff, oldNodes) { 71 | const start = oldNodes.length; 72 | const childNodes = oldNodes.slice(); 73 | for (let i = 0; i < 1000; i++) 74 | childNodes.push(createNode(start + i)); 75 | return diff(container, oldNodes, childNodes, get, before()); 76 | }, 77 | clear(diff, oldNodes) { 78 | return diff(container, oldNodes, [], get, before()); 79 | }, 80 | create1000(diff, oldNodes) { 81 | const childNodes = []; 82 | for (let i = 0; i < 1000; i++) 83 | childNodes.push(createNode(i)); 84 | return diff(container, oldNodes, childNodes, get, before()); 85 | }, 86 | create10000(diff, oldNodes) { 87 | const childNodes = []; 88 | for (let i = 0; i < 10000; i++) 89 | childNodes.push(createNode(i)); 90 | return diff(container, oldNodes, childNodes, get, before()); 91 | }, 92 | prepend1000(diff, oldNodes) { 93 | const childNodes = []; 94 | for (let i = 0; i < 1000; i++) 95 | childNodes.push(createNode(-i)); 96 | return diff( 97 | container, 98 | oldNodes, 99 | childNodes.reverse().concat(oldNodes), 100 | get, 101 | before() 102 | ); 103 | }, 104 | swapRows(diff, oldNodes) { 105 | const childNodes = oldNodes.slice(); 106 | const $1 = childNodes[1]; 107 | const index = childNodes.length - 2; 108 | childNodes[1] = childNodes[index]; 109 | childNodes[index] = $1; 110 | return diff(container, oldNodes, childNodes, get, before()); 111 | }, 112 | updateEach10thRow(diff, oldNodes) { 113 | const childNodes = oldNodes.slice(); 114 | for (let i = 0; i < childNodes.length; i += 10) 115 | childNodes[i] = createNode(i + '!'); 116 | return diff(container, oldNodes, childNodes, get, before()); 117 | } 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /test/verify.js: -------------------------------------------------------------------------------- 1 | const udomdiff = require('../cjs'); 2 | 3 | const {Dommy, Nody, get} = require('./utils.js'); 4 | 5 | let nodes = []; 6 | const diff = list => { 7 | parent.reset(); 8 | let future = list.map(node); 9 | console.log(`\x1b[1m[${nodes.map(value).join(', ')}]\x1b[0m`); 10 | console.log(`\x1b[1m[${future.map(value).join(', ')}]\x1b[0m`); 11 | nodes = udomdiff(parent, nodes, future, get); 12 | console.log(parent.operations.join('\n')); 13 | console.assert( 14 | nodes.map(value).join(', ') === future.map(value).join(', '), 15 | `[${nodes.map(value).join(', ')}]` 16 | ); 17 | }; 18 | 19 | const cache = Object.create(null); 20 | const node = value => cache[value] || ( 21 | cache[value] = new Nody(parent, value) 22 | ); 23 | const value = node => node.value; 24 | 25 | let parent = new Dommy(); 26 | 27 | // diff([1, 2, 3, 4]); 28 | // diff([1, 2, 4, 5, 3]); 29 | 30 | diff([1, 2, 3]); 31 | diff([3, 1, 2]); 32 | -------------------------------------------------------------------------------- /udomdiff-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/udomdiff/e58db3ad28b72ade55a14452a73331a0db4d0871/udomdiff-head.jpg --------------------------------------------------------------------------------