├── .editorconfig ├── .gitignore ├── .jshintrc ├── README.md ├── dist ├── all.js ├── all.min.js ├── vd.js └── vd.min.js ├── gulpfile.js ├── index.js ├── lib └── operate.js ├── package.json ├── patch.js ├── q-vd.js ├── render.js ├── test ├── cases │ └── test.js ├── index.html ├── mocha.opts └── test.js ├── vd.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .monitor 3 | .*.swp 4 | .nodemonignore 5 | releases 6 | *.log 7 | *.err 8 | fleet.json 9 | public/browserify 10 | bin/*.json 11 | .bin 12 | build 13 | compile 14 | .lock-wscript 15 | coverage 16 | node_modules 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "bitwise": false, 4 | "camelcase": true, 5 | "curly": false, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "forin": false, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": "nofunc", 12 | "newcap": false, 13 | "noarg": true, 14 | "nonew": true, 15 | "plusplus": false, 16 | "proto": true, 17 | "quotmark": false, 18 | "regexp": false, 19 | "undef": true, 20 | "unused": true, 21 | "strict": false, 22 | "trailing": true, 23 | "noempty": true, 24 | "maxdepth": 5, 25 | "maxparams": 5, 26 | "globals": { 27 | "console": true, 28 | "Buffer": true, 29 | "setTimeout": true, 30 | "clearTimeout": true, 31 | "setInterval": true, 32 | "clearInterval": true, 33 | "require": false, 34 | "module": false, 35 | "exports": true, 36 | "global": false, 37 | "process": true, 38 | "__dirname": false, 39 | "__filename": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | qvd 2 | ==== 3 | 4 | > 简化版Virtual DOM,用于Mobile页面渲染。 5 | 6 | ### Example 7 | 8 | ```javascript 9 | var h = require('qvd').h 10 | , diff = require('qvd').diff; 11 | 12 | var a = h('div', { style: { textAlign: 'center' }, [h('text', 'hello')] }) 13 | , b = h('div', { style: { borer: '1px' } }); 14 | 15 | diff(a, b); 16 | ``` 17 | 18 | ### API 19 | 20 | ```javascript 21 | /** 22 | * h(tagName, properties, children) 23 | * 创建一个节点 24 | * @param {String} tagName tag名 25 | * @param {Object} properties 属性对象 26 | * @param {Array} children 子节点数组 27 | */ 28 | h('div', { style: { border: '1px' } }, [h('p')]); 29 | ``` 30 | 31 | ```javascript 32 | /** 33 | * h('text', text) 34 | * 创建文字节点 35 | * @param {String} text 文字节点内容 36 | */ 37 | h('text', 'hello world'); 38 | ``` 39 | 40 | ```javascript 41 | /** 42 | * render(vd) 43 | * 得到生成的html片段 44 | * @param {VD} vd 45 | * @returns {String} 46 | */ 47 | //

