├── test.html
├── .gitattributes
├── test-register.cjs
├── plan.md
├── deflate.js
├── deflate.obj.js
├── .github
└── workflows
│ └── test.yml
├── package.json
├── README.md
├── .gitignore
├── inflate.js
├── libs
├── list-difference.js
├── snabbdom.js
├── udomdiff.js
└── stage0.js
└── test.js
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/test-register.cjs:
--------------------------------------------------------------------------------
1 | let { JSDOM } = require('jsdom')
2 |
3 | const { window } = new JSDOM(``, {
4 | url: "http://localhost/",
5 | storageQuota: 10000000,
6 | pretendToBeVisual: true,
7 | FetchExternalResources: false,
8 | ProcessExternalResources: false
9 | })
10 |
11 | let props = Object.getOwnPropertyNames(window)
12 |
13 | props.forEach(prop => {
14 | if (prop in global) return
15 | Object.defineProperty(global, prop, {
16 | configurable: true,
17 | get: () => window[prop]
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/plan.md:
--------------------------------------------------------------------------------
1 | # plan
2 |
3 | * [x] Name: swapdom
4 | * merg, merger
5 | * dom, dom-diff
6 | * morph
7 | * apply node template part list
8 | * replace nodes
9 | * swap, domswap, swapdom
10 | * swapd?
11 | + swap dom
12 | + swapped
13 | + reminds spect
14 | + style of hyperf
15 | * swapdom.js?
16 | + snabbdom-like
17 | + dom in name
18 | + more clear meaning, more bound to HTML
19 | + more complete name
20 | - goes against hyperf, like hyperfrag
21 | * domswap.js?
22 | + domdiff like
23 | + too much fanciness is spect org
24 | + first comes DOM (noun), indicating area, then what it does. Like in hyper-f. Less confusion.
25 | - nah, too boring. Also not fan of domdiff association.
26 |
--------------------------------------------------------------------------------
/deflate.js:
--------------------------------------------------------------------------------
1 | // deflate version of differ, ~170b
2 | // NOTE: doesn't support live b
3 | const swap = (parent, a, b, end = null, { remove, insert } = swap) => {
4 | let i = 0, cur, next, bi, bidx = new Set(b)
5 |
6 | while (bi = a[i++]) !bidx.has(bi) ? remove(bi, parent) : cur = cur || bi
7 | cur = cur || end, i = 0
8 |
9 | while (bi = b[i++]) {
10 | next = cur ? cur.nextSibling : end
11 |
12 | // skip
13 | if (cur === bi) cur = next
14 |
15 | else {
16 | // swap 1:1 (saves costly swaps)
17 | if (b[i] === next) cur = next
18 |
19 | // insert
20 | insert(bi, cur, parent)
21 | }
22 | }
23 |
24 | return b
25 | }
26 |
27 | swap.insert = (a, b, parent) => parent.insertBefore(a, b)
28 | swap.remove = (a, parent) => parent.removeChild(a)
29 |
30 | export default swap
31 |
--------------------------------------------------------------------------------
/deflate.obj.js:
--------------------------------------------------------------------------------
1 | // deflate version, but for objects as inputs
2 | const swap = (parent, a, b, end = null, { insert, remove } = swap) => {
3 | let i = 0, cur, bi, next, ins,
4 | bidx = new WeakSet(b),
5 | keys = Object.keys(b)
6 |
7 | // first remove unneeded
8 | for (i in a) bi = a[i], !bidx.has(bi) ? remove(bi, parent) : (cur ||= bi)
9 | cur ||= end
10 |
11 | // then add needed
12 | while (i = keys.shift()) {
13 | bi = b[i], next = cur?.nextSibling || end
14 | if (cur === bi) cur = next
15 | else {
16 | // swap 1:1 (saves costly swaps)
17 | if (b[keys[0]] === next) cur = next
18 |
19 | insert(bi, cur, parent), ins = 1
20 | }
21 | }
22 |
23 | return b
24 | }
25 |
26 | swap.insert = (a, b, parent) => parent.insertBefore(a, b)
27 | swap.remove = (a, parent) => parent.removeChild(a)
28 |
29 | export default swap
30 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: test
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [16.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run build --if-present
31 | - run: npm test
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "jsdom": "^19.0.0",
4 | "tst": "^7.1.0",
5 | "wait-please": "^3.1.0"
6 | },
7 | "name": "swapdom",
8 | "description": "Fast & tiny DOM swapper.",
9 | "version": "1.2.1",
10 | "main": "inflate.js",
11 | "module": "inflate.js",
12 | "browser": "inflate.js",
13 | "type": "module",
14 | "exports": {
15 | ".": "./inflate.js",
16 | "./deflate": "./deflate.js",
17 | "./inflate": "./inflate.js"
18 | },
19 | "scripts": {
20 | "test": "node -r ./test-register.cjs test.js"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/spectjs/swapdom.git"
25 | },
26 | "keywords": [
27 | "domdiff",
28 | "domswap",
29 | "dom",
30 | "swap",
31 | "snabbdom",
32 | "heckel",
33 | "morphdom"
34 | ],
35 | "author": "Dmitry Iv",
36 | "license": "ISC",
37 | "bugs": {
38 | "url": "https://github.com/spectjs/swapdom/issues"
39 | },
40 | "homepage": "https://github.com/spectjs/swapdom#readme"
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # swapdom [](https://github.com/spectjs/swapdom/actions/workflows/test.yml) [](http://npmjs.org/swapdom)
2 |
3 | > Fast & tiny DOM swapper.
4 |
5 | ```js
6 | import swap from 'swapdom'
7 |
8 | swap(parentNode, oldNodes, newNodes, endNode)
9 | ```
10 |
11 | `deflate.js` strategy is smaller (248b), but a bit slower on some cases and doesn't support live collections.
12 | `inflate.js` strategy is bigger (318b), but faster and supports live collections.
13 |
14 | Provide custom mutators as:
15 | ```js
16 | swap.same = (a,b) => a?.isSameNode(b)
17 | swap.replace = (a,b, parent) => a.replaceWith(b)
18 | swap.insert = (a,b, parent) => a ? a.before(b) : parent.append(b)
19 | swap.remove = (a, parent) => a.remove()
20 | ```
21 |
22 | See [benchmark](https://github.com/luwes/js-diff-benchmark) (it's called _spect_ there).
23 |
24 | ## Alternatives
25 |
26 | * [list-difference objects](https://github.com/paldepind/list-difference/blob/master/index.js)
27 | * [list-difference maps](https://github.com/luwes/js-diff-benchmark/blob/master/libs/list-difference.js)
28 |
29 |
ॐ
30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | .DS_Store 78 | -------------------------------------------------------------------------------- /inflate.js: -------------------------------------------------------------------------------- 1 | // inflate version of differ, ~260b 2 | // + no sets / maps used 3 | // + prepend/append/remove/clear short paths 4 | // + a can be live childNodes/HTMLCollection 5 | 6 | const swap = (parent, a, b, end = null, { remove, insert, replace } = swap) => { 7 | let i = 0, cur, next, bi, n = b.length, m = a.length 8 | 9 | // skip head/tail 10 | while (i < n && i < m && a[i] === b[i]) i++ 11 | while (i < n && i < m && b[n - 1] === a[m - 1]) end = b[--m, --n] 12 | 13 | // append/prepend/trim shortcuts 14 | if (i == m) while (i < n) insert(end, b[i++], parent) 15 | 16 | // NOTE: can't use shortcut for childNodes as input 17 | if (i == n) while (i < m) remove(a[i++], parent) 18 | 19 | else { 20 | cur = a[i] 21 | 22 | while (i < n) { 23 | bi = b[i++], next = cur ? cur.nextSibling : end 24 | 25 | // skip 26 | if (cur === bi) cur = next 27 | 28 | // swap / replace 29 | else if (i < n && b[i] === next) (replace(cur, bi, parent), cur = next) 30 | 31 | // insert 32 | else insert(cur, bi, parent) 33 | } 34 | 35 | // remove tail 36 | // NOTE: that can remove elements not in a (if inserted externally) 37 | while (cur !== end) (next = cur.nextSibling, remove(cur, parent), cur = next) 38 | } 39 | 40 | return b 41 | } 42 | 43 | swap.replace = (a, b, parent) => parent.replaceChild(b, a) 44 | swap.insert = (a, b, parent) => parent.insertBefore(b, a) 45 | swap.remove = (a, parent) => parent.removeChild(a) 46 | 47 | 48 | export default swap 49 | -------------------------------------------------------------------------------- /libs/list-difference.js: -------------------------------------------------------------------------------- 1 | export default function(parent, a, b, before) { 2 | const aIdx = new Map(); 3 | const bIdx = new Map(); 4 | let i; 5 | let j; 6 | 7 | // Create a mapping from keys to their position in the old list 8 | for (i = 0; i < a.length; i++) { 9 | aIdx.set(a[i], i); 10 | } 11 | 12 | // Create a mapping from keys to their position in the new list 13 | for (i = 0; i < b.length; i++) { 14 | bIdx.set(b[i], i); 15 | } 16 | 17 | for (i = j = 0; i !== a.length || j !== b.length;) { 18 | var aElm = a[i], bElm = b[j]; 19 | if (aElm === null) { 20 | // This is a element that has been moved to earlier in the list 21 | i++; 22 | } else if (b.length <= j) { 23 | // No more elements in new, this is a delete 24 | parent.removeChild(a[i]); 25 | i++; 26 | } else if (a.length <= i) { 27 | // No more elements in old, this is an addition 28 | parent.insertBefore(bElm, a[i] || before); 29 | j++; 30 | } else if (aElm === bElm) { 31 | // No difference, we move on 32 | i++; j++; 33 | } else { 34 | // Look for the current element at this location in the new list 35 | // This gives us the idx of where this element should be 36 | var curElmInNew = bIdx.get(aElm); 37 | // Look for the the wanted elment at this location in the old list 38 | // This gives us the idx of where the wanted element is now 39 | var wantedElmInOld = aIdx.get(bElm); 40 | if (curElmInNew === undefined) { 41 | // Current element is not in new list, it has been removed 42 | parent.removeChild(a[i]); 43 | i++; 44 | } else if (wantedElmInOld === undefined) { 45 | // New element is not in old list, it has been added 46 | parent.insertBefore( 47 | bElm, 48 | a[i] || before 49 | ); 50 | j++; 51 | } else { 52 | // Element is in both lists, it has been moved 53 | parent.insertBefore( 54 | a[wantedElmInOld], 55 | a[i] || before 56 | ); 57 | a[wantedElmInOld] = null; 58 | if (wantedElmInOld > i + 1) i++; 59 | j++; 60 | } 61 | } 62 | } 63 | return b; 64 | }; 65 | -------------------------------------------------------------------------------- /libs/snabbdom.js: -------------------------------------------------------------------------------- 1 | export default function(parent, a, b, afterNode) { 2 | const a_index = new Map(); 3 | const b_index = new Map(); 4 | 5 | let need_indices; 6 | let start_i = 0; 7 | let end_i = a.length - 1; 8 | let start_j = 0; 9 | let end_j = b.length - 1; 10 | let start_a = a[start_i]; 11 | let end_a = a[end_i]; 12 | let start_b = b[start_j]; 13 | let end_b = b[end_j]; 14 | let old_start_j; 15 | let new_start_i; 16 | 17 | while (start_i <= end_i && start_j <= end_j) { 18 | if (start_a == null) { 19 | start_a = a[++start_i]; 20 | } else if (end_a == null) { 21 | end_a = a[--end_i]; 22 | } else if (start_b == null) { 23 | start_b = b[++start_j]; 24 | } else if (end_b == null) { 25 | end_b = b[--end_j]; 26 | } else if (start_a === start_b) { 27 | start_a = a[++start_i]; 28 | start_b = b[++start_j]; 29 | } else if (end_a === end_b) { 30 | end_a = a[--end_i]; 31 | end_b = b[--end_j]; 32 | } 33 | else if (start_a === end_b) { 34 | parent.insertBefore( 35 | a[start_i], 36 | a[end_i].nextSibling || afterNode 37 | ); 38 | start_a = a[++start_i]; 39 | end_b = b[--end_j]; 40 | } else if (end_a === start_b) { 41 | parent.insertBefore( 42 | a[end_i], 43 | a[start_i] || afterNode 44 | ); 45 | end_a = a[--end_i]; 46 | start_b = b[++start_j]; 47 | } 48 | else { 49 | let i; 50 | // Lazily build maps here. They are relevant only if there has been 51 | // a move, or a mid-list insertion or deletion and not if there 52 | // has been an insertion at the end or deletion from the front. 53 | if (!need_indices) { 54 | need_indices = true; 55 | 56 | // Create a mapping from keys to their position in the old list 57 | for (i = 0; i < a.length; i++) { 58 | a_index.set(a[i], i); 59 | } 60 | // Create a mapping from keys to their position in the new list 61 | for (i = 0; i < b.length; i++) { 62 | b_index.set(b[i], i); 63 | } 64 | } 65 | 66 | old_start_j = a_index.get(start_b); 67 | new_start_i = b_index.get(start_a); 68 | 69 | // Replacement 70 | // If considered on its own (with no other fine-grained update method) 71 | // this is still slower than virtual dom libraries in the general case 72 | // because it doesn't recursively diff and patch the replaced node. 73 | if (old_start_j === undefined && new_start_i === undefined) { 74 | parent.replaceChild( 75 | start_b, 76 | a[start_i] // old 77 | ); 78 | start_a = a[++start_i]; 79 | start_b = b[++start_j]; 80 | } 81 | // Insertion 82 | else if (old_start_j === undefined) { 83 | parent.insertBefore( 84 | start_b, 85 | a[start_i] || afterNode 86 | ); 87 | start_b = b[++start_j]; 88 | } 89 | // Deletion 90 | else if (new_start_i === undefined) { 91 | parent.removeChild(start_a); 92 | start_a = a[++start_i]; 93 | } 94 | // Move 95 | else { 96 | parent.insertBefore( 97 | start_b, 98 | a[start_i] || afterNode 99 | ); 100 | a[old_start_j] = null; 101 | start_b = b[++start_j]; 102 | } 103 | } 104 | } 105 | if (start_i <= end_i || start_j <= end_j) { 106 | if (start_i > end_i) { // old list exhausted; process new list additions 107 | for (start_j; start_j <= end_j; start_b = b[++start_j]) { 108 | parent.insertBefore(start_b, afterNode); 109 | } 110 | } else { // new list exhausted; process old list removals 111 | for (start_i; start_i <= end_i; ++start_i) { 112 | parent.removeChild(a[start_i]); 113 | } 114 | } 115 | } 116 | return b; 117 | }; 118 | -------------------------------------------------------------------------------- /libs/udomdiff.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 | export default (parentNode, a, b, before) => { 19 | const bLength = b.length; 20 | let aEnd = a.length; 21 | let bEnd = bLength; 22 | let aStart = 0; 23 | let bStart = 0; 24 | let map = null; 25 | while (aStart < aEnd || bStart < bEnd) { 26 | // append head, tail, or nodes in between: fast path 27 | if (aEnd === aStart) { 28 | // we could be in a situation where the rest of nodes that 29 | // need to be added are not at the end, and in such case 30 | // the node to `insertBefore`, if the index is more than 0 31 | // must be retrieved, otherwise it's gonna be the first item. 32 | const node = bEnd < bLength ? 33 | (bStart ? 34 | (b[bStart - 1].nextSibling) : 35 | b[bEnd - bStart]) : 36 | before; 37 | while (bStart < bEnd) 38 | parentNode.insertBefore(b[bStart++], node); 39 | } 40 | // remove head or tail: fast path 41 | else if (bEnd === bStart) { 42 | while (aStart < aEnd) { 43 | // remove the node only if it's unknown or not live 44 | if (!map || !map.has(a[aStart])) 45 | parentNode.removeChild(a[aStart]); 46 | aStart++; 47 | } 48 | } 49 | // same node: fast path 50 | else if (a[aStart] === b[bStart]) { 51 | aStart++; 52 | bStart++; 53 | } 54 | // same tail: fast path 55 | else if (a[aEnd - 1] === b[bEnd - 1]) { 56 | aEnd--; 57 | bEnd--; 58 | } 59 | // The once here single last swap "fast path" has been removed in v1.1.0 60 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 61 | // reverse swap: also fast path 62 | else if ( 63 | a[aStart] === b[bEnd - 1] && 64 | b[bStart] === a[aEnd - 1] 65 | ) { 66 | // this is a "shrink" operation that could happen in these cases: 67 | // [1, 2, 3, 4, 5] 68 | // [1, 4, 3, 2, 5] 69 | // or asymmetric too 70 | // [1, 2, 3, 4, 5] 71 | // [1, 2, 3, 5, 6, 4] 72 | const node = a[--aEnd].nextSibling; 73 | parentNode.insertBefore( 74 | b[bStart++], 75 | a[aStart++].nextSibling 76 | ); 77 | parentNode.insertBefore(b[--bEnd], node); 78 | // mark the future index as identical (yeah, it's dirty, but cheap 👍) 79 | // The main reason to do this, is that when a[aEnd] will be reached, 80 | // the loop will likely be on the fast path, as identical to b[bEnd]. 81 | // In the best case scenario, the next loop will skip the tail, 82 | // but in the worst one, this node will be considered as already 83 | // processed, bailing out pretty quickly from the map index check 84 | a[aEnd] = b[bEnd]; 85 | } 86 | // map based fallback, "slow" path 87 | else { 88 | // the map requires an O(bEnd - bStart) operation once 89 | // to store all future nodes indexes for later purposes. 90 | // In the worst case scenario, this is a full O(N) cost, 91 | // and such scenario happens at least when all nodes are different, 92 | // but also if both first and last items of the lists are different 93 | if (!map) { 94 | map = new Map; 95 | let i = bStart; 96 | while (i < bEnd) 97 | 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 | const 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 | let i = aStart; 106 | // counts the amount of nodes that are the same in the future 107 | let sequence = 1; 108 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 109 | sequence++; 110 | // effort decision here: if the sequence is longer than replaces 111 | // needed to reach such sequence, which would brings again this loop 112 | // to the fast path, prepend the difference before a sequence, 113 | // and move only the future list index forward, so that aStart 114 | // and bStart will be aligned again, hence on the fast path. 115 | // An example considering aStart and bStart are both 0: 116 | // a: [1, 2, 3, 4] 117 | // b: [7, 1, 2, 3, 6] 118 | // this would place 7 before 1 and, from that time on, 1, 2, and 3 119 | // will be processed at zero cost 120 | if (sequence > (index - bStart)) { 121 | const node = a[aStart]; 122 | while (bStart < index) 123 | parentNode.insertBefore(b[bStart++], node); 124 | } 125 | // if the effort wasn't good enough, fallback to a replace, 126 | // moving both source and target indexes forward, hoping that some 127 | // similar node will be found later on, to go back to the fast path 128 | else { 129 | parentNode.replaceChild( 130 | b[bStart++], 131 | a[aStart++] 132 | ); 133 | } 134 | } 135 | // otherwise move the source forward, 'cause there's nothing to do 136 | else 137 | aStart++; 138 | } 139 | // this node has no meaning in the future list, so it's more than safe 140 | // to remove it, and check the next live node out instead, meaning 141 | // that only the live list index should be forwarded 142 | else 143 | parentNode.removeChild(a[aStart++]); 144 | } 145 | } 146 | return b; 147 | }; 148 | -------------------------------------------------------------------------------- /libs/stage0.js: -------------------------------------------------------------------------------- 1 | // This is almost straightforward implementation of reconcillation algorithm 2 | // based on ivi documentation: 3 | // https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366 4 | // With some fast paths from Surplus implementation: 5 | // https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L86 6 | // 7 | // https://github.com/Freak613/stage0/blob/master/reconcile.js 8 | // How this implementation differs from others, is that it's working with data directly, 9 | // without maintaining nodes arrays, and uses dom props firstChild/lastChild/nextSibling 10 | // for markers moving. 11 | 12 | export default function reconcile( 13 | parent, 14 | renderedValues, 15 | data, 16 | afterNode, 17 | beforeNode 18 | ) { 19 | // Fast path for clear 20 | if (data.length === 0) { 21 | let node = 22 | beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild, 23 | tmp; 24 | 25 | if (afterNode === undefined) afterNode = null; 26 | 27 | while (node !== afterNode) { 28 | tmp = node.nextSibling; 29 | parent.removeChild(node); 30 | node = tmp; 31 | } 32 | return data; 33 | } 34 | 35 | // Fast path for create 36 | if (renderedValues.length === 0) { 37 | let node, 38 | mode = afterNode !== undefined ? 1 : 0; 39 | for (let i = 0, len = data.length; i < len; i++) { 40 | node = data[i]; 41 | mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); 42 | } 43 | return data; 44 | } 45 | 46 | let prevStart = 0, 47 | newStart = 0, 48 | loop = true, 49 | prevEnd = renderedValues.length - 1, 50 | newEnd = data.length - 1, 51 | a, 52 | b, 53 | prevStartNode = beforeNode ? beforeNode.nextSibling : parent.firstChild, 54 | newStartNode = prevStartNode, 55 | prevEndNode = afterNode ? afterNode.previousSibling : parent.lastChild; 56 | 57 | fixes: while (loop) { 58 | loop = false; 59 | let _node; 60 | 61 | // Skip prefix 62 | (a = renderedValues[prevStart]), (b = data[newStart]); 63 | while (a === b) { 64 | prevStart++; 65 | newStart++; 66 | newStartNode = prevStartNode = prevStartNode.nextSibling; 67 | if (prevEnd < prevStart || newEnd < newStart) break fixes; 68 | a = renderedValues[prevStart]; 69 | b = data[newStart]; 70 | } 71 | 72 | // Skip suffix 73 | (a = renderedValues[prevEnd]), (b = data[newEnd]); 74 | while (a === b) { 75 | prevEnd--; 76 | newEnd--; 77 | afterNode = prevEndNode; 78 | prevEndNode = prevEndNode.previousSibling; 79 | if (prevEnd < prevStart || newEnd < newStart) break fixes; 80 | a = renderedValues[prevEnd]; 81 | b = data[newEnd]; 82 | } 83 | 84 | // Fast path to swap backward 85 | (a = renderedValues[prevEnd]), (b = data[newStart]); 86 | while (a === b) { 87 | loop = true; 88 | _node = prevEndNode.previousSibling; 89 | parent.insertBefore(prevEndNode, newStartNode); 90 | prevEndNode = _node; 91 | newStart++; 92 | prevEnd--; 93 | if (prevEnd < prevStart || newEnd < newStart) break fixes; 94 | a = renderedValues[prevEnd]; 95 | b = data[newStart]; 96 | } 97 | 98 | // Fast path to swap forward 99 | (a = renderedValues[prevStart]), (b = data[newEnd]); 100 | while (a === b) { 101 | loop = true; 102 | _node = prevStartNode.nextSibling; 103 | parent.insertBefore(prevStartNode, afterNode); 104 | prevStart++; 105 | afterNode = prevStartNode; 106 | prevStartNode = _node; 107 | newEnd--; 108 | if (prevEnd < prevStart || newEnd < newStart) break fixes; 109 | a = renderedValues[prevStart]; 110 | b = data[newEnd]; 111 | } 112 | } 113 | 114 | // Fast path for shrink 115 | if (newEnd < newStart) { 116 | if (prevStart <= prevEnd) { 117 | let next; 118 | while (prevStart <= prevEnd) { 119 | if (prevEnd === 0) { 120 | parent.removeChild(prevEndNode); 121 | } else { 122 | next = prevEndNode.previousSibling; 123 | parent.removeChild(prevEndNode); 124 | prevEndNode = next; 125 | } 126 | prevEnd--; 127 | } 128 | } 129 | return data; 130 | } 131 | 132 | // Fast path for add 133 | if (prevEnd < prevStart) { 134 | if (newStart <= newEnd) { 135 | let node, 136 | mode = afterNode ? 1 : 0; 137 | while (newStart <= newEnd) { 138 | node = data[newStart]; 139 | mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); 140 | newStart++; 141 | } 142 | } 143 | return data; 144 | } 145 | 146 | // Positions for reusing nodes from current DOM state 147 | const P = new Array(newEnd + 1 - newStart); 148 | for (let i = newStart; i <= newEnd; i++) P[i] = -1; 149 | 150 | // Index to resolve position from current to new 151 | const I = new Map(); 152 | for (let i = newStart; i <= newEnd; i++) I.set(data[i], i); 153 | 154 | let reusingNodes = newStart + data.length - 1 - newEnd, 155 | toRemove = []; 156 | 157 | for (let i = prevStart; i <= prevEnd; i++) { 158 | if (I.has(renderedValues[i])) { 159 | P[I.get(renderedValues[i])] = i; 160 | reusingNodes++; 161 | } else { 162 | toRemove.push(i); 163 | } 164 | } 165 | 166 | // Fast path for full replace 167 | if (reusingNodes === 0) { 168 | let node = 169 | beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild, 170 | tmp; 171 | 172 | if (afterNode === undefined) afterNode = null; 173 | 174 | while (node !== afterNode) { 175 | tmp = node.nextSibling; 176 | parent.removeChild(node); 177 | node = tmp; 178 | prevStart++; 179 | } 180 | 181 | let mode = afterNode ? 1 : 0; 182 | for (let i = newStart; i <= newEnd; i++) { 183 | node = data[i]; 184 | mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); 185 | } 186 | 187 | return data; 188 | } 189 | 190 | // What else? 191 | const longestSeq = longestPositiveIncreasingSubsequence(P, newStart); 192 | 193 | // Collect nodes to work with them 194 | const nodes = []; 195 | let tmpC = prevStartNode; 196 | for (let i = prevStart; i <= prevEnd; i++) { 197 | nodes[i] = tmpC; 198 | tmpC = tmpC.nextSibling; 199 | } 200 | 201 | for (let i = 0; i < toRemove.length; i++) 202 | parent.removeChild(nodes[toRemove[i]]); 203 | 204 | let lisIdx = longestSeq.length - 1, 205 | tmpD; 206 | for (let i = newEnd; i >= newStart; i--) { 207 | if (longestSeq[lisIdx] === i) { 208 | afterNode = nodes[P[longestSeq[lisIdx]]]; 209 | lisIdx--; 210 | } else { 211 | if (P[i] === -1) { 212 | tmpD = data[i]; 213 | } else { 214 | tmpD = nodes[P[i]]; 215 | } 216 | parent.insertBefore(tmpD, afterNode); 217 | afterNode = tmpD; 218 | } 219 | } 220 | 221 | return data; 222 | }; 223 | 224 | // Picked from 225 | // https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368 226 | 227 | // return an array of the indices of ns that comprise the longest increasing subsequence within ns 228 | function longestPositiveIncreasingSubsequence(ns, newStart) { 229 | var seq = [], 230 | is = [], 231 | l = -1, 232 | pre = new Array(ns.length); 233 | 234 | for (var i = newStart, len = ns.length; i < len; i++) { 235 | var n = ns[i]; 236 | if (n < 0) continue; 237 | var j = findGreatestIndexLEQ(seq, n); 238 | if (j !== -1) pre[i] = is[j]; 239 | if (j === l) { 240 | l++; 241 | seq[l] = n; 242 | is[l] = i; 243 | } else if (n < seq[j + 1]) { 244 | seq[j + 1] = n; 245 | is[j + 1] = i; 246 | } 247 | } 248 | 249 | for (i = is[l]; l >= 0; i = pre[i], l--) { 250 | seq[l] = i; 251 | } 252 | 253 | return seq; 254 | } 255 | 256 | function findGreatestIndexLEQ(seq, n) { 257 | // invariant: lo is guaranteed to be index of a value <= n, hi to be > 258 | // therefore, they actually start out of range: (-1, last + 1) 259 | var lo = -1, 260 | hi = seq.length; 261 | 262 | // fast path for simple increasing sequences 263 | if (hi > 0 && seq[hi - 1] <= n) return hi - 1; 264 | 265 | while (hi - lo > 1) { 266 | var mid = Math.floor((lo + hi) / 2); 267 | if (seq[mid] > n) { 268 | hi = mid; 269 | } else { 270 | lo = mid; 271 | } 272 | } 273 | 274 | return lo; 275 | } 276 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // dom diff algo benchmark 2 | import t, { is, ok, any } from './node_modules/tst/tst.js' 3 | import { time } from './node_modules/wait-please/index.js' 4 | // import diff from './libs/list-difference.js' 5 | // import diff from './libs/udomdiff.js' 6 | // import diff from './libs/snabbdom.js' 7 | // import diff from './libs/stage0.js' 8 | // import diff from './inflate.js' 9 | import diff from './deflate.js' 10 | // import diff from './deflate.obj.js' 11 | 12 | 13 | const t1 = document.createElement('i1'), 14 | t2 = document.createElement('i2'), 15 | t3 = document.createElement('i3'), 16 | t4 = document.createElement('i4'), 17 | t5 = document.createElement('i5'), 18 | t6 = document.createElement('i6'), 19 | t7 = document.createElement('i7'), 20 | t8 = document.createElement('i8'), 21 | t9 = document.createElement('i9'), 22 | t0 = document.createElement('i0') 23 | 24 | 25 | // test fragment stub with node-compatible API 26 | const frag = () => { 27 | let f = document.createDocumentFragment() 28 | f.count = 0 29 | f.reset = () => f.count = 0 30 | 31 | let _insertBefore = f.insertBefore 32 | f.insertBefore = function () { _insertBefore.apply(this, arguments), f.count++ } 33 | let _appendChild = f.appendChild 34 | f.appendChild = function () { _appendChild.apply(this, arguments), f.count++ } 35 | let _replaceChild = f.replaceChild 36 | f.replaceChild = function () { _replaceChild.apply(this, arguments), f.count++ } 37 | let _removeChild = f.removeChild 38 | f.removeChild = function () { _removeChild.apply(this, arguments), f.count++ } 39 | 40 | return f 41 | } 42 | 43 | t('begin', () => console.time('total')) 44 | 45 | t('create', t => { 46 | let parent = frag(); 47 | 48 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5], null); 49 | is([...parent.childNodes], [t1, t2, t3, t4, t5], 'create') 50 | ok(parent.count <= 5) 51 | }) 52 | 53 | t('remove', t => { 54 | let parent = frag(); 55 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5],); 56 | 57 | console.log('---remove') 58 | diff(parent, [...parent.childNodes], [t1, t3, t5],); 59 | is([...parent.childNodes], [t1, t3, t5], 'remove') 60 | }) 61 | 62 | t('insert', t => { 63 | let parent = frag(); 64 | diff(parent, [...parent.childNodes], [t1, t3, t5],); 65 | is([...parent.childNodes], [t1, t3, t5], 'created') 66 | 67 | console.log('insert') 68 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5],); 69 | is([...parent.childNodes], [t1, t2, t3, t4, t5], 'insert') 70 | }) 71 | 72 | t('append before', t => { 73 | let parent = frag(); 74 | diff(parent, [], [t1, t5]); 75 | diff(parent, [], [t3], t5); 76 | is([...parent.childNodes], [t1, t3, t5], 'insert') 77 | }) 78 | 79 | t('prepend', t => { 80 | let parent = frag(); 81 | diff(parent, [], [t4, t5], null); 82 | diff(parent, [t4, t5], [t1, t2, t3, t4, t5], null); 83 | is([...parent.childNodes], [t1, t2, t3, t4, t5], 'prepended') 84 | }) 85 | t('swap 2/5', t => { 86 | let parent = frag(); 87 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5],); 88 | parent.reset() 89 | 90 | console.log('---swap') 91 | diff(parent, [...parent.childNodes], [t1, t5, t3, t4, t2],); 92 | is([...parent.childNodes], [t1, t5, t3, t4, t2]) 93 | 94 | // ok(parent.count <= 2, 'ops') 95 | ok(parent.count <= 6, 'ops') 96 | }) 97 | t('swap-replace', t => { 98 | let parent = frag(); 99 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6],); 100 | parent.reset() 101 | 102 | console.log('---swap') 103 | diff(parent, [...parent.childNodes], [t1, t5, t3, t8, t4],); 104 | is([...parent.childNodes], [t1, t5, t3, t8, t4]) 105 | }) 106 | 107 | t('swap', t => { 108 | let parent = frag(); 109 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6, t7, t8, t9],); 110 | parent.reset() 111 | 112 | console.log('---swap') 113 | diff(parent, [...parent.childNodes], [t1, t8, t3, t4, t5, t6, t7, t2, t9],); 114 | is([...parent.childNodes], [t1, t8, t3, t4, t5, t6, t7, t2, t9]) 115 | 116 | // ok(parent.count <= 2, 'ops') 117 | }) 118 | 119 | t('swap-tail', t => { 120 | let parent = frag(); 121 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6, t7, t8, t9]); 122 | parent.reset() 123 | 124 | console.log('---swap') 125 | diff(parent, [...parent.childNodes], [t9, t2, t3, t4, t5, t6, t7, t8, t1]); 126 | is([...parent.childNodes], [t9, t2, t3, t4, t5, t6, t7, t8, t1]) 127 | 128 | // ok(parent.count <= 2, 'ops') 129 | }) 130 | 131 | t('single', t => { 132 | let parent = frag(); 133 | diff(parent, [...parent.childNodes], [t1]); 134 | parent.reset() 135 | 136 | diff(parent, [...parent.childNodes], [t2]); 137 | is([...parent.childNodes], [t2]) 138 | 139 | ok(parent.count < 3, 'ops') 140 | }) 141 | 142 | t('one in the middle', t => { 143 | let parent = frag(); 144 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 145 | parent.reset() 146 | 147 | console.log('---swap') 148 | diff(parent, [...parent.childNodes], [t1, t2, t6, t4, t5]); 149 | is([...parent.childNodes], [t1, t2, t6, t4, t5]) 150 | 151 | // ok(parent.count <= 4, 'ops') 152 | }) 153 | 154 | t('ring', t => { 155 | let parent = frag(); 156 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 157 | parent.reset() 158 | 159 | console.log('---ring') 160 | diff(parent, [...parent.childNodes], [t2, t3, t4, t5, t1]); 161 | is([...parent.childNodes], [t2, t3, t4, t5, t1]) 162 | 163 | ok(parent.count <= 4, 'ops') 164 | }) 165 | 166 | t('shiftpop', t => { 167 | let parent = frag(); 168 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 169 | parent.reset() 170 | 171 | console.log('---shiftpop') 172 | diff(parent, [...parent.childNodes], [t0, t1, t2, t3, t4]); 173 | is([...parent.childNodes], [t0, t1, t2, t3, t4]) 174 | 175 | // ok(parent.count <= 4, 'ops') 176 | }) 177 | 178 | t('endswap', t => { 179 | let parent = frag(); 180 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 181 | parent.reset() 182 | 183 | console.log('--->') 184 | diff(parent, [...parent.childNodes], [t5, t2, t3, t4, t0]); 185 | is([...parent.childNodes], [t5, t2, t3, t4, t0]) 186 | 187 | // ok(parent.count <= 6, 'ops') 188 | }) 189 | 190 | t('endswap2', t => { 191 | let parent = frag(); 192 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 193 | parent.reset() 194 | 195 | console.log('--->') 196 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t6]); 197 | is([...parent.childNodes], [t1, t2, t3, t4, t6]) 198 | }) 199 | 200 | // FIXME: this removes extra nodes 201 | t.todo('foreign', t => { 202 | let parent = frag(); 203 | diff(parent, [], [t1, t2, t3]); 204 | let t2clone = t2.cloneNode(true) 205 | parent.insertBefore(t2clone, t2) 206 | parent.reset() 207 | 208 | console.log('---foreign swap') 209 | diff(parent, [t1, t2, t3], [t2, t3]); 210 | is([...parent.childNodes], [t2clone, t2, t3]) 211 | }) 212 | 213 | t('shuffle1', t => { 214 | let parent = frag(); 215 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 216 | parent.reset() 217 | 218 | console.log('---swap') 219 | diff(parent, [...parent.childNodes], [t1, t3, t5, t4, t2],); 220 | is([...parent.childNodes], [t1, t3, t5, t4, t2]) 221 | ok(parent.count <= 6, 'ops count') 222 | }) 223 | 224 | t('shuffle2', t => { 225 | let parent = frag(); 226 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 227 | parent.reset() 228 | 229 | console.log('---swap') 230 | diff(parent, [...parent.childNodes], [t4, t5, t3, t2, t1],); 231 | is([...parent.childNodes], [t4, t5, t3, t2, t1]) 232 | ok(parent.count < 7, 'ops count') 233 | }) 234 | 235 | t('shuffle3', t => { 236 | let parent = frag(); 237 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 238 | parent.reset() 239 | 240 | console.log('---swap') 241 | diff(parent, [...parent.childNodes], [t2, t5, t1, t4, t3],); 242 | is([...parent.childNodes], [t2, t5, t1, t4, t3]) 243 | ok(parent.count < 7, 'ops count') 244 | }) 245 | 246 | t('shuffle4', t => { 247 | let parent = frag(); 248 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 249 | parent.reset() 250 | 251 | console.log('---swap') 252 | diff(parent, [...parent.childNodes], [t3, t4, t5, t2, t1],); 253 | is([...parent.childNodes], [t3, t4, t5, t2, t1]) 254 | ok(parent.count < 10, 'ops count') 255 | }) 256 | 257 | t('shuffle5', t => { 258 | let parent = frag(); 259 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 260 | parent.reset() 261 | 262 | console.log('---chain-swap') 263 | diff(parent, [...parent.childNodes], [t1, t2, t3, t5, t4],); 264 | is([...parent.childNodes], [t1, t2, t3, t5, t4]) 265 | ok(parent.count < 10, 'ops count') 266 | }) 267 | 268 | t('shuffle6', t => { 269 | let parent = frag(); 270 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 271 | parent.reset() 272 | 273 | console.log('---chain-swap') 274 | diff(parent, [...parent.childNodes], [t4, t3, t2, t1, t5],); 275 | is([...parent.childNodes], [t4, t3, t2, t1, t5]) 276 | ok(parent.count < 10, 'ops count') 277 | }) 278 | 279 | t('shuffle7', t => { 280 | let parent = frag(); 281 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 282 | parent.reset() 283 | 284 | console.log('---chain-swap') 285 | diff(parent, [...parent.childNodes], [t2, t3, t4, t5, t1],); 286 | is([...parent.childNodes], [t2, t3, t4, t5, t1]) 287 | ok(parent.count < 10, 'ops count') 288 | }) 289 | 290 | t('shuffle8', t => { 291 | let parent = frag(); 292 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 293 | parent.reset() 294 | 295 | console.log('---chain-swap') 296 | diff(parent, [...parent.childNodes], [t3, t4, t2, t1, t5],); 297 | is([...parent.childNodes], [t3, t4, t2, t1, t5]) 298 | ok(parent.count < 10, 'ops count') 299 | }) 300 | 301 | t('tiny-random-2', t => { 302 | let parent = frag(); 303 | const initial = [t1, t2, t3, t4, t5] 304 | diff(parent, [...parent.childNodes], initial); 305 | parent.reset() 306 | 307 | console.log('---chain-swap') 308 | let ordered = initial.slice().sort(() => Math.random() - Math.random()) 309 | diff(parent, [...parent.childNodes], ordered, null); 310 | is([...parent.childNodes], ordered) 311 | ok(parent.count < 10, 'ops count') 312 | }) 313 | 314 | t.skip('live-nodes', t => { 315 | // we skip since we test object as input 316 | let parent = frag(); 317 | const initial = [t1, t2, t3, t4, t5] 318 | diff(parent, [...parent.childNodes], initial); 319 | parent.reset() 320 | 321 | console.log('---chain-swap') 322 | let ordered = initial.slice().sort(() => Math.random() - Math.random()) 323 | diff(parent, parent.childNodes, ordered,); 324 | is([...parent.childNodes], ordered) 325 | ok(parent.count < 10, 'ops count') 326 | }) 327 | 328 | t('reverse', t => { 329 | let parent = frag(); 330 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5],); 331 | is([...parent.childNodes], [t1, t2, t3, t4, t5]) 332 | console.log('---reverse') 333 | diff(parent, [...parent.childNodes], [t5, t4, t3, t2, t1],); 334 | is([...parent.childNodes], [t5, t4, t3, t2, t1]) 335 | }) 336 | 337 | t('clear', t => { 338 | let parent = frag(); 339 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 340 | console.log('---clear') 341 | diff(parent, [...parent.childNodes], [],); 342 | is([...parent.childNodes], []) 343 | }) 344 | t.skip('clear live', t => { 345 | let parent = frag(); 346 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5]); 347 | console.log('---clear') 348 | diff(parent, parent.childNodes, [],); 349 | is([...parent.childNodes], []) 350 | }) 351 | 352 | t('remove with head/tail', t => { 353 | let parent = frag(); 354 | console.groupCollapsed('init') 355 | diff(parent, [...parent.childNodes], [t0, t1, t2, t3, t4, t5, t6]); 356 | console.groupEnd() 357 | console.log('---remove') 358 | diff(parent, [...parent.childNodes], [t1, t3, t5],); 359 | is([...parent.childNodes], [t1, t3, t5]) 360 | }) 361 | 362 | t('reverse-add', t => { 363 | let parent = frag(); 364 | diff(parent, [...parent.childNodes], [t5, t4, t3, t2, t1],); 365 | 366 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6],); 367 | is([...parent.childNodes], [t1, t2, t3, t4, t5, t6]) 368 | console.groupEnd() 369 | }) 370 | 371 | t('swap 10', t => { 372 | let parent = frag(); 373 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6, t7, t8, t9, t0],); 374 | parent.reset() 375 | console.log('---swap') 376 | diff(parent, [...parent.childNodes], [t1, t8, t3, t4, t5, t6, t7, t2, t9, t0],); 377 | is([...parent.childNodes], [t1, t8, t3, t4, t5, t6, t7, t2, t9, t0], 'order') 378 | // ok(parent.count < 5, 'ops count') 379 | // ok(parent.count < 7, 'ops count') // since we test deflate without shortcut 380 | }) 381 | 382 | t('update each 3', t => { 383 | console.groupCollapsed('create') 384 | let parent = frag(); 385 | diff(parent, [...parent.childNodes], [t1, t2, t3, t4, t5, t6, t7, t8, t9],); 386 | console.groupEnd() 387 | console.log('---update') 388 | let x = document.createTextNode(0), y = document.createTextNode(0), z = document.createTextNode(0) 389 | diff(parent, [...parent.childNodes], [t1, t2, x, t4, t5, y, t7, t8, z],); 390 | is([...parent.childNodes], [t1, t2, x, t4, t5, y, t7, t8, z]) 391 | }) 392 | 393 | t.todo('morph text', t => { 394 | let parent = frag(); 395 | diff(parent, [...parent.childNodes], [t1, t2, t3],); 396 | console.log('---update') 397 | let x = document.createTextNode('i1'), y = document.createTextNode('i2'), z = document.createTextNode('i3') 398 | diff(parent, [...parent.childNodes], [z, y, x],); 399 | is([...parent.childNodes], [z, t2, x]) 400 | }) 401 | 402 | t('create/replace ops', t => { 403 | // That's fine: failed due to wrong nodes 404 | let parent = frag() 405 | const N = 100 406 | 407 | let start = 0; 408 | let childNodes = []; 409 | for (let i = 0; i < N; i++) childNodes.push(document.createTextNode(start + i)) 410 | 411 | parent.reset() 412 | diff(parent, [...parent.childNodes], childNodes,) 413 | 414 | ok(parent.count <= N) 415 | 416 | // replace 417 | start = N 418 | childNodes = [] 419 | for (let i = 0; i < N; i++) childNodes.push(document.createTextNode(start + i)) 420 | 421 | parent.reset() 422 | console.log('---replace') 423 | diff(parent, [...parent.childNodes], childNodes,) 424 | console.log(parent.count) 425 | is((parent.count - N) <= 100, true, 'ops count') 426 | }) 427 | 428 | t('js-diff-benchmark random', t => { 429 | const parent = frag() 430 | let childNodes = create1000(parent, diff); 431 | 432 | const shuffled = childNodes.slice().sort(() => Math.random() - Math.random()) 433 | childNodes = diff(parent, childNodes, shuffled); 434 | 435 | is(childNodes.length, 1000, 'result length') 436 | ok(childNodes.every((row, i) => row === parent.childNodes[i]), 'order') 437 | }) 438 | 439 | // actual benchmark 440 | t('create 1000', async t => { 441 | const parent = frag() 442 | console.time('create 1000'); 443 | const rows = create1000(parent, diff); 444 | console.timeEnd('create 1000'); 445 | is([...parent.childNodes].every((row, i) => row === rows[i]), true); 446 | const out = ['operations', parent.count]; 447 | if (parent.count > 1000) { 448 | console.warn(`+${parent.count - 1000}`); 449 | } 450 | console.log(...out, '\n'); 451 | parent.reset(); 452 | }) 453 | 454 | t('random', async t => { 455 | const parent = frag() 456 | create1000(parent, diff); 457 | parent.reset(); 458 | console.time('random'); 459 | const rows = random(parent, diff); 460 | console.timeEnd('random'); 461 | 462 | ok([...parent.childNodes].every((row, i) => row === rows[i]), 'data is correct'); 463 | const out = ['operations', parent.count]; 464 | if (parent.count > 1000) { 465 | console.warn(`+${parent.count - 1000}`); 466 | } 467 | console.log(...out, '\n'); 468 | parent.reset(); 469 | }) 470 | 471 | t('reverse 1000', async t => { 472 | const parent = frag() 473 | create1000(parent, diff); 474 | random(parent, diff); 475 | parent.reset() 476 | console.time('reverse'); 477 | const rows = reverse(parent, diff); 478 | console.timeEnd('reverse'); 479 | ok([...parent.childNodes].every((row, i) => row === rows[i])); 480 | const out = ['operations', parent.count]; 481 | if (parent.count > 1000) { 482 | console.warn(`+${parent.count - 1000}`); 483 | } 484 | console.log(...out, '\n'); 485 | parent.reset(); 486 | }) 487 | 488 | t('clear 1000', async t => { 489 | const parent = frag() 490 | create1000(parent, diff); 491 | parent.reset() 492 | console.time('clear'); 493 | const rows = clear(parent, diff); 494 | console.timeEnd('clear'); 495 | ok([...parent.childNodes].every((row, i) => row === rows[i])) 496 | is(rows.length, 0); 497 | const out = ['operations', parent.count]; 498 | if (parent.count > 1000) { 499 | console.warn(`+${parent.count - 1000}`); 500 | } 501 | console.log(...out, '\n'); 502 | parent.reset(); 503 | }) 504 | 505 | t('replace 1000', async t => { 506 | const parent = frag() 507 | create1000(parent, diff); 508 | parent.reset(); 509 | console.time('replace 1000'); 510 | const rows = create1000(parent, diff); 511 | console.timeEnd('replace 1000'); 512 | is([...parent.childNodes].every((row, i) => row === rows[i]), true); 513 | const out = ['operations', parent.count]; 514 | if (parent.count > 2000) { 515 | console.warn(`+${parent.count - 2000}`); 516 | } 517 | console.log(...out, '\n'); 518 | clear(parent, diff); 519 | parent.reset(); 520 | 521 | }) 522 | 523 | t('append 1000', async t => { 524 | const parent = frag() 525 | create1000(parent, diff); 526 | parent.reset(); 527 | console.time('append 1000'); 528 | const rows = append1000(parent, diff); 529 | console.timeEnd('append 1000'); 530 | ok([...parent.childNodes].every((row, i) => row === rows[i])) 531 | is(rows.length, 2000); 532 | const out = ['operations', parent.count]; 533 | if (parent.count > 1000) { 534 | console.warn(`+${parent.count - 1000}`); 535 | } 536 | console.log(...out, '\n'); 537 | parent.reset(); 538 | }) 539 | 540 | t('append more', async t => { 541 | const parent = frag() 542 | create1000(parent, diff); 543 | append1000(parent, diff); 544 | parent.reset(); 545 | console.time('append more'); 546 | const rows = append1000(parent, diff); 547 | console.timeEnd('append more'); 548 | is([...parent.childNodes].every((row, i) => row === rows[i]), true) 549 | is(rows.length, 3000); 550 | const out = ['operations', parent.count]; 551 | if (parent.count > 1000) { 552 | console.warn(`+${parent.count - 1000}`); 553 | } 554 | console.log(...out, '\n'); 555 | parent.reset(); 556 | clear(parent, diff); 557 | }) 558 | 559 | 560 | t('swap rows', async t => { 561 | const parent = frag() 562 | create1000(parent, diff); 563 | parent.reset(); 564 | console.time('swap rows'); 565 | swapRows(parent, diff); 566 | console.timeEnd('swap rows'); 567 | const out = ['operations', parent.count]; 568 | if (parent.count > 2) { 569 | console.warn(`+${parent.count - 2}`); 570 | } 571 | console.log(...out, '\n'); 572 | parent.reset(); 573 | }) 574 | 575 | t('update every 10th row', async t => { 576 | const parent = frag() 577 | create1000(parent, diff); 578 | parent.reset(); 579 | console.time('update every 10th row'); 580 | updateEach10thRow(parent, diff); 581 | console.timeEnd('update every 10th row'); 582 | const out = ['operations', parent.count]; 583 | if (parent.count > 200) { 584 | console.warn(`+${parent.count - 200}`); 585 | } 586 | console.log(...out, '\n'); 587 | parent.reset(); 588 | 589 | clear(parent, diff); 590 | parent.reset(); 591 | }) 592 | 593 | t('create comparisons', async t => { 594 | let parent, childNodes 595 | 596 | const N = 1e5 597 | 598 | parent = frag() 599 | childNodes = [] 600 | for (let i = 0; i < N; i++) childNodes.push(document.createTextNode(i)) 601 | console.time('appendChild') 602 | for (let i = 0; i < N; i++) parent.appendChild(childNodes[i]); 603 | console.timeEnd('appendChild') 604 | 605 | await time(250) 606 | 607 | parent = frag() 608 | childNodes = [] 609 | for (let i = 0; i < N; i++) childNodes.push(document.createTextNode(i)) 610 | console.time('insertBefore') 611 | for (let i = 0; i < N; i++) parent.insertBefore(childNodes[i], null); 612 | console.timeEnd('insertBefore') 613 | 614 | await time(250) 615 | 616 | parent = frag() 617 | childNodes = [] 618 | for (let i = 0; i < N; i++) childNodes.push(document.createTextNode(i)) 619 | console.time('diff') 620 | diff(parent, [], childNodes) 621 | console.timeEnd('diff') 622 | }) 623 | 624 | t.skip('create 10000 rows', async t => { 625 | const parent = frag() 626 | 627 | console.time('create 10000 rows'); 628 | create10000(parent, diff); 629 | console.timeEnd('create 10000 rows'); 630 | 631 | const out = ['operations', parent.count]; 632 | if (parent.count > 10000) { 633 | console.warn(`+${parent.count - 10000}`); 634 | } 635 | console.log(...out, '\n'); 636 | parent.reset(); 637 | }) 638 | 639 | t.skip('swap over 10000 rows', async t => { 640 | const parent = frag() 641 | create10000(parent, diff); 642 | parent.reset() 643 | console.time('swap over 10000 rows'); 644 | swapRows(parent, diff); 645 | console.timeEnd('swap over 10000 rows'); 646 | const out = ['operations', parent.count]; 647 | if (parent.count > 2) { 648 | console.warn(`+${parent.count - 2}`); 649 | } 650 | console.log(...out, '\n'); 651 | parent.reset(); 652 | }) 653 | 654 | t.skip('clear 10000', async t => { 655 | const parent = frag() 656 | create10000(parent, diff); 657 | parent.reset() 658 | console.time('clear 10000'); 659 | clear(parent, diff); 660 | console.timeEnd('clear 10000'); 661 | const out = ['operations', parent.count]; 662 | if (parent.count > 10000) { 663 | console.warn(`+${parent.count - 10000}`); 664 | } 665 | console.log(...out, '\n'); 666 | parent.reset(); 667 | }) 668 | 669 | t('end', t => console.timeEnd('total')) 670 | 671 | 672 | function random(parent, diff) { 673 | return diff( 674 | parent, 675 | [...parent.childNodes], 676 | Array.from(parent.childNodes).sort(() => Math.random() - Math.random()), 677 | ); 678 | } 679 | 680 | function reverse(parent, diff) { 681 | return diff( 682 | parent, 683 | [...parent.childNodes], 684 | Array.from(parent.childNodes).reverse(), 685 | 686 | ); 687 | } 688 | 689 | function clear(parent, diff) { 690 | return diff( 691 | parent, 692 | [...parent.childNodes], 693 | [], 694 | ); 695 | } 696 | 697 | function create1000(parent, diff) { 698 | const start = parent.childNodes.length; 699 | const childNodes = []; 700 | for (let i = 0; i < 1000; i++) 701 | childNodes.push(document.createTextNode(start + i)); 702 | return diff( 703 | parent, 704 | [...parent.childNodes], 705 | childNodes, 706 | ); 707 | } 708 | 709 | function append1000(parent, diff) { 710 | const start = parent.childNodes.length - 1; 711 | const childNodes = [...parent.childNodes]; 712 | for (let i = 0; i < 1000; i++) 713 | childNodes.push(document.createTextNode(start + i)); 714 | return diff( 715 | parent, 716 | [...parent.childNodes], 717 | childNodes, 718 | ); 719 | } 720 | 721 | function create10000(parent, diff) { 722 | const childNodes = []; 723 | for (let i = 0; i < 10000; i++) 724 | childNodes.push(document.createTextNode(i)); 725 | return diff( 726 | parent, 727 | [...parent.childNodes], 728 | childNodes, 729 | 730 | ); 731 | } 732 | 733 | function swapRows(parent, diff) { 734 | const childNodes = [...parent.childNodes]; 735 | const $1 = childNodes[1]; 736 | childNodes[1] = childNodes[998]; 737 | childNodes[998] = $1; 738 | return diff( 739 | parent, 740 | [...parent.childNodes], 741 | childNodes, 742 | 743 | ); 744 | } 745 | 746 | function updateEach10thRow(parent, diff) { 747 | const childNodes = [...parent.childNodes]; 748 | for (let i = 0; i < childNodes.length; i += 10) 749 | childNodes[i] = document.createTextNode(i + '!'); 750 | return diff( 751 | parent, 752 | [...parent.childNodes], 753 | childNodes, 754 | 755 | ); 756 | } 757 | --------------------------------------------------------------------------------