diffing算法是React实现增量渲染的关键。当state或props更新时,render()函数被调用来渲染新的组件。React需要一种方法来高效地渲染,即尽可能复用组件,而不是推倒重来。
树上编辑距离算法(太复杂了看不懂)提供了一种在O(n³)复杂度(n是树上元素个数)内得到所需的最少状态转移数的方法。而这一复杂度对于React的实时性要求是不可接受的。
React基于两个简化实现了O(n)的diffing算法:
1.两棵树的根标签不同,则两棵树不同(即使子节点都相同);
2.可以通过手动指定key属性来标识不同的节点。
diffing算法的关键在于使用虚拟节点(Vnode),尽量不创建真实节点;并使用新旧头尾四个指针来进行双向匹配,非常酷
以下是diffing算法的(极简版)实现与测试样例。
1 export function patch(oldVnode, newVnode) { 2 if (newVnode.text && newVnode.children && newVnode.children.length) { 3 throw new Error('text and children cannot both exist') 4 } 5 if (!oldVnode.sel) { //old node is a DOM,wrap it 6 oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode) 7 } 8 if (isSame(oldVnode, newVnode)) { 9 patchVnode(oldVnode, newVnode) 10 } else { 11 //replace the old node with new one 12 let newDOM = createElement(newVnode); 13 if (oldVnode.elm) { 14 oldVnode.elm.parentNode.insertBefore(newDOM, oldVnode.elm) 15 } 16 oldVnode.elm.parentNode.removeChild(oldVnode.elm) 17 } 18 } 19 20 function patchVnode(oldVnode, newVnode) { 21 if (oldVnode === newVnode) { 22 return 23 } 24 newVnode.elm = oldVnode.elm 25 if (newVnode.text) { 26 if (newVnode.text !== oldVnode.text) { 27 oldVnode.elm.innerText = newVnode.text 28 } 29 } else { 30 if (oldVnode.text) { 31 oldVnode.elm.innerText = null 32 for (let child of newVnode.children) { 33 oldVnode.elm.appendChild(createElement(child)) 34 } 35 } else { 36 //both old and new nodes has children,do the diffing! 37 diff(oldVnode.elm, oldVnode.children, newVnode.children) 38 } 39 } 40 } 41 42 // the main diffing algorithm 43 function diff(parent, olds, news) { 44 //use four pointers 45 let new1 = 0 46 let old1 = 0 47 let new2 = news.length - 1 48 let old2 = olds.length - 1 49 let old1Vnode = olds[new1] 50 let old2Vnode = olds[old2] 51 let new1Vnode = news[old1] 52 let new2Vnode = news[new2] 53 let oldKeyMap = {} 54 while (new1 <= new2 && old1 <= old2) { 55 //skip all undefined 56 if (!old1Vnode) old1Vnode = olds[++old1] 57 else if (!old2Vnode) old2Vnode = olds[--old2] 58 else if (!new1Vnode) new1Vnode = news[++new1] 59 else if (!new2Vnode) new2Vnode = news[--new2] 60 //four shortcuts 61 else if (isSame(old1Vnode, new1Vnode)) { 62 patchVnode(old1Vnode, new1Vnode) 63 old1Vnode = olds[++old1] 64 new1Vnode = news[++new1] 65 } else if (isSame(old2Vnode, new2Vnode)) { 6 江苏党政培训 www.jxganxun.cn 6 patchVnode(old2Vnode, new2Vnode) 67 old2Vnode = olds[--old2] 68 new2Vnode = news[--new2] 69 } else if (isSame(old1Vnode, new2Vnode)) { 70 patchVnode(old1Vnode, new2Vnode) 71 parent.insertBefore(old1Vnode.elm, old2Vnode.elm.nextSibling) 72 old1Vnode = olds[++old1] 73 new2Vnode = news[--new2] 74 } else if (isSame(old2Vnode, new1Vnode)) { 75 patchVnode(old2Vnode, new1Vnode) 76 parent.insertBefore(old2Vnode.elm, old1Vnode.elm) 77 old2Vnode = olds[--old2] 78 new1Vnode = news[++new1] 79 } else { 80 //use a map to record key to OldIdx 81 //if all shortcuts failed, use the map to locate the next Vnode(new1Vnode) 82 for (let i = old1; i <= old2; i++) { 83 let k = olds[i]?.key 84 if (k) { 85 oldKeyMap[k] = i 86 } 87 } 88 let idxInOld = oldKeyMap[new1Vnode.key] 89 if (idxInOld) { 90 //an existing node. move it to the right place 91 let toRemoved = olds[idxInOld] 92 if (toRemoved.sel !== new1Vnode.sel) { 93 parent.insertBefore(createElement(new1Vnode), old1Vnode.elm) 94 } else { 95 patchVnode(toRemoved, new1Vnode) 96 olds[idxInOld] = undefined 97 parent.insertBefore(toRemoved.elm, old1Vnode.elm) 98 } 99 } else {100 //a brand-new node101 parent.insertBefore(createElement(new1Vnode), old1Vnode.elm)102 }103 new1Vnode = news[++new1]104 }105 }106 if (new1 <= new2) {107 let before = news[new2 + 1] ? news[new2 + 1].elm : null108 for (let i = new1; i <= new2; i++) {109 parent.insertBefore(createElement(news[i]), before)110 }111 }112 if (old1 <= old2) {113 for (let i = old1; i <= old2; i++) {114 if (olds[i]) {115 parent.removeChild(olds[i].elm)116 }117 }118 }119 }120 121 // convert virtualDOM to DOM recursively122 function createElement(vnode) {123 let dom = document.createElement(vnode.sel);124 //if is textNode125 if (vnode.text && !(vnode.children && vnode.children.length)) {126 dom.innerText = vnode.text127 } else if (Array.isArray(vnode.children) && vnode.children.length) {128 for (let ch of vnode.children) {129 let childDOM = createElement(ch);130 dom.appendChild(childDOM)131 }132 }133 vnode.elm = dom134 return dom135 }136 137 export function h(sel, data, children) {138 if (arguments.length !== 3) {139 throw new Error('#arg error')140 }141 if (isPrimitive(children)) {142 return vnode(sel, data, undefined, children.toString(), undefined)143 } else if (Array.isArray(children)) {144 let c = []145 for (let i = 0; i < children.length; i++) {146 if (isPrimitive(children[i])) {147 children[i] = vnode(undefined, undefined, undefined, children[i], undefined)148 }149 c.push(children[i])150 }151 return vnode(sel, data, c, undefined, undefined)152 } else if (typeof children === 'object' && children.sel) {153 return vnode(sel, data, [children], undefined, undefined)154 }155 throw new Error('unexpected args')156 }157 158 function isPrimitive(obj) {159 return typeof obj === 'string' ||160 typeof obj === 'number' ||161 obj instanceof String ||162 obj instanceof Number163 }164 165 function vnode(sel, data, children, text, elm) {166 let key = data?.key167 return {sel, data, children, text, elm, key}168 }169 170 function isSame(oldVnode, newVnode) {171 return oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key172 }
测试(欢迎捉虫):
1 let container = document.querySelector('#container'); 2 let vnode = h('ul',{},[ 3 h('li',{key:'A'},'A'), 4 h('li',{key:'B'},'B'), 5 h('li',{key:'C'},'C'), 6 h('li',{key:'D'},'D'), 7 h('li',{key:'E'},'E'), 8 ]) 9 10 patch(container,vnode)11 12 let vnode2 = h('ul',{},[13 h('li',{key:'D'},'D'),14 h('li',{key:'E'},'E'),15 h('li',{key:'G'},'G'),16 h('span',{key:'F'},'F'),17 h('li',{key:'A'},'A'),18 h('span',{key:'C'},'C'),19 20 ])21 patch(vnode,vnode2)22 23 let vnode3 = h('ul',{},[24 h('li',{},'啦啦啦'),25 h('li',{},[26 h('div',{},'div11'),27 h('div',{},'div222')28 ])29 ])30 patch(vnode2,vnode3)