48 | render(h('div', { style: { border: '1px' } }, [h('p')])) 49 | ``` 50 | 51 | ```javasript 52 | /** 53 | * diff(a, b) 54 | * @param {VD} a 目前状态的a虚拟节点 55 | * @param {VD} b 要变成状态的b虚拟节点 56 | * @returns {Array} patches 57 | */ 58 | diff(a, b); 59 | ``` 60 | 61 | ```javascript 62 | /** 63 | * patch(patches, container) 64 | * @param {Array} patches diff方法返回的值 65 | * @param {Node} container a 虚拟节点的容器 66 | */ 67 | // 节点变换,从a到b 68 | patch(patches, container); 69 | ``` 70 | 71 | ### License 72 | 73 | Copyright (c) 2014 Matt-Esch. 74 | 75 | Copyright (c) 2015 Daniel Yang. 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy 78 | of this software and associated documentation files (the "Software"), to deal 79 | in the Software without restriction, including without limitation the rights 80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 81 | copies of the Software, and to permit persons to whom the Software is 82 | furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in 85 | all copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 93 | THE SOFTWARE. 94 | -------------------------------------------------------------------------------- /dist/all.js: -------------------------------------------------------------------------------- 1 | define("vd", [], function() { return /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports, __webpack_require__) { 46 | 47 | var vd = __webpack_require__(1) 48 | , render = __webpack_require__(2) 49 | , patch = __webpack_require__(3); 50 | 51 | module.exports = { 52 | diff: vd.diff, 53 | h: vd.h, 54 | render: render, 55 | patch: patch 56 | }; 57 | 58 | /***/ }, 59 | /* 1 */ 60 | /***/ function(module, exports, __webpack_require__) { 61 | 62 | var OPERATE = __webpack_require__(4); 63 | 64 | function isText(node) { 65 | return node.tag === 'text'; 66 | } 67 | 68 | function isEle(node) { 69 | return node.tag !== 'text'; 70 | } 71 | 72 | function isObject(obj) { 73 | return typeof obj === 'object'; 74 | } 75 | 76 | function remove(arr, index, key) { 77 | arr.splice(index, 1); 78 | 79 | return { 80 | from: index, 81 | key: key 82 | }; 83 | } 84 | 85 | function walk(a, b, patches, index) { 86 | patches[index] = apply = patches[index] || []; 87 | // no need to walk 88 | if (b == null) { 89 | apply.push({ 90 | from: a, 91 | operate: OPERATE.REMOVE 92 | }); 93 | // a and b is element 94 | } else if(isEle(a) && isEle(b)) { 95 | if (a.tag === b.tag) { 96 | var diff = diffProps(a.props, b.props); 97 | if (diff) { 98 | apply.push({ 99 | diff: diff, 100 | from: a, 101 | operate: OPERATE.PROPS 102 | }); 103 | } 104 | diffChildren(a, b, patches, apply, index); 105 | } else { 106 | apply.push({ 107 | from: b, 108 | to: a, 109 | operate: OPERATE.REPLACE 110 | }); 111 | } 112 | // at least one is text 113 | } else { 114 | if ( 115 | isEle(a) || isEle(b) 116 | || a.text !== b.text 117 | ) { 118 | apply.push({ 119 | from: b, 120 | to: a, 121 | operate: OPERATE.REPLACE 122 | }); 123 | } 124 | } 125 | } 126 | 127 | function diffProps(a, b, apply) { 128 | var key, aVal, bVal, diff; 129 | 130 | for (key in a) { 131 | if (!(key in b)) { 132 | diff = diff || {}; 133 | diff[key] = undefined; 134 | continue; 135 | } 136 | 137 | aVal = a[key]; 138 | bVal = b[key]; 139 | 140 | if (aVal === bVal) { 141 | continue; 142 | } else if (isObject(aVal) && isObject(bVal)) { 143 | var objectDiff = diffProps(aVal, bVal); 144 | if (objectDiff) { 145 | diff = diff || {}; 146 | diff[key] = objectDiff; 147 | } 148 | } else { 149 | diff = diff || {}; 150 | diff[key] = bVal; 151 | } 152 | } 153 | 154 | for (key in b) { 155 | if (!(key in a)) { 156 | diff = diff || {}; 157 | diff[key] = b[key]; 158 | } 159 | } 160 | 161 | return diff; 162 | } 163 | 164 | function keyIndex(children) { 165 | var keys = {} 166 | , free = [] 167 | , length = children.length 168 | , i = 0 169 | , child; 170 | 171 | for (; i < length; i++) { 172 | child = children[i]; 173 | 174 | if (child.key) { 175 | keys[child.key] = i; 176 | } else { 177 | free.push(i); 178 | } 179 | } 180 | 181 | return { 182 | keys: keys, // A hash of key name to index 183 | free: free // An array of unkeyed item indices 184 | }; 185 | } 186 | 187 | function reorder(aChildren, bChildren) { 188 | // O(M) time, O(M) memory 189 | var bChildIndex = keyIndex(bChildren) 190 | , bKeys = bChildIndex.keys 191 | , bFree = bChildIndex.free; 192 | 193 | if (bFree.length === bChildren.length) { 194 | return { 195 | children: bChildren, 196 | moves: null 197 | }; 198 | } 199 | 200 | // O(N) time, O(N) memory 201 | var aChildIndex = keyIndex(aChildren) 202 | , aKeys = aChildIndex.keys 203 | , aFree = aChildIndex.free; 204 | 205 | if (aFree.length === aChildren.length) { 206 | return { 207 | children: bChildren, 208 | moves: null 209 | }; 210 | } 211 | 212 | // O(MAX(N, M)) memory 213 | var newChildren = [] 214 | , freeIndex = 0 215 | , freeCount = bFree.length 216 | , deletedItems = 0 217 | , aItem 218 | , itemIndex 219 | , i = 0 220 | , l = aChildren.length; 221 | 222 | // Iterate through a and match a node in b 223 | // O(N) time, 224 | for (; i < l; i++) { 225 | aItem = aChildren[i]; 226 | 227 | if (aItem.key) { 228 | if (bKeys.hasOwnProperty(aItem.key)) { 229 | // Match up the old keys 230 | itemIndex = bKeys[aItem.key]; 231 | newChildren.push(bChildren[itemIndex]); 232 | } else { 233 | // Remove old keyed items 234 | itemIndex = i - deletedItems++; 235 | newChildren.push(null); 236 | } 237 | } else { 238 | // Match the item in a with the next free item in b 239 | if (freeIndex < freeCount) { 240 | itemIndex = bFree[freeIndex++]; 241 | newChildren.push(bChildren[itemIndex]); 242 | } else { 243 | // There are no free items in b to match with 244 | // the free items in a, so the extra free nodes 245 | // are deleted. 246 | itemIndex = i - deletedItems++; 247 | newChildren.push(null); 248 | } 249 | } 250 | } 251 | 252 | var lastFreeIndex = freeIndex >= bFree.length ? 253 | bChildren.length : 254 | bFree[freeIndex] 255 | , j = 0 256 | , newItem; 257 | 258 | l = bChildren.length; 259 | 260 | // Iterate through b and append any new keys 261 | // O(M) time 262 | for (; j < l; j++) { 263 | newItem = bChildren[j]; 264 | 265 | if (newItem.key) { 266 | if (!aKeys.hasOwnProperty(newItem.key)) { 267 | // Add any new keyed items 268 | // We are adding new items to the end and then sorting them 269 | // in place. In future we should insert new items in place. 270 | newChildren.push(newItem); 271 | } 272 | } else if (j >= lastFreeIndex) { 273 | // Add any leftover non-keyed items 274 | newChildren.push(newItem); 275 | } 276 | } 277 | 278 | var simulate = newChildren.slice() 279 | , simulateIndex = 0 280 | , removes = [] 281 | , inserts = [] 282 | , simulateItem 283 | , wantedItem 284 | , k = 0; 285 | 286 | for (; k < l;) { 287 | wantedItem = bChildren[k]; 288 | simulateItem = simulate[simulateIndex]; 289 | 290 | // remove items 291 | while (simulateItem === null && simulate.length) { 292 | removes.push(remove(simulate, simulateIndex, null)); 293 | simulateItem = simulate[simulateIndex]; 294 | } 295 | 296 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 297 | // if we need a key in this position... 298 | if (wantedItem.key) { 299 | if (simulateItem && simulateItem.key) { 300 | // if an insert doesn't put this key in place, it needs to move 301 | if (bKeys[simulateItem.key] !== k + 1) { 302 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 303 | simulateItem = simulate[simulateIndex]; 304 | // if the remove didn't put the wanted item in place, we need to insert it 305 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 306 | inserts.push({key: wantedItem.key, to: k}); 307 | // items are matching, so skip ahead 308 | } else { 309 | simulateIndex++ 310 | } 311 | } else { 312 | inserts.push({key: wantedItem.key, to: k}) 313 | } 314 | } else { 315 | inserts.push({key: wantedItem.key, to: k}) 316 | } 317 | k++ 318 | // a key in simulate has no matching wanted key, remove it 319 | } else if (simulateItem && simulateItem.key) { 320 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 321 | } 322 | } else { 323 | simulateIndex++; 324 | k++; 325 | } 326 | } 327 | 328 | // remove all the remaining nodes from simulate 329 | while(simulateIndex < simulate.length) { 330 | simulateItem = simulate[simulateIndex]; 331 | removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key)); 332 | } 333 | 334 | // If the only moves we have are deletes then we can just 335 | // let the delete patch remove these items. 336 | if (removes.length === deletedItems && !inserts.length) { 337 | return { 338 | children: newChildren, 339 | moves: null 340 | }; 341 | } 342 | 343 | return { 344 | children: newChildren, 345 | moves: { 346 | removes: removes, 347 | inserts: inserts 348 | } 349 | }; 350 | } 351 | 352 | function diffChildren(a, b, patches, apply, index) { 353 | var aChildren = a.children 354 | , orderedSet = reorder(aChildren, b.children) 355 | , bChildren = orderedSet.children 356 | , aLen = aChildren.length 357 | , bLen = bChildren.length 358 | , len = aLen > bLen ? aLen : bLen 359 | , i = 0 360 | , leftNode 361 | , rightNode; 362 | 363 | for (var i = 0; i < len; i++) { 364 | leftNode = aChildren[i]; 365 | rightNode = bChildren[i]; 366 | index += 1; 367 | 368 | if (!leftNode) { 369 | if (rightNode) { 370 | apply.push({ 371 | from: rightNode, 372 | to: a, 373 | operate: OPERATE.INSERT 374 | }); 375 | } 376 | } else { 377 | walk(leftNode, rightNode, patches, index) 378 | } 379 | } 380 | 381 | if (orderedSet.moves) { 382 | apply.push({ 383 | operate: OPERATE.ORDER, 384 | from: orderedSet.moves, 385 | to: a 386 | }); 387 | } 388 | 389 | return apply; 390 | } 391 | 392 | /** 393 | * h(tagName, props, children) 394 | * h('text', 'hello') 395 | * { 396 | * tag: 'div', 397 | * children: [], 398 | * props: {} 399 | * } 400 | */ 401 | function h(tagName, props, children) { 402 | props = props || {}; 403 | children = children || []; 404 | var key; 405 | if (tagName === 'text') { 406 | return { 407 | tag: tagName, 408 | text: props 409 | }; 410 | } else { 411 | if ('key' in props) { 412 | key = props.key; 413 | delete props['key']; 414 | } 415 | for (var i = 0, l = children.length; i < l; i++) { 416 | // text 417 | typeof children[i] === 'string' && 418 | (children[i] = h('text', children[i])); 419 | } 420 | return { 421 | tag: tagName, 422 | props: props, 423 | children: children, 424 | key: key 425 | }; 426 | } 427 | } 428 | 429 | /** 430 | * diff 431 | * @param {VD} a 432 | * @param {VD} b 433 | */ 434 | function diff(a, b) { 435 | var patches = {}, res = []; 436 | res.a = a; 437 | walk(a, b, patches, 0); 438 | for (var i = 0; patches[i]; i++) { 439 | patches[i].length && 440 | res.push(patches[i]) 441 | } 442 | return res; 443 | } 444 | 445 | module.exports = { 446 | h: h, 447 | diff: diff 448 | }; 449 | 450 | /***/ }, 451 | /* 2 */ 452 | /***/ function(module, exports, __webpack_require__) { 453 | 454 | var singleTag = { 455 | area: true, 456 | base: true, 457 | basefont: true, 458 | br: true, 459 | col: true, 460 | command: true, 461 | embed: true, 462 | frame: true, 463 | hr: true, 464 | img: true, 465 | input: true, 466 | isindex: true, 467 | keygen: true, 468 | link: true, 469 | meta: true, 470 | param: true, 471 | source: true, 472 | track: true, 473 | wbr: true, 474 | } 475 | , booleanAttributes = { 476 | allowfullscreen: true, 477 | async: true, 478 | autofocus: true, 479 | autoplay: true, 480 | checked: true, 481 | controls: true, 482 | default: true, 483 | defer: true, 484 | disabled: true, 485 | hidden: true, 486 | ismap: true, 487 | loop: true, 488 | multiple: true, 489 | muted: true, 490 | open: true, 491 | readonly: true, 492 | required: true, 493 | reversed: true, 494 | scoped: true, 495 | seamless: true, 496 | selected: true, 497 | typemustmatch: true 498 | } 499 | , REG_CAP = /[A-Z]/g; 500 | 501 | function isEle(node) { 502 | return node.tag !== 'text'; 503 | } 504 | 505 | function formatStyle(style) { 506 | var key, value 507 | , output = 'style="'; 508 | 509 | for (key in style) { 510 | value = style[key]; 511 | 512 | key = key.replace(REG_CAP, function (cap) { 513 | return '-' + cap.toLowerCase(); 514 | }); 515 | 516 | output += key + ': ' + value + ';'; 517 | } 518 | 519 | return output + '"'; 520 | } 521 | 522 | function formatProps(props) { 523 | if (!props) return; 524 | 525 | var output = '' 526 | , key 527 | , value; 528 | 529 | for (key in props) { 530 | value = props[key]; 531 | 532 | if (output) { 533 | output += ' '; 534 | } 535 | 536 | if (!value && booleanAttributes[key]) { 537 | output += key; 538 | } else if (key === 'style') { 539 | output += formatStyle(value); 540 | } else { 541 | output += key + '="' + value + '"'; 542 | } 543 | } 544 | return output; 545 | } 546 | 547 | function renderTag(vd) { 548 | var tag = '<'+ vd.tag 549 | , props = formatProps(vd.props); 550 | 551 | if (props) { 552 | tag += ' ' + props; 553 | } 554 | 555 | tag += '>'; 556 | 557 | if (vd.children) { 558 | tag += render(vd.children); 559 | } 560 | 561 | if (!singleTag[vd.tag]) { 562 | tag += ''; 563 | } 564 | 565 | return tag; 566 | } 567 | 568 | function renderText(vd) { 569 | return vd.text; 570 | } 571 | 572 | 573 | function render(vds) { 574 | !Array.isArray(vds) && 575 | (vds = [vds]); 576 | 577 | var output = '' 578 | , l = vds.length 579 | , vd; 580 | 581 | for (var i = 0; i < l; i++) { 582 | vd = vds[i] 583 | 584 | if (isEle(vd)) { 585 | output += renderTag(vd); 586 | } else { 587 | output += renderText(vd); 588 | } 589 | } 590 | return output; 591 | } 592 | 593 | module.exports = render; 594 | 595 | /***/ }, 596 | /* 3 */ 597 | /***/ function(module, exports, __webpack_require__) { 598 | 599 | // TODO in the rough 600 | var OPERATE = __webpack_require__(4) 601 | , DEAL = {}; 602 | 603 | function isText(vd) { 604 | return vd.tag === 'text'; 605 | } 606 | 607 | function getVDByKey(vds, key, cache) { 608 | cache = cache || {}; 609 | if (cache[key]) return cache[key]; 610 | for (var i = 0, l = vds.length; i < l; i++) { 611 | if (vds[i].key === key) return (cache[key] = vds[i]); 612 | } 613 | } 614 | 615 | function find(vd, vds) { 616 | var i = vds.indexOf(vd) 617 | , res, tmp; 618 | if (!(~i)) { 619 | vds.every(function (item, j) { 620 | if (!item.children || !item.children.length) return true; 621 | res = find(vd, item.children); 622 | if (res) { 623 | tmp = res; 624 | res = [j]; 625 | [].push.apply(res, tmp); 626 | return false; 627 | } else { 628 | return true; 629 | } 630 | }); 631 | return res; 632 | } 633 | return [i]; 634 | } 635 | 636 | function getRef(vd, root, a) { 637 | var ref = vd.ref; 638 | if (ref) return ref; 639 | if (!root || !a) throw new Error('Should bind the reference node.'); 640 | var indexes = find(vd, [a]); 641 | if (!indexes) throw new Error('Couldn\'t find the reference node.'); 642 | ref = root.childNodes[0]; 643 | for (var i = 1, l = indexes.length; i < l; i++) { 644 | ref = ref.childNodes[indexes[i]]; 645 | } 646 | vd.ref = ref; 647 | return ref; 648 | } 649 | 650 | function setProps(to, from, node) { 651 | for (key in from) { 652 | // style 653 | if (key === 'style') { 654 | style = node.style; 655 | value = from[key]; 656 | if (!value) { 657 | style.cssText = ''; 658 | to.style = {}; 659 | } else { 660 | value = from[key]; 661 | for (i in value) { 662 | if (value[i] === undefined) { 663 | style[i] = ''; 664 | delete to.style[i]; 665 | } else { 666 | style[i] = value[i]; 667 | to.style[i] = value[i]; 668 | } 669 | } 670 | } 671 | // property 672 | } else if (key in node) { 673 | value = from[key]; 674 | type = typeof node[key]; 675 | if (value === undefined) { 676 | type === 'string' ? 677 | (node[key] = '') : 678 | (node[key] = null); 679 | delete to[key]; 680 | } else { 681 | node[key] = to[key] = value; 682 | } 683 | // attribute 684 | } else { 685 | value = from[key]; 686 | if (value === undefined) { 687 | node.removeAttribute(key); 688 | delete to[key]; 689 | } else { 690 | node.setAttribute(key, value); 691 | to[key] = value; 692 | } 693 | } 694 | } 695 | } 696 | 697 | function create(vds, parent) { 698 | !Array.isArray(vds) && (vds = [vds]); 699 | parent = parent || document.createDocumentFragment(); 700 | var node; 701 | vds.forEach(function (vd) { 702 | if (isText(vd)) { 703 | node = document.createTextNode(vd.text); 704 | } else { 705 | node = document.createElement(vd.tag); 706 | } 707 | parent.appendChild(node); 708 | vd.children && vd.children.length && 709 | create(vd.children, node); 710 | 711 | vd.props && 712 | setProps({ style: {} }, vd.props, node); 713 | }); 714 | return parent; 715 | } 716 | 717 | function splice(vd, a, replacement) { 718 | var indexes, i, l; 719 | indexes = find(vd, [a]); 720 | for (i = 1, l = indexes.length - 1; i < l; i++) { 721 | a = a.children[indexes[i]]; 722 | } 723 | replacement ? 724 | a.children.splice(indexes[l], 1, replacement) : 725 | a.children.splice(indexes[l], 1); 726 | } 727 | 728 | DEAL[OPERATE.REMOVE] = function (item, root, a) { 729 | var node = getRef(item.from, root, a); 730 | node.parentNode.removeChild(node); 731 | item.from.ref = null; 732 | splice(item.from, a); 733 | }; 734 | 735 | DEAL[OPERATE.INSERT] = function (item, root, a) { 736 | var node = create(item.from) 737 | , parent = getRef(item.to, root, a); 738 | parent.appendChild(node); 739 | item.to.children.push(item.from); 740 | }; 741 | 742 | DEAL[OPERATE.REPLACE] = function (item, root, a) { 743 | var node = create(item.from) 744 | , target = getRef(item.to, root, a) 745 | , indexes 746 | , i 747 | , l; 748 | target.parentNode.replaceChild(node, target); 749 | item.to.ref = null; 750 | splice(item.to, a, item.from); 751 | }; 752 | 753 | DEAL[OPERATE.ORDER] = function (item, root, a) { 754 | var to = item.to 755 | , from = item.from 756 | , children = to.children 757 | , parent = getRef(to, root, a) 758 | , grandpa = parent.parentNode 759 | , removes = from.removes 760 | , inserts = from.inserts 761 | , cache = {}; 762 | 763 | removes.forEach(function (order) { 764 | var vd = getVDByKey(children, order.key, cache) 765 | , node = getRef(vd, grandpa, to); 766 | parent.removeChild(node); 767 | splice(vd, to); 768 | }); 769 | inserts.forEach(function (order) { 770 | var vd = getVDByKey(children, order.key, cache) 771 | , node = getRef(vd, grandpa, to) 772 | , before = getRef(children[order.to], grandpa, to); 773 | parent.insertBefore(node, before); 774 | children.splice(order.to, 0, vd); 775 | }); 776 | cache = null; 777 | }; 778 | 779 | DEAL[OPERATE.PROPS] = function (item, root, a) { 780 | var props = item.from.props 781 | , node = getRef(item.from, root, a) 782 | , diff = item.diff 783 | , key, value, i, l, style, type; 784 | setProps(props, diff, node); 785 | }; 786 | 787 | function patch(patches, root) { 788 | var i = 0, l = patches.length 789 | , a = patches.a; 790 | root = root || a.hook; 791 | for (; i < l; i++) { 792 | patches[i].forEach(function (item) { 793 | DEAL[item.operate](item, root, a); 794 | }); 795 | } 796 | } 797 | 798 | module.exports = patch; 799 | 800 | /***/ }, 801 | /* 4 */ 802 | /***/ function(module, exports, __webpack_require__) { 803 | 804 | module.exports = { 805 | REMOVE: 1, 806 | INSERT: 2, 807 | REPLACE: 3, 808 | ORDER: 4, 809 | PROPS: 5 810 | }; 811 | 812 | /***/ } 813 | /******/ ])});; -------------------------------------------------------------------------------- /dist/all.min.js: -------------------------------------------------------------------------------- 1 | define("vd",[],function(){return function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}var t={};return r.m=e,r.c=t,r.p="",r(0)}([function(e,r,t){var n=t(1),o=t(2),l=t(3);e.exports={diff:n.diff,h:n.h,render:o,patch:l}},function(e,r,t){function n(e){return"text"!==e.tag}function o(e){return"object"==typeof e}function l(e,r,t){return e.splice(r,1),{from:r,key:t}}function i(e,r,t,o){if(t[o]=apply=t[o]||[],null==r)apply.push({from:e,operate:h.REMOVE});else if(n(e)&&n(r))if(e.tag===r.tag){var l=u(e.props,r.props);l&&apply.push({diff:l,from:e,operate:h.PROPS}),s(e,r,t,apply,o)}else apply.push({from:r,to:e,operate:h.REPLACE});else(n(e)||n(r)||e.text!==r.text)&&apply.push({from:r,to:e,operate:h.REPLACE})}function u(e,r){var t,n,l,i;for(t in e)if(t in r){if(n=e[t],l=r[t],n!==l)if(o(n)&&o(l)){var f=u(n,l);f&&(i=i||{},i[t]=f)}else i=i||{},i[t]=l}else i=i||{},i[t]=void 0;for(t in r)t in e||(i=i||{},i[t]=r[t]);return i}function f(e){for(var r,t={},n=[],o=e.length,l=0;o>l;l++)r=e[l],r.key?t[r.key]=l:n.push(l);return{keys:t,free:n}}function a(e,r){var t=f(r),n=t.keys,o=t.free;if(o.length===r.length)return{children:r,moves:null};var i=f(e),u=i.keys,a=i.free;if(a.length===e.length)return{children:r,moves:null};for(var s,c,p=[],h=0,y=o.length,d=0,v=0,k=e.length;k>v;v++)s=e[v],s.key?n.hasOwnProperty(s.key)?(c=n[s.key],p.push(r[c])):(c=v-d++,p.push(null)):y>h?(c=o[h++],p.push(r[c])):(c=v-d++,p.push(null));var m,g=h>=o.length?r.length:o[h],E=0;for(k=r.length;k>E;E++)m=r[E],m.key?u.hasOwnProperty(m.key)||p.push(m):E>=g&&p.push(m);for(var x,R,O=p.slice(),P=0,A=[],C=[],b=0;k>b;){for(R=r[b],x=O[P];null===x&&O.length;)A.push(l(O,P,null)),x=O[P];x&&x.key===R.key?(P++,b++):R.key?(x&&x.key&&n[x.key]!==b+1?(A.push(l(O,P,x.key)),x=O[P],x&&x.key===R.key?P++:C.push({key:R.key,to:b})):C.push({key:R.key,to:b}),b++):x&&x.key&&A.push(l(O,P,x.key))}for(;Py?p:y,v=0,v=0;d>v;v++)l=f[v],u=c[v],o+=1,l?i(l,u,t,o):u&&n.push({from:u,to:e,operate:h.INSERT});return s.moves&&n.push({operate:h.ORDER,from:s.moves,to:e}),n}function c(e,r,t){r=r||{},t=t||[];var n;if("text"===e)return{tag:e,text:r};"key"in r&&(n=r.key,delete r.key);for(var o=0,l=t.length;l>o;o++)"string"==typeof t[o]&&(t[o]=c("text",t[o]));return{tag:e,props:r,children:t,key:n}}function p(e,r){var t={},n=[];n.a=e,i(e,r,t,0);for(var o=0;t[o];o++)t[o].length&&n.push(t[o]);return n}var h=t(4);e.exports={h:c,diff:p}},function(e){function r(e){return"text"!==e.tag}function t(e){var r,t,n='style="';for(r in e)t=e[r],r=r.replace(a,function(e){return"-"+e.toLowerCase()}),n+=r+": "+t+";";return n+'"'}function n(e){if(e){var r,n,o="";for(r in e)n=e[r],o&&(o+=" "),o+=!n&&f[r]?r:"style"===r?t(n):r+'="'+n+'"';return o}}function o(e){var r="<"+e.tag,t=n(e.props);return t&&(r+=" "+t),r+=">",e.children&&(r+=i(e.children)),u[e.tag]||(r+=""),r}function l(e){return e.text}function i(e){!Array.isArray(e)&&(e=[e]);for(var t,n="",i=e.length,u=0;i>u;u++)t=e[u],n+=r(t)?o(t):l(t);return n}var u={area:!0,base:!0,basefont:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,isindex:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0},f={allowfullscreen:!0,async:!0,autofocus:!0,autoplay:!0,checked:!0,controls:!0,"default":!0,defer:!0,disabled:!0,hidden:!0,ismap:!0,loop:!0,multiple:!0,muted:!0,open:!0,readonly:!0,required:!0,reversed:!0,scoped:!0,seamless:!0,selected:!0,typemustmatch:!0},a=/[A-Z]/g;e.exports=i},function(e,r,t){function n(e){return"text"===e.tag}function o(e,r,t){if(t=t||{},t[r])return t[r];for(var n=0,o=e.length;o>n;n++)if(e[n].key===r)return t[r]=e[n]}function l(e,r){var t,n,o=r.indexOf(e);return~o?[o]:(r.every(function(r,o){return r.children&&r.children.length?(t=l(e,r.children),t?(n=t,t=[o],[].push.apply(t,n),!1):!0):!0}),t)}function u(e,r,t){var n=e.ref;if(n)return n;if(!r||!t)throw new Error("Should bind the reference node.");var o=l(e,[t]);if(!o)throw new Error("Couldn't find the reference node.");n=r.childNodes[0];for(var i=1,u=o.length;u>i;i++)n=n.childNodes[o[i]];return e.ref=n,n}function f(e,r,t){for(key in r)if("style"===key)if(style=t.style,value=r[key],value){value=r[key];for(i in value)void 0===value[i]?(style[i]="",delete e.style[i]):(style[i]=value[i],e.style[i]=value[i])}else style.cssText="",e.style={};else key in t?(value=r[key],type=typeof t[key],void 0===value?(t[key]="string"===type?"":null,delete e[key]):t[key]=e[key]=value):(value=r[key],void 0===value?(t.removeAttribute(key),delete e[key]):(t.setAttribute(key,value),e[key]=value))}function a(e,r){!Array.isArray(e)&&(e=[e]),r=r||document.createDocumentFragment();var t;return e.forEach(function(e){t=n(e)?document.createTextNode(e.text):document.createElement(e.tag),r.appendChild(t),e.children&&e.children.length&&a(e.children,t),e.props&&f({style:{}},e.props,t)}),r}function s(e,r,t){var n,o,i;for(n=l(e,[r]),o=1,i=n.length-1;i>o;o++)r=r.children[n[o]];t?r.children.splice(n[i],1,t):r.children.splice(n[i],1)}function c(e,r){var t=0,n=e.length,o=e.a;for(r=r||o.hook;n>t;t++)e[t].forEach(function(e){h[e.operate](e,r,o)})}var p=t(4),h={};h[p.REMOVE]=function(e,r,t){var n=u(e.from,r,t);n.parentNode.removeChild(n),e.from.ref=null,s(e.from,t)},h[p.INSERT]=function(e,r,t){var n=a(e.from),o=u(e.to,r,t);o.appendChild(n),e.to.children.push(e.from)},h[p.REPLACE]=function(e,r,t){var n=a(e.from),o=u(e.to,r,t);o.parentNode.replaceChild(n,o),e.to.ref=null,s(e.to,t,e.from)},h[p.ORDER]=function(e,r,t){var n=e.to,l=e.from,i=n.children,f=u(n,r,t),a=f.parentNode,c=l.removes,p=l.inserts,h={};c.forEach(function(e){var r=o(i,e.key,h),t=u(r,a,n);f.removeChild(t),s(r,n)}),p.forEach(function(e){var r=o(i,e.key,h),t=u(r,a,n),l=u(i[e.to],a,n);f.insertBefore(t,l),i.splice(e.to,0,r)}),h=null},h[p.PROPS]=function(e,r,t){var n=e.from.props,o=u(e.from,r,t),l=e.diff;f(n,l,o)},e.exports=c},function(e){e.exports={REMOVE:1,INSERT:2,REPLACE:3,ORDER:4,PROPS:5}}])}); -------------------------------------------------------------------------------- /dist/vd.js: -------------------------------------------------------------------------------- 1 | define("vd", [], function() { return /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports, __webpack_require__) { 46 | 47 | var OPERATE = __webpack_require__(1); 48 | 49 | function isText(node) { 50 | return node.tag === 'text'; 51 | } 52 | 53 | function isEle(node) { 54 | return node.tag !== 'text'; 55 | } 56 | 57 | function isObject(obj) { 58 | return typeof obj === 'object'; 59 | } 60 | 61 | function remove(arr, index, key) { 62 | arr.splice(index, 1); 63 | 64 | return { 65 | from: index, 66 | key: key 67 | }; 68 | } 69 | 70 | function walk(a, b, patches, index) { 71 | patches[index] = apply = patches[index] || []; 72 | // no need to walk 73 | if (b == null) { 74 | apply.push({ 75 | from: a, 76 | operate: OPERATE.REMOVE 77 | }); 78 | // a and b is element 79 | } else if(isEle(a) && isEle(b)) { 80 | if (a.tag === b.tag) { 81 | var diff = diffProps(a.props, b.props); 82 | if (diff) { 83 | apply.push({ 84 | diff: diff, 85 | from: a, 86 | operate: OPERATE.PROPS 87 | }); 88 | } 89 | diffChildren(a, b, patches, apply, index); 90 | } else { 91 | apply.push({ 92 | from: b, 93 | to: a, 94 | operate: OPERATE.REPLACE 95 | }); 96 | } 97 | // at least one is text 98 | } else { 99 | if ( 100 | isEle(a) || isEle(b) 101 | || a.text !== b.text 102 | ) { 103 | apply.push({ 104 | from: b, 105 | to: a, 106 | operate: OPERATE.REPLACE 107 | }); 108 | } 109 | } 110 | } 111 | 112 | function diffProps(a, b, apply) { 113 | var key, aVal, bVal, diff; 114 | 115 | for (key in a) { 116 | if (!(key in b)) { 117 | diff = diff || {}; 118 | diff[key] = undefined; 119 | continue; 120 | } 121 | 122 | aVal = a[key]; 123 | bVal = b[key]; 124 | 125 | if (aVal === bVal) { 126 | continue; 127 | } else if (isObject(aVal) && isObject(bVal)) { 128 | var objectDiff = diffProps(aVal, bVal); 129 | if (objectDiff) { 130 | diff = diff || {}; 131 | diff[key] = objectDiff; 132 | } 133 | } else { 134 | diff = diff || {}; 135 | diff[key] = bVal; 136 | } 137 | } 138 | 139 | for (key in b) { 140 | if (!(key in a)) { 141 | diff = diff || {}; 142 | diff[key] = b[key]; 143 | } 144 | } 145 | 146 | return diff; 147 | } 148 | 149 | function keyIndex(children) { 150 | var keys = {} 151 | , free = [] 152 | , length = children.length 153 | , i = 0 154 | , child; 155 | 156 | for (; i < length; i++) { 157 | child = children[i]; 158 | 159 | if (child.key) { 160 | keys[child.key] = i; 161 | } else { 162 | free.push(i); 163 | } 164 | } 165 | 166 | return { 167 | keys: keys, // A hash of key name to index 168 | free: free // An array of unkeyed item indices 169 | }; 170 | } 171 | 172 | function reorder(aChildren, bChildren) { 173 | // O(M) time, O(M) memory 174 | var bChildIndex = keyIndex(bChildren) 175 | , bKeys = bChildIndex.keys 176 | , bFree = bChildIndex.free; 177 | 178 | if (bFree.length === bChildren.length) { 179 | return { 180 | children: bChildren, 181 | moves: null 182 | }; 183 | } 184 | 185 | // O(N) time, O(N) memory 186 | var aChildIndex = keyIndex(aChildren) 187 | , aKeys = aChildIndex.keys 188 | , aFree = aChildIndex.free; 189 | 190 | if (aFree.length === aChildren.length) { 191 | return { 192 | children: bChildren, 193 | moves: null 194 | }; 195 | } 196 | 197 | // O(MAX(N, M)) memory 198 | var newChildren = [] 199 | , freeIndex = 0 200 | , freeCount = bFree.length 201 | , deletedItems = 0 202 | , aItem 203 | , itemIndex 204 | , i = 0 205 | , l = aChildren.length; 206 | 207 | // Iterate through a and match a node in b 208 | // O(N) time, 209 | for (; i < l; i++) { 210 | aItem = aChildren[i]; 211 | 212 | if (aItem.key) { 213 | if (bKeys.hasOwnProperty(aItem.key)) { 214 | // Match up the old keys 215 | itemIndex = bKeys[aItem.key]; 216 | newChildren.push(bChildren[itemIndex]); 217 | } else { 218 | // Remove old keyed items 219 | itemIndex = i - deletedItems++; 220 | newChildren.push(null); 221 | } 222 | } else { 223 | // Match the item in a with the next free item in b 224 | if (freeIndex < freeCount) { 225 | itemIndex = bFree[freeIndex++]; 226 | newChildren.push(bChildren[itemIndex]); 227 | } else { 228 | // There are no free items in b to match with 229 | // the free items in a, so the extra free nodes 230 | // are deleted. 231 | itemIndex = i - deletedItems++; 232 | newChildren.push(null); 233 | } 234 | } 235 | } 236 | 237 | var lastFreeIndex = freeIndex >= bFree.length ? 238 | bChildren.length : 239 | bFree[freeIndex] 240 | , j = 0 241 | , newItem; 242 | 243 | l = bChildren.length; 244 | 245 | // Iterate through b and append any new keys 246 | // O(M) time 247 | for (; j < l; j++) { 248 | newItem = bChildren[j]; 249 | 250 | if (newItem.key) { 251 | if (!aKeys.hasOwnProperty(newItem.key)) { 252 | // Add any new keyed items 253 | // We are adding new items to the end and then sorting them 254 | // in place. In future we should insert new items in place. 255 | newChildren.push(newItem); 256 | } 257 | } else if (j >= lastFreeIndex) { 258 | // Add any leftover non-keyed items 259 | newChildren.push(newItem); 260 | } 261 | } 262 | 263 | var simulate = newChildren.slice() 264 | , simulateIndex = 0 265 | , removes = [] 266 | , inserts = [] 267 | , simulateItem 268 | , wantedItem 269 | , k = 0; 270 | 271 | for (; k < l;) { 272 | wantedItem = bChildren[k]; 273 | simulateItem = simulate[simulateIndex]; 274 | 275 | // remove items 276 | while (simulateItem === null && simulate.length) { 277 | removes.push(remove(simulate, simulateIndex, null)); 278 | simulateItem = simulate[simulateIndex]; 279 | } 280 | 281 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 282 | // if we need a key in this position... 283 | if (wantedItem.key) { 284 | if (simulateItem && simulateItem.key) { 285 | // if an insert doesn't put this key in place, it needs to move 286 | if (bKeys[simulateItem.key] !== k + 1) { 287 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 288 | simulateItem = simulate[simulateIndex]; 289 | // if the remove didn't put the wanted item in place, we need to insert it 290 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 291 | inserts.push({key: wantedItem.key, to: k}); 292 | // items are matching, so skip ahead 293 | } else { 294 | simulateIndex++ 295 | } 296 | } else { 297 | inserts.push({key: wantedItem.key, to: k}) 298 | } 299 | } else { 300 | inserts.push({key: wantedItem.key, to: k}) 301 | } 302 | k++ 303 | // a key in simulate has no matching wanted key, remove it 304 | } else if (simulateItem && simulateItem.key) { 305 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 306 | } 307 | } else { 308 | simulateIndex++; 309 | k++; 310 | } 311 | } 312 | 313 | // remove all the remaining nodes from simulate 314 | while(simulateIndex < simulate.length) { 315 | simulateItem = simulate[simulateIndex]; 316 | removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key)); 317 | } 318 | 319 | // If the only moves we have are deletes then we can just 320 | // let the delete patch remove these items. 321 | if (removes.length === deletedItems && !inserts.length) { 322 | return { 323 | children: newChildren, 324 | moves: null 325 | }; 326 | } 327 | 328 | return { 329 | children: newChildren, 330 | moves: { 331 | removes: removes, 332 | inserts: inserts 333 | } 334 | }; 335 | } 336 | 337 | function diffChildren(a, b, patches, apply, index) { 338 | var aChildren = a.children 339 | , orderedSet = reorder(aChildren, b.children) 340 | , bChildren = orderedSet.children 341 | , aLen = aChildren.length 342 | , bLen = bChildren.length 343 | , len = aLen > bLen ? aLen : bLen 344 | , i = 0 345 | , leftNode 346 | , rightNode; 347 | 348 | for (var i = 0; i < len; i++) { 349 | leftNode = aChildren[i]; 350 | rightNode = bChildren[i]; 351 | index += 1; 352 | 353 | if (!leftNode) { 354 | if (rightNode) { 355 | apply.push({ 356 | from: rightNode, 357 | to: a, 358 | operate: OPERATE.INSERT 359 | }); 360 | } 361 | } else { 362 | walk(leftNode, rightNode, patches, index) 363 | } 364 | } 365 | 366 | if (orderedSet.moves) { 367 | apply.push({ 368 | operate: OPERATE.ORDER, 369 | from: orderedSet.moves, 370 | to: a 371 | }); 372 | } 373 | 374 | return apply; 375 | } 376 | 377 | /** 378 | * h(tagName, props, children) 379 | * h('text', 'hello') 380 | * { 381 | * tag: 'div', 382 | * children: [], 383 | * props: {} 384 | * } 385 | */ 386 | function h(tagName, props, children) { 387 | props = props || {}; 388 | children = children || []; 389 | var key; 390 | if (tagName === 'text') { 391 | return { 392 | tag: tagName, 393 | text: props 394 | }; 395 | } else { 396 | if ('key' in props) { 397 | key = props.key; 398 | delete props['key']; 399 | } 400 | for (var i = 0, l = children.length; i < l; i++) { 401 | // text 402 | typeof children[i] === 'string' && 403 | (children[i] = h('text', children[i])); 404 | } 405 | return { 406 | tag: tagName, 407 | props: props, 408 | children: children, 409 | key: key 410 | }; 411 | } 412 | } 413 | 414 | /** 415 | * diff 416 | * @param {VD} a 417 | * @param {VD} b 418 | */ 419 | function diff(a, b) { 420 | var patches = {}, res = []; 421 | res.a = a; 422 | walk(a, b, patches, 0); 423 | for (var i = 0; patches[i]; i++) { 424 | patches[i].length && 425 | res.push(patches[i]) 426 | } 427 | return res; 428 | } 429 | 430 | module.exports = { 431 | h: h, 432 | diff: diff 433 | }; 434 | 435 | /***/ }, 436 | /* 1 */ 437 | /***/ function(module, exports, __webpack_require__) { 438 | 439 | module.exports = { 440 | REMOVE: 1, 441 | INSERT: 2, 442 | REPLACE: 3, 443 | ORDER: 4, 444 | PROPS: 5 445 | }; 446 | 447 | /***/ } 448 | /******/ ])});; -------------------------------------------------------------------------------- /dist/vd.min.js: -------------------------------------------------------------------------------- 1 | define("vd",[],function(){return function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}var t={};return r.m=e,r.c=t,r.p="",r(0)}([function(e,r,t){function n(e){return"text"!==e.tag}function o(e){return"object"==typeof e}function l(e,r,t){return e.splice(r,1),{from:r,key:t}}function u(e,r,t,o){if(t[o]=apply=t[o]||[],null==r)apply.push({from:e,operate:y.REMOVE});else if(n(e)&&n(r))if(e.tag===r.tag){var l=p(e.props,r.props);l&&apply.push({diff:l,from:e,operate:y.PROPS}),h(e,r,t,apply,o)}else apply.push({from:r,to:e,operate:y.REPLACE});else(n(e)||n(r)||e.text!==r.text)&&apply.push({from:r,to:e,operate:y.REPLACE})}function p(e,r){var t,n,l,u;for(t in e)if(t in r){if(n=e[t],l=r[t],n!==l)if(o(n)&&o(l)){var f=p(n,l);f&&(u=u||{},u[t]=f)}else u=u||{},u[t]=l}else u=u||{},u[t]=void 0;for(t in r)t in e||(u=u||{},u[t]=r[t]);return u}function f(e){for(var r,t={},n=[],o=e.length,l=0;o>l;l++)r=e[l],r.key?t[r.key]=l:n.push(l);return{keys:t,free:n}}function s(e,r){var t=f(r),n=t.keys,o=t.free;if(o.length===r.length)return{children:r,moves:null};var u=f(e),p=u.keys,s=u.free;if(s.length===e.length)return{children:r,moves:null};for(var h,i,a=[],y=0,k=o.length,c=0,v=0,g=e.length;g>v;v++)h=e[v],h.key?n.hasOwnProperty(h.key)?(i=n[h.key],a.push(r[i])):(i=v-c++,a.push(null)):k>y?(i=o[y++],a.push(r[i])):(i=v-c++,a.push(null));var d,m=y>=o.length?r.length:o[y],E=0;for(g=r.length;g>E;E++)d=r[E],d.key?p.hasOwnProperty(d.key)||a.push(d):E>=m&&a.push(d);for(var x,R,P=a.slice(),O=0,S=[],A=[],C=0;g>C;){for(R=r[C],x=P[O];null===x&&P.length;)S.push(l(P,O,null)),x=P[O];x&&x.key===R.key?(O++,C++):R.key?(x&&x.key&&n[x.key]!==C+1?(S.push(l(P,O,x.key)),x=P[O],x&&x.key===R.key?O++:A.push({key:R.key,to:C})):A.push({key:R.key,to:C}),C++):x&&x.key&&S.push(l(P,O,x.key))}for(;Ok?a:k,v=0,v=0;c>v;v++)l=f[v],p=i[v],o+=1,l?u(l,p,t,o):p&&n.push({from:p,to:e,operate:y.INSERT});return h.moves&&n.push({operate:y.ORDER,from:h.moves,to:e}),n}function i(e,r,t){r=r||{},t=t||[];var n;if("text"===e)return{tag:e,text:r};"key"in r&&(n=r.key,delete r.key);for(var o=0,l=t.length;l>o;o++)"string"==typeof t[o]&&(t[o]=i("text",t[o]));return{tag:e,props:r,children:t,key:n}}function a(e,r){var t={},n=[];n.a=e,u(e,r,t,0);for(var o=0;t[o];o++)t[o].length&&n.push(t[o]);return n}var y=t(1);e.exports={h:i,diff:a}},function(e){e.exports={REMOVE:1,INSERT:2,REPLACE:3,ORDER:4,PROPS:5}}])}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | , map = require('map-stream') 3 | , maxmin = require('maxmin') 4 | , uglify = require('gulp-uglify') 5 | , webpack = require('gulp-webpack') 6 | , config = require('./webpack.config'); 7 | 8 | function Size(name) { 9 | this._name = name; 10 | this._max = undefined; 11 | this._min = undefined; 12 | } 13 | Size.prototype.max = function () { 14 | var self = this; 15 | return map(function (file, cb) { 16 | self._max = file.contents; 17 | cb(null, file); 18 | }); 19 | }; 20 | Size.prototype.min = function (rename) { 21 | var self = this; 22 | return map(function (file, cb) { 23 | self._min = file.contents; 24 | rename && 25 | (file.path = rename(file.path)); 26 | cb(null, file); 27 | }); 28 | }; 29 | Size.prototype.print = function () { 30 | var self = this; 31 | setTimeout(function () { 32 | console.log(self._name, maxmin(self._max, self._min, true)); 33 | }, 0); 34 | } 35 | 36 | var size = new Size('vd.js') 37 | , allSize = new Size('all.js'); 38 | 39 | gulp.task('vd-build', function (done) { 40 | gulp.src(['vd.js']) 41 | .pipe(webpack(config.vd)) 42 | .pipe(size.max()) 43 | .pipe(gulp.dest('./dist')) 44 | .on('end', function () { 45 | done(); 46 | }); 47 | }); 48 | 49 | gulp.task('vd-uglify', ['vd-build'], function (done) { 50 | gulp.src(['dist/vd.js']) 51 | .pipe(uglify({ 52 | preserveComments: 'some' 53 | })) 54 | .pipe(size.min(function (path) { 55 | return path.replace(/\.js$/, '.min.js'); 56 | })) 57 | .pipe(gulp.dest('./dist')) 58 | .on('end', function () { 59 | size.print(); 60 | done(); 61 | }) 62 | }); 63 | 64 | gulp.task('all-build', function (done) { 65 | gulp.src(['index.js']) 66 | .pipe(webpack(config.all)) 67 | .pipe(allSize.max()) 68 | .pipe(gulp.dest('./dist')) 69 | .on('end', function () { 70 | done(); 71 | }); 72 | }); 73 | 74 | gulp.task('all-uglify', ['all-build'], function (done) { 75 | gulp.src(['dist/all.js']) 76 | .pipe(uglify({ 77 | preserveComments: 'some' 78 | })) 79 | .pipe(allSize.min(function (path) { 80 | return path.replace(/\.js$/, '.min.js'); 81 | })) 82 | .pipe(gulp.dest('./dist')) 83 | .on('end', function () { 84 | allSize.print(); 85 | done(); 86 | }) 87 | }); 88 | 89 | gulp.task('default', ['vd-uglify', 'all-uglify']); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var vd = require('./vd') 2 | , render = require('./render') 3 | , patch = require('./patch'); 4 | 5 | module.exports = { 6 | diff: vd.diff, 7 | h: vd.h, 8 | render: render, 9 | patch: patch 10 | }; -------------------------------------------------------------------------------- /lib/operate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | REMOVE: 1, 3 | INSERT: 2, 4 | REPLACE: 3, 5 | ORDER: 4, 6 | PROPS: 5 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qvd", 3 | "version": "0.0.1", 4 | "description": "A very simple Virtual DOM and diffing aloorithm for mobile fork from virtual-dom.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "gulp": "^3.8.11", 12 | "gulp-uglify": "^1.1.0", 13 | "gulp-webpack": "^1.3.1", 14 | "map-stream": "^0.0.5", 15 | "maxmin": "^1.1.0", 16 | "mocha": "^2.2.1", 17 | "should": "^5.2.0" 18 | }, 19 | "scripts": { 20 | "test": "mocha" 21 | }, 22 | "author": "", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /patch.js: -------------------------------------------------------------------------------- 1 | // TODO in the rough 2 | var OPERATE = require('./lib/operate') 3 | , DEAL = {}; 4 | 5 | function isText(vd) { 6 | return vd.tag === 'text'; 7 | } 8 | 9 | function getVDByKey(vds, key, cache) { 10 | cache = cache || {}; 11 | if (cache[key]) return cache[key]; 12 | for (var i = 0, l = vds.length; i < l; i++) { 13 | if (vds[i].key === key) return (cache[key] = vds[i]); 14 | } 15 | } 16 | 17 | function find(vd, vds) { 18 | var i = vds.indexOf(vd) 19 | , res, tmp; 20 | if (!(~i)) { 21 | vds.every(function (item, j) { 22 | if (!item.children || !item.children.length) return true; 23 | res = find(vd, item.children); 24 | if (res) { 25 | tmp = res; 26 | res = [j]; 27 | [].push.apply(res, tmp); 28 | return false; 29 | } else { 30 | return true; 31 | } 32 | }); 33 | return res; 34 | } 35 | return [i]; 36 | } 37 | 38 | function getRef(vd, root, a) { 39 | var ref = vd.ref; 40 | if (ref) return ref; 41 | if (!root || !a) throw new Error('Should bind the reference node.'); 42 | var indexes = find(vd, [a]); 43 | if (!indexes) throw new Error('Couldn\'t find the reference node.'); 44 | ref = root.childNodes[0]; 45 | for (var i = 1, l = indexes.length; i < l; i++) { 46 | ref = ref.childNodes[indexes[i]]; 47 | } 48 | vd.ref = ref; 49 | return ref; 50 | } 51 | 52 | function setProps(to, from, node) { 53 | for (key in from) { 54 | // style 55 | if (key === 'style') { 56 | style = node.style; 57 | value = from[key]; 58 | if (!value) { 59 | style.cssText = ''; 60 | to.style = {}; 61 | } else { 62 | value = from[key]; 63 | for (i in value) { 64 | if (value[i] === undefined) { 65 | style[i] = ''; 66 | delete to.style[i]; 67 | } else { 68 | style[i] = value[i]; 69 | to.style[i] = value[i]; 70 | } 71 | } 72 | } 73 | // property 74 | } else if (key in node) { 75 | value = from[key]; 76 | type = typeof node[key]; 77 | if (value === undefined) { 78 | type === 'string' ? 79 | (node[key] = '') : 80 | (node[key] = null); 81 | delete to[key]; 82 | } else { 83 | node[key] = to[key] = value; 84 | } 85 | // attribute 86 | } else { 87 | value = from[key]; 88 | if (value === undefined) { 89 | node.removeAttribute(key); 90 | delete to[key]; 91 | } else { 92 | node.setAttribute(key, value); 93 | to[key] = value; 94 | } 95 | } 96 | } 97 | } 98 | 99 | function create(vds, parent) { 100 | !Array.isArray(vds) && (vds = [vds]); 101 | parent = parent || document.createDocumentFragment(); 102 | var node; 103 | vds.forEach(function (vd) { 104 | if (isText(vd)) { 105 | node = document.createTextNode(vd.text); 106 | } else { 107 | node = document.createElement(vd.tag); 108 | } 109 | parent.appendChild(node); 110 | vd.children && vd.children.length && 111 | create(vd.children, node); 112 | 113 | vd.props && 114 | setProps({ style: {} }, vd.props, node); 115 | }); 116 | return parent; 117 | } 118 | 119 | function splice(vd, a, replacement) { 120 | var indexes, i, l; 121 | indexes = find(vd, [a]); 122 | for (i = 1, l = indexes.length - 1; i < l; i++) { 123 | a = a.children[indexes[i]]; 124 | } 125 | replacement ? 126 | a.children.splice(indexes[l], 1, replacement) : 127 | a.children.splice(indexes[l], 1); 128 | } 129 | 130 | DEAL[OPERATE.REMOVE] = function (item, root, a) { 131 | var node = getRef(item.from, root, a); 132 | node.parentNode.removeChild(node); 133 | item.from.ref = null; 134 | splice(item.from, a); 135 | }; 136 | 137 | DEAL[OPERATE.INSERT] = function (item, root, a) { 138 | var node = create(item.from) 139 | , parent = getRef(item.to, root, a); 140 | parent.appendChild(node); 141 | item.to.children.push(item.from); 142 | }; 143 | 144 | DEAL[OPERATE.REPLACE] = function (item, root, a) { 145 | var node = create(item.from) 146 | , target = getRef(item.to, root, a) 147 | , indexes 148 | , i 149 | , l; 150 | target.parentNode.replaceChild(node, target); 151 | item.to.ref = null; 152 | splice(item.to, a, item.from); 153 | }; 154 | 155 | DEAL[OPERATE.ORDER] = function (item, root, a) { 156 | var to = item.to 157 | , from = item.from 158 | , children = to.children 159 | , parent = getRef(to, root, a) 160 | , grandpa = parent.parentNode 161 | , removes = from.removes 162 | , inserts = from.inserts 163 | , cache = {}; 164 | 165 | removes.forEach(function (order) { 166 | var vd = getVDByKey(children, order.key, cache) 167 | , node = getRef(vd, grandpa, to); 168 | parent.removeChild(node); 169 | splice(vd, to); 170 | }); 171 | inserts.forEach(function (order) { 172 | var vd = getVDByKey(children, order.key, cache) 173 | , node = getRef(vd, grandpa, to) 174 | , before = getRef(children[order.to], grandpa, to); 175 | parent.insertBefore(node, before); 176 | children.splice(order.to, 0, vd); 177 | }); 178 | cache = null; 179 | }; 180 | 181 | DEAL[OPERATE.PROPS] = function (item, root, a) { 182 | var props = item.from.props 183 | , node = getRef(item.from, root, a) 184 | , diff = item.diff 185 | , key, value, i, l, style, type; 186 | setProps(props, diff, node); 187 | }; 188 | 189 | function patch(patches, root) { 190 | var i = 0, l = patches.length 191 | , a = patches.a; 192 | root = root || a.hook; 193 | for (; i < l; i++) { 194 | patches[i].forEach(function (item) { 195 | DEAL[item.operate](item, root, a); 196 | }); 197 | } 198 | } 199 | 200 | module.exports = patch; -------------------------------------------------------------------------------- /q-vd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO 3 | * q-vd directive for Q.js 4 | * @example 5 | * 11 | * Just use jsx-transform transform to: 12 | * h('li', null, [ 13 | * h('img', { src: "avatar.png", class: "profile" }), 14 | * h('h3', null, [[this.user.firstName, user.lastName].join(' ')]) 15 | * ]); 16 | * q-vd directive will bind data to this, and when data change, it will rerender the dom 17 | */ 18 | var Q = require('Q') 19 | , h = require('./vd').h 20 | , diff = require('./vd').diff 21 | , render = require('./render') 22 | , vds = {} 23 | , jsxs = {} 24 | , uid = 0; 25 | 26 | function Jsx(name, jsx) { 27 | if (jsx) { 28 | jsxs[name] = new Function('h', jsx); 29 | } else { 30 | jsx = jsxs[name]; 31 | if (!jsx) throw new Error('jsx ' + name + ' not exists.'); 32 | return jsx; 33 | } 34 | } 35 | 36 | function first(name, el, data) { 37 | var jsx = Jsx(name) 38 | , vd; 39 | vd = jsx.call(data, h); 40 | el.innerHTML = render(vd); 41 | // TODO need to bind ele & vd 42 | el.vdId = ++uid; 43 | vds[uid] = vd; 44 | } 45 | 46 | function update(data) { 47 | var el = this.el 48 | , name = this.arg 49 | , vdId = el.vdId; 50 | if (!vdId) return first(name, el, data); 51 | var aVd = vds[vdId] 52 | , jsx = Jsx(name) 53 | , bVd 54 | , patches; 55 | bVd = jsx.call(data, h); 56 | patches = diff(aVd, bVd); 57 | // TODO need to deal with patches 58 | vds[vdId] = bVd; 59 | } 60 | 61 | Q.jsx = Jsx; 62 | Q.options.directives.vd = update; -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | var singleTag = { 2 | area: true, 3 | base: true, 4 | basefont: true, 5 | br: true, 6 | col: true, 7 | command: true, 8 | embed: true, 9 | frame: true, 10 | hr: true, 11 | img: true, 12 | input: true, 13 | isindex: true, 14 | keygen: true, 15 | link: true, 16 | meta: true, 17 | param: true, 18 | source: true, 19 | track: true, 20 | wbr: true, 21 | } 22 | , booleanAttributes = { 23 | allowfullscreen: true, 24 | async: true, 25 | autofocus: true, 26 | autoplay: true, 27 | checked: true, 28 | controls: true, 29 | default: true, 30 | defer: true, 31 | disabled: true, 32 | hidden: true, 33 | ismap: true, 34 | loop: true, 35 | multiple: true, 36 | muted: true, 37 | open: true, 38 | readonly: true, 39 | required: true, 40 | reversed: true, 41 | scoped: true, 42 | seamless: true, 43 | selected: true, 44 | typemustmatch: true 45 | } 46 | , REG_CAP = /[A-Z]/g; 47 | 48 | function isEle(node) { 49 | return node.tag !== 'text'; 50 | } 51 | 52 | function formatStyle(style) { 53 | var key, value 54 | , output = 'style="'; 55 | 56 | for (key in style) { 57 | value = style[key]; 58 | 59 | key = key.replace(REG_CAP, function (cap) { 60 | return '-' + cap.toLowerCase(); 61 | }); 62 | 63 | output += key + ': ' + value + ';'; 64 | } 65 | 66 | return output + '"'; 67 | } 68 | 69 | function formatProps(props) { 70 | if (!props) return; 71 | 72 | var output = '' 73 | , key 74 | , value; 75 | 76 | for (key in props) { 77 | value = props[key]; 78 | 79 | if (output) { 80 | output += ' '; 81 | } 82 | 83 | if (!value && booleanAttributes[key]) { 84 | output += key; 85 | } else if (key === 'style') { 86 | output += formatStyle(value); 87 | } else { 88 | output += key + '="' + value + '"'; 89 | } 90 | } 91 | return output; 92 | } 93 | 94 | function renderTag(vd) { 95 | var tag = '<'+ vd.tag 96 | , props = formatProps(vd.props); 97 | 98 | if (props) { 99 | tag += ' ' + props; 100 | } 101 | 102 | tag += '>'; 103 | 104 | if (vd.children) { 105 | tag += render(vd.children); 106 | } 107 | 108 | if (!singleTag[vd.tag]) { 109 | tag += ''; 110 | } 111 | 112 | return tag; 113 | } 114 | 115 | function renderText(vd) { 116 | return vd.text; 117 | } 118 | 119 | 120 | function render(vds) { 121 | !Array.isArray(vds) && 122 | (vds = [vds]); 123 | 124 | var output = '' 125 | , l = vds.length 126 | , vd; 127 | 128 | for (var i = 0; i < l; i++) { 129 | vd = vds[i] 130 | 131 | if (isEle(vd)) { 132 | output += renderTag(vd); 133 | } else { 134 | output += renderText(vd); 135 | } 136 | } 137 | return output; 138 | } 139 | 140 | module.exports = render; -------------------------------------------------------------------------------- /test/cases/test.js: -------------------------------------------------------------------------------- 1 | var diff = vd.diff 2 | , h = vd.h 3 | , patch = vd.patch 4 | , render = vd.render 5 | , container = document.getElementById('container') 6 | , a = h('div', { style: { color: '#fff' } }); 7 | 8 | describe('render', function () { 9 | it('should able render a template', function () { 10 | var div; 11 | container.innerHTML = render(a); 12 | div = container.childNodes[0]; 13 | div.tagName.toLowerCase().should.equal('div'); 14 | div.style.color.should.equal('rgb(255, 255, 255)'); 15 | }); 16 | 17 | it('should able rerender properties', function () { 18 | var b = h('div', { style: { color: '#000' } }) 19 | , patches = diff(a, b) 20 | , div; 21 | 22 | patch(patches, container); 23 | div = container.childNodes[0]; 24 | div.style.color.should.equal('rgb(0, 0, 0)'); 25 | 26 | b = h('div', { style: { background: '#000' } }); 27 | patches = diff(a, b); 28 | patch(patches, container); 29 | div.style.color.should.equal(''); 30 | div.style.background.should.equal('rgb(0, 0, 0)'); 31 | 32 | b = h('div'); 33 | patches = diff(a, b); 34 | patch(patches, container); 35 | div.style.length.should.equal(0); 36 | 37 | b = h('div', { 'data-id': '123456' }); 38 | patches = diff(a, b); 39 | patch(patches, container); 40 | div.getAttribute('data-id').should.equal('123456'); 41 | }); 42 | 43 | it('should able insert node', function () { 44 | var b = h('div', null, ['hello']) 45 | , patches = diff(a, b) 46 | , div, node; 47 | 48 | patch(patches, container); 49 | div = container.childNodes[0]; 50 | node = div.childNodes[0]; 51 | node.nodeType.should.equal(3); 52 | node.nodeValue.should.equal('hello'); 53 | 54 | a = h('div'); 55 | container.innerHTML = render(a); 56 | div = container.childNodes[0]; 57 | 58 | b = h('div', null, [h('p', { style: { color: '#000' } }, ['hello'])]); 59 | patches = diff(a, b); 60 | patch(patches, container); 61 | node = div.childNodes[0]; 62 | node.tagName.toLowerCase().should.equal('p'); 63 | node.innerText.should.equal('hello'); 64 | node.style.color.should.equal('rgb(0, 0, 0)'); 65 | }); 66 | 67 | it('should able replace node', function () { 68 | var b = h('div', null, [h('span', null, ['tencent'])]) 69 | , patches = diff(a, b) 70 | , div 71 | , node; 72 | 73 | patch(patches, container); 74 | div = container.childNodes[0]; 75 | node = div.childNodes[0]; 76 | node.tagName.toLowerCase().should.equal('span'); 77 | node.innerText.should.equal('tencent'); 78 | }); 79 | 80 | it('should able remove node', function () { 81 | var b = h('div') 82 | , patches = diff(a, b) 83 | , div; 84 | 85 | patch(patches, container); 86 | div = container.childNodes[0]; 87 | div.childNodes.length.should.equal(0); 88 | }); 89 | 90 | it('should able order node', function () { 91 | a = h('div', null, [h('p', { key: 1 }, ['hello']), h('p', { key: 2 }, ['tencent'])]); 92 | var b = h('div', null, [h('p', { key: 2 }, ['tencent']), h('p', { key: 1 }, ['hello'])]) 93 | , patches 94 | , div 95 | , p; 96 | 97 | container.innerHTML = render(a); 98 | patches = diff(a, b); 99 | patch(patches, container); 100 | div = container.childNodes[0]; 101 | p = div.childNodes[0]; 102 | p.tagName.toLowerCase().should.equal('p'); 103 | p.innerText.should.equal('tencent'); 104 | p = div.childNodes[1]; 105 | p.tagName.toLowerCase().should.equal('p'); 106 | p.innerText.should.equal('hello'); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 21 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var qvd = require('../') 2 | , h = qvd.h 3 | , diff = qvd.diff 4 | , render = qvd.render 5 | , OPERATE = require('../lib/operate'); 6 | 7 | describe('diff', function () { 8 | it('should not need any operate for two equal div', function () { 9 | var a1 = h('div') 10 | , a2 = h('div') 11 | , b1 = h('div', { style: { color: '#fff' } }) 12 | , b2 = h('div', { style: { color: '#fff' } }) 13 | , c1 = h('div', { style: { color: '#fff' } }, [h('text', 'hello')]) 14 | , c2 = h('div', { style: { color: '#fff' } }, [h('text', 'hello')]) 15 | , d1 = h('div', null, [h('p', null, [h('text', 'hello')])]) 16 | , d2 = h('div', null, [h('p', null, [h('text', 'hello')])]); 17 | 18 | diff(a1, a2).length.should.equal(0); 19 | diff(b1, b2).length.should.equal(0); 20 | diff(c1, c2).length.should.equal(0); 21 | diff(d1, d2).length.should.equal(0); 22 | }); 23 | 24 | it('should able to check the difference for two div', function () { 25 | var text1 = h('text', 'hello') 26 | , text2 = h('text', 'hello world') 27 | , p1 = h('p', { key: 1 }, [text1]) 28 | , p2 = h('p', { key: 2 }, [text1]) 29 | , p3 = h('p', { key: 2}, [text1]) 30 | , p4 = h('p', { key: 1 }, [text2]); 31 | 32 | var res 33 | , a1 = h('div', { style: { color: '#000' } }) 34 | , a2 = h('div', { style: { color: '#fff' } }) 35 | , b1 = h('div', { style: { color: '#fff' } }, [h('p', null, [text1])]) 36 | , b2 = h('div', { style: { color: '#fff' } }, [h('p', null, [text2])]) 37 | , c1 = h('div', null, [p1]) 38 | , c2 = h('div', null, [p2]) 39 | , d1 = h('div', null, [p1, p2]) 40 | , d2 = h('div', null, [p3, p4]); 41 | 42 | res = diff(a1, a2); 43 | res.length.should.equal(1); 44 | res[0].length.should.equal(1); 45 | res[0][0].operate.should.equal(OPERATE.PROPS); 46 | res[0][0].diff.style.color.should.equal('#fff'); 47 | 48 | res = diff(b1, b2); 49 | res.length.should.equal(1); 50 | res[0].length.should.equal(1); 51 | res[0][0].operate.should.equal(OPERATE.REPLACE); 52 | res[0][0].from.should.equal(text1); 53 | res[0][0].to.should.equal(text2); 54 | 55 | res = diff(c1, c2); 56 | res.length.should.equal(2); 57 | res[0].length.should.equal(1); 58 | res[1].length.should.equal(1); 59 | res[0][0].operate.should.equal(OPERATE.INSERT); 60 | res[0][0].from.should.equal(p2); 61 | res[1][0].operate.should.equal(OPERATE.REMOVE); 62 | res[1][0].from.should.equal(p1); 63 | 64 | res = diff(d1, d2); 65 | res.length.should.equal(2); 66 | res[0].length.should.equal(1); 67 | res[1].length.should.equal(1); 68 | res[0][0].operate.should.equal(OPERATE.ORDER); 69 | res[0][0].to.should.eql({ removes: [{ from: 1, key: 2 }], inserts: [{ key: 2, to: 0 }] }); 70 | res[1][0].from.should.equal(text1); 71 | res[1][0].to.should.equal(text2); 72 | }); 73 | }); 74 | 75 | describe('render', function () { 76 | it('should render a div which has a p in it', function () { 77 | var vd = h('div', { style: { backgroundColor: '#fff' } }, [h('p', null, [h('text', 'hello')])]); 78 | render(vd).should.equal('

