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算法的简化版实现的主要内容,如果未能解决你的问题,请参考以下文章

React的diffing算法(面试题)

React的diffing算法(面试题)

0208DOM的diffing算法-React

React入门学习-- diffing 算法

React入门学习-- diffing 算法

DOM的diffing算法