React diffing算法的简化版实现
Posted yzbmz5913
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React diffing算法的简化版实现相关的知识,希望对你有一定的参考价值。
diffing算法是React实现增量渲染的关键。当state或props更新时,render()函数被调用来渲染新的组件。React需要一种方法来高效地渲染,即尽可能复用组件,而不是推倒重来。
树上编辑距离算法(太复杂了看不懂)提供了一种在O(n³)复杂度(n是树上元素个数)内得到所需的最少状态转移数的方法。而这一复杂度对于React的实时性要求是不可接受的。
React基于两个简化实现了O(n)的diffing算法:
1.两棵树的根标签不同,则两棵树不同(即使子节点都相同);
2.可以通过手动指定key属性来标识不同的节点。
diffing算法的关键在于使用虚拟节点(Vnode),尽量不创建真实节点;并使用新旧头尾四个指针来进行双向匹配,非常酷
以下是diffing算法的(极简版)实现与测试样例。
export function patch(oldVnode, newVnode) if (newVnode.text && newVnode.children && newVnode.children.length) throw new Error(\'text and children cannot both exist\') if (!oldVnode.sel) //old node is a DOM,wrap it oldVnode = vnode(oldVnode.tagName.toLowerCase(), , [], undefined, oldVnode) if (isSame(oldVnode, newVnode)) patchVnode(oldVnode, newVnode) else //replace the old node with new one let newDOM = createElement(newVnode); if (oldVnode.elm) oldVnode.elm.parentNode.insertBefore(newDOM, oldVnode.elm) oldVnode.elm.parentNode.removeChild(oldVnode.elm) function patchVnode(oldVnode, newVnode) if (oldVnode === newVnode) return newVnode.elm = oldVnode.elm if (newVnode.text) if (newVnode.text !== oldVnode.text) oldVnode.elm.innerText = newVnode.text else if (oldVnode.text) oldVnode.elm.innerText = null for (let child of newVnode.children) oldVnode.elm.appendChild(createElement(child)) else //both old and new nodes has children,do the diffing! diff(oldVnode.elm, oldVnode.children, newVnode.children) // the main diffing algorithm function diff(parent, olds, news) //use four pointers let new1 = 0 let old1 = 0 let new2 = news.length - 1 let old2 = olds.length - 1 let old1Vnode = olds[new1] let old2Vnode = olds[old2] let new1Vnode = news[old1] let new2Vnode = news[new2] let oldKeyMap = while (new1 <= new2 && old1 <= old2) //skip all undefined if (!old1Vnode) old1Vnode = olds[++old1] else if (!old2Vnode) old2Vnode = olds[--old2] else if (!new1Vnode) new1Vnode = news[++new1] else if (!new2Vnode) new2Vnode = news[--new2] //four shortcuts else if (isSame(old1Vnode, new1Vnode)) patchVnode(old1Vnode, new1Vnode) old1Vnode = olds[++old1] new1Vnode = news[++new1] else if (isSame(old2Vnode, new2Vnode)) patchVnode(old2Vnode, new2Vnode) old2Vnode = olds[--old2] new2Vnode = news[--new2] else if (isSame(old1Vnode, new2Vnode)) patchVnode(old1Vnode, new2Vnode) parent.insertBefore(old1Vnode.elm, old2Vnode.elm.nextSibling) old1Vnode = olds[++old1] new2Vnode = news[--new2] else if (isSame(old2Vnode, new1Vnode)) patchVnode(old2Vnode, new1Vnode) parent.insertBefore(old2Vnode.elm, old1Vnode.elm) old2Vnode = olds[--old2] new1Vnode = news[++new1] else //use a map to record key to OldIdx //if all shortcuts failed, use the map to locate the next Vnode(new1Vnode) for (let i = old1; i <= old2; i++) let k = olds[i]?.key if (k) oldKeyMap[k] = i let idxInOld = oldKeyMap[new1Vnode.key] if (idxInOld) //an existing node. move it to the right place let toRemoved = olds[idxInOld] if (toRemoved.sel !== new1Vnode.sel) parent.insertBefore(createElement(new1Vnode), old1Vnode.elm) else patchVnode(toRemoved, new1Vnode) olds[idxInOld] = undefined parent.insertBefore(toRemoved.elm, old1Vnode.elm) else //a brand-new node parent.insertBefore(createElement(new1Vnode), old1Vnode.elm) new1Vnode = news[++new1] if (new1 <= new2) let before = news[new2 + 1] ? news[new2 + 1].elm : null for (let i = new1; i <= new2; i++) parent.insertBefore(createElement(news[i]), before) if (old1 <= old2) for (let i = old1; i <= old2; i++) if (olds[i]) parent.removeChild(olds[i].elm) // convert virtualDOM to DOM recursively function createElement(vnode) let dom = document.createElement(vnode.sel); //if is textNode if (vnode.text && !(vnode.children && vnode.children.length)) dom.innerText = vnode.text else if (Array.isArray(vnode.children) && vnode.children.length) for (let ch of vnode.children) let childDOM = createElement(ch); dom.appendChild(childDOM) vnode.elm = dom return dom export function h(sel, data, children) if (arguments.length !== 3) throw new Error(\'#arg error\') if (isPrimitive(children)) return vnode(sel, data, undefined, children.toString(), undefined) else if (Array.isArray(children)) let c = [] for (let i = 0; i < children.length; i++) if (isPrimitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined) c.push(children[i]) return vnode(sel, data, c, undefined, undefined) else if (typeof children === \'object\' && children.sel) return vnode(sel, data, [children], undefined, undefined) throw new Error(\'unexpected args\') function isPrimitive(obj) return typeof obj === \'string\' || typeof obj === \'number\' || obj instanceof String || obj instanceof Number function vnode(sel, data, children, text, elm) let key = data?.key return sel, data, children, text, elm, key function isSame(oldVnode, newVnode) return oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key
测试(欢迎捉虫):
let container = document.querySelector(\'#container\'); let vnode = h(\'ul\',,[ h(\'li\',key:\'A\',\'A\'), h(\'li\',key:\'B\',\'B\'), h(\'li\',key:\'C\',\'C\'), h(\'li\',key:\'D\',\'D\'), h(\'li\',key:\'E\',\'E\'), ]) patch(container,vnode) let vnode2 = h(\'ul\',,[ h(\'li\',key:\'D\',\'D\'), h(\'li\',key:\'E\',\'E\'), h(\'li\',key:\'G\',\'G\'), h(\'span\',key:\'F\',\'F\'), h(\'li\',key:\'A\',\'A\'), h(\'span\',key:\'C\',\'C\'), ]) patch(vnode,vnode2) let vnode3 = h(\'ul\',,[ h(\'li\',,\'啦啦啦\'), h(\'li\',,[ h(\'div\',,\'div11\'), h(\'div\',,\'div222\') ]) ]) patch(vnode2,vnode3)
以上是关于React diffing算法的简化版实现的主要内容,如果未能解决你的问题,请参考以下文章