hello

') 79 | }); 80 | 81 | it('should render a input has value', function () { 82 | var vd = h('input', { value: 'hello' }); 83 | render(vd).should.equal(''); 84 | }); 85 | 86 | it('should render a disabled input', function () { 87 | var vd = h('input', { disabled: '' }); 88 | render(vd).should.equal(''); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /vd.js: -------------------------------------------------------------------------------- 1 | var OPERATE = require('./lib/operate'); 2 | 3 | function isText(node) { 4 | return node.tag === 'text'; 5 | } 6 | 7 | function isEle(node) { 8 | return node.tag !== 'text'; 9 | } 10 | 11 | function isObject(obj) { 12 | return typeof obj === 'object'; 13 | } 14 | 15 | function remove(arr, index, key) { 16 | arr.splice(index, 1); 17 | 18 | return { 19 | from: index, 20 | key: key 21 | }; 22 | } 23 | 24 | function walk(a, b, patches, index) { 25 | patches[index] = apply = patches[index] || []; 26 | // no need to walk 27 | if (b == null) { 28 | apply.push({ 29 | from: a, 30 | operate: OPERATE.REMOVE 31 | }); 32 | // a and b is element 33 | } else if(isEle(a) && isEle(b)) { 34 | if (a.tag === b.tag) { 35 | var diff = diffProps(a.props, b.props); 36 | if (diff) { 37 | apply.push({ 38 | diff: diff, 39 | from: a, 40 | operate: OPERATE.PROPS 41 | }); 42 | } 43 | diffChildren(a, b, patches, apply, index); 44 | } else { 45 | apply.push({ 46 | from: b, 47 | to: a, 48 | operate: OPERATE.REPLACE 49 | }); 50 | } 51 | // at least one is text 52 | } else { 53 | if ( 54 | isEle(a) || isEle(b) 55 | || a.text !== b.text 56 | ) { 57 | apply.push({ 58 | from: b, 59 | to: a, 60 | operate: OPERATE.REPLACE 61 | }); 62 | } 63 | } 64 | } 65 | 66 | function diffProps(a, b, apply) { 67 | var key, aVal, bVal, diff; 68 | 69 | for (key in a) { 70 | if (!(key in b)) { 71 | diff = diff || {}; 72 | diff[key] = undefined; 73 | continue; 74 | } 75 | 76 | aVal = a[key]; 77 | bVal = b[key]; 78 | 79 | if (aVal === bVal) { 80 | continue; 81 | } else if (isObject(aVal) && isObject(bVal)) { 82 | var objectDiff = diffProps(aVal, bVal); 83 | if (objectDiff) { 84 | diff = diff || {}; 85 | diff[key] = objectDiff; 86 | } 87 | } else { 88 | diff = diff || {}; 89 | diff[key] = bVal; 90 | } 91 | } 92 | 93 | for (key in b) { 94 | if (!(key in a)) { 95 | diff = diff || {}; 96 | diff[key] = b[key]; 97 | } 98 | } 99 | 100 | return diff; 101 | } 102 | 103 | function keyIndex(children) { 104 | var keys = {} 105 | , free = [] 106 | , length = children.length 107 | , i = 0 108 | , child; 109 | 110 | for (; i < length; i++) { 111 | child = children[i]; 112 | 113 | if (child.key) { 114 | keys[child.key] = i; 115 | } else { 116 | free.push(i); 117 | } 118 | } 119 | 120 | return { 121 | keys: keys, // A hash of key name to index 122 | free: free // An array of unkeyed item indices 123 | }; 124 | } 125 | 126 | function reorder(aChildren, bChildren) { 127 | // O(M) time, O(M) memory 128 | var bChildIndex = keyIndex(bChildren) 129 | , bKeys = bChildIndex.keys 130 | , bFree = bChildIndex.free; 131 | 132 | if (bFree.length === bChildren.length) { 133 | return { 134 | children: bChildren, 135 | moves: null 136 | }; 137 | } 138 | 139 | // O(N) time, O(N) memory 140 | var aChildIndex = keyIndex(aChildren) 141 | , aKeys = aChildIndex.keys 142 | , aFree = aChildIndex.free; 143 | 144 | if (aFree.length === aChildren.length) { 145 | return { 146 | children: bChildren, 147 | moves: null 148 | }; 149 | } 150 | 151 | // O(MAX(N, M)) memory 152 | var newChildren = [] 153 | , freeIndex = 0 154 | , freeCount = bFree.length 155 | , deletedItems = 0 156 | , aItem 157 | , itemIndex 158 | , i = 0 159 | , l = aChildren.length; 160 | 161 | // Iterate through a and match a node in b 162 | // O(N) time, 163 | for (; i < l; i++) { 164 | aItem = aChildren[i]; 165 | 166 | if (aItem.key) { 167 | if (bKeys.hasOwnProperty(aItem.key)) { 168 | // Match up the old keys 169 | itemIndex = bKeys[aItem.key]; 170 | newChildren.push(bChildren[itemIndex]); 171 | } else { 172 | // Remove old keyed items 173 | itemIndex = i - deletedItems++; 174 | newChildren.push(null); 175 | } 176 | } else { 177 | // Match the item in a with the next free item in b 178 | if (freeIndex < freeCount) { 179 | itemIndex = bFree[freeIndex++]; 180 | newChildren.push(bChildren[itemIndex]); 181 | } else { 182 | // There are no free items in b to match with 183 | // the free items in a, so the extra free nodes 184 | // are deleted. 185 | itemIndex = i - deletedItems++; 186 | newChildren.push(null); 187 | } 188 | } 189 | } 190 | 191 | var lastFreeIndex = freeIndex >= bFree.length ? 192 | bChildren.length : 193 | bFree[freeIndex] 194 | , j = 0 195 | , newItem; 196 | 197 | l = bChildren.length; 198 | 199 | // Iterate through b and append any new keys 200 | // O(M) time 201 | for (; j < l; j++) { 202 | newItem = bChildren[j]; 203 | 204 | if (newItem.key) { 205 | if (!aKeys.hasOwnProperty(newItem.key)) { 206 | // Add any new keyed items 207 | // We are adding new items to the end and then sorting them 208 | // in place. In future we should insert new items in place. 209 | newChildren.push(newItem); 210 | } 211 | } else if (j >= lastFreeIndex) { 212 | // Add any leftover non-keyed items 213 | newChildren.push(newItem); 214 | } 215 | } 216 | 217 | var simulate = newChildren.slice() 218 | , simulateIndex = 0 219 | , removes = [] 220 | , inserts = [] 221 | , simulateItem 222 | , wantedItem 223 | , k = 0; 224 | 225 | for (; k < l;) { 226 | wantedItem = bChildren[k]; 227 | simulateItem = simulate[simulateIndex]; 228 | 229 | // remove items 230 | while (simulateItem === null && simulate.length) { 231 | removes.push(remove(simulate, simulateIndex, null)); 232 | simulateItem = simulate[simulateIndex]; 233 | } 234 | 235 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 236 | // if we need a key in this position... 237 | if (wantedItem.key) { 238 | if (simulateItem && simulateItem.key) { 239 | // if an insert doesn't put this key in place, it needs to move 240 | if (bKeys[simulateItem.key] !== k + 1) { 241 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 242 | simulateItem = simulate[simulateIndex]; 243 | // if the remove didn't put the wanted item in place, we need to insert it 244 | if (!simulateItem || simulateItem.key !== wantedItem.key) { 245 | inserts.push({key: wantedItem.key, to: k}); 246 | // items are matching, so skip ahead 247 | } else { 248 | simulateIndex++ 249 | } 250 | } else { 251 | inserts.push({key: wantedItem.key, to: k}) 252 | } 253 | } else { 254 | inserts.push({key: wantedItem.key, to: k}) 255 | } 256 | k++ 257 | // a key in simulate has no matching wanted key, remove it 258 | } else if (simulateItem && simulateItem.key) { 259 | removes.push(remove(simulate, simulateIndex, simulateItem.key)); 260 | } 261 | } else { 262 | simulateIndex++; 263 | k++; 264 | } 265 | } 266 | 267 | // remove all the remaining nodes from simulate 268 | while(simulateIndex < simulate.length) { 269 | simulateItem = simulate[simulateIndex]; 270 | removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key)); 271 | } 272 | 273 | // If the only moves we have are deletes then we can just 274 | // let the delete patch remove these items. 275 | if (removes.length === deletedItems && !inserts.length) { 276 | return { 277 | children: newChildren, 278 | moves: null 279 | }; 280 | } 281 | 282 | return { 283 | children: newChildren, 284 | moves: { 285 | removes: removes, 286 | inserts: inserts 287 | } 288 | }; 289 | } 290 | 291 | function diffChildren(a, b, patches, apply, index) { 292 | var aChildren = a.children 293 | , orderedSet = reorder(aChildren, b.children) 294 | , bChildren = orderedSet.children 295 | , aLen = aChildren.length 296 | , bLen = bChildren.length 297 | , len = aLen > bLen ? aLen : bLen 298 | , i = 0 299 | , leftNode 300 | , rightNode; 301 | 302 | for (var i = 0; i < len; i++) { 303 | leftNode = aChildren[i]; 304 | rightNode = bChildren[i]; 305 | index += 1; 306 | 307 | if (!leftNode) { 308 | if (rightNode) { 309 | apply.push({ 310 | from: rightNode, 311 | to: a, 312 | operate: OPERATE.INSERT 313 | }); 314 | } 315 | } else { 316 | walk(leftNode, rightNode, patches, index) 317 | } 318 | } 319 | 320 | if (orderedSet.moves) { 321 | apply.push({ 322 | operate: OPERATE.ORDER, 323 | from: orderedSet.moves, 324 | to: a 325 | }); 326 | } 327 | 328 | return apply; 329 | } 330 | 331 | /** 332 | * h(tagName, props, children) 333 | * h('text', 'hello') 334 | * { 335 | * tag: 'div', 336 | * children: [], 337 | * props: {} 338 | * } 339 | */ 340 | function h(tagName, props, children) { 341 | props = props || {}; 342 | children = children || []; 343 | var key; 344 | if (tagName === 'text') { 345 | return { 346 | tag: tagName, 347 | text: props 348 | }; 349 | } else { 350 | if ('key' in props) { 351 | key = props.key; 352 | delete props['key']; 353 | } 354 | for (var i = 0, l = children.length; i < l; i++) { 355 | // text 356 | typeof children[i] === 'string' && 357 | (children[i] = h('text', children[i])); 358 | } 359 | return { 360 | tag: tagName, 361 | props: props, 362 | children: children, 363 | key: key 364 | }; 365 | } 366 | } 367 | 368 | /** 369 | * diff 370 | * @param {VD} a 371 | * @param {VD} b 372 | */ 373 | function diff(a, b) { 374 | var patches = {}, res = []; 375 | res.a = a; 376 | walk(a, b, patches, 0); 377 | for (var i = 0; patches[i]; i++) { 378 | patches[i].length && 379 | res.push(patches[i]) 380 | } 381 | return res; 382 | } 383 | 384 | module.exports = { 385 | h: h, 386 | diff: diff 387 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | vd: { 3 | output: { 4 | filename: 'vd.js', 5 | library: 'vd', 6 | libraryTarget: 'amd' 7 | } 8 | }, 9 | all: { 10 | output: { 11 | filename: 'all.js', 12 | library: 'vd', 13 | libraryTarget: 'amd' 14 | } 15 | } 16 | } --------------------------------------------------------------------------------