2-2-12 React原理基础 (DOM - DIFF )部分

Posted 沿着路走到底

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-2-12 React原理基础 (DOM - DIFF )部分相关的知识,希望对你有一定的参考价值。

WHY DOM-DIFF?

我们将一个React函数看做是一个返回VirtualDOM Tree的方法:

function SomeComponent()         
    return <div>
        <span>...</span>
        <Button>...</Button>
    </div>

上面的JSX会被转化成树状结构:

div
 span
 Button

当箭头函数的返回值发生变化的时候,比如(新增了某个节点):

function SomeComponent(a)         
    return <div>
        <span>...</span>
        <Button>...</Button>
        a && <p>新增的节点</p>
    </div>

我们可以有很多的策略完成这样的更新,比如把所有节点都删了,再重新构造。

当然,**最划算**的渲染策略,是在`div` 下新增一个`p` ,而不是将整个DOM替换。

React的调和算法帮助我们找到这种最快的更新途径。

伪代码

具体来说,React渲染函数的执行,会产生一个树状结构:

<div style=... />
// 会被转化为
React.createElement('div', 
    style : 
        ...
    
)

`createElement` 后形成一个VirtualDOM节点

class ReactElement 
    type : SomeComponent   
    props : 
        children
     
    

Element可以看做“数据”的描述, 也就是元数据。 Element可以看做是虚拟DOM,代表了真实的DOM结构。因为每次渲染函数调用都生成一个新的Element,因此在Element之上还需要一个封装。

例如(伪代码):

class FiberNode 
    type : SomeComponent
    props : ...
    
    update(...) 

同一个渲染函数的两次执行,究竟产生哪些DOM操作,需要由DOM-DIFF模块确定

具体来说,对于某个给定的组件:

function SomeComponent(...) 
    return <div>...</div>

当组件`SomeComponent`触发更新的时候,`react` 会这样处理:

... Fiber Context 
    
    let vDOMOld  // 上一次调用SomeComponent产生的virtualDOM
    ……
    update() 
        const vdomNext = SomeComponent(...)
        const updates = domDiff(vDOMOld, vDOMNext)
        vDOMOld = vDOMNext
        apply(updates)
    

React更新产生虚拟DOM节点,然后通过diff算法比较两个DOM节点的差异,决定更新步骤,最后再向DOM应用这些更新。

在上面的程序中,箭头函数的返回值会被React记录下来,下次更新会尝试使用这个值继续比较。

这里有一个要求,domDiff算法的效率必须足够高,因为所有的更新都依赖它

DOM-DIFF细节

对于类型相同的节点

if ( vDOMOld.type === vDOMNext.type)

对于类型相同的节点,比如:

function Button(text) 
    return <button>text</button>

当发生变化的时候:

<Button text="点我" />
// 到
<Button text="请按键" />

只需要替换属性即可,React遇到这种情况,会帮用户替换掉属性。

不同类型的节点

当遇到不同类型的节点,React会直接替换。

<div>
  <Counter />
</div>

// 到
<span>
  <Counter />
</span>

子节点的处理

对于子节点, React会进行对比和替换。

<ul>
  <li>first</li>
  <li>second</li>
</ul>
// 到
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React会只insert第三个li。

但是对于下面这种乱序的情况,React会逐一替换:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

这是因为DOM-DIFF会用简单的算法,顺序比较,而不是用动态规划。在比较一个列表的时候,用最短编辑距离(动态规划)后,仍然有昂贵的DOM操作(插入、删除)等。而动态规划算法的复杂度有O(n^2)。

为了解决比较慢的问题,React引入了key来解决:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>


//  到
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

上面程序中,React会用key来进行比较,上面程序会考虑到key相同的情况,认为key相同的是同一个节点。 因此2014会是一个插入的节点。

伪代码

function* domDIFF(vDOM1, vDOM2) 
    if(!vDOM1) 
        yield new InsertUpdate(vDOM1, vDOM2)
        return
    
    
        
    if(vDOM1.type === vDOM2.type) 
        if(vDOM1.key === vDOM2.key)             
          yield new AttributeUpdate(vDOM1, vDOM2)                
          yield * domDIFFArray(vDOM1.children, vDOM2.children)
         else 
          yield new ReplaceUpdate(vDOM1, vDOM2)
        
        return
     else 
        yield new ReplaceUpdate(vDOM1, vDOM2)
    
 




function toMap(arr) 
    const map = new Map()
    arr.forEach(item => 
        if(item.key)
          map.set(item.key, item)  
    )
    return map


function * domDiffArray(arr1, arr2) 
    if(!arr1 || !arr2) 
        yield new ReplaceUpdate(vDOM1, vDOM2)
        return
    

    const m1 = toMap(arr1)
    const m2 = toMap(arr2)
    
    // 需要删除的VDOM
    const deletes = arr1.filter( (item, i) =>         
        return item.key ? 
            !m2.has(item.key)
           	: i >= arr2.length
    )
    
    for(let item of deletes)
        yield new ReplaceUpdate(item, null)
    
    
    // 需要Replace的VDOM    
    for(let i = 0; i <arr1.length; i++) 
        const a = arr1[i]
        if(a.key ) 
            if(m2.has(a.key)) 
                yield * domDIFF(a, m2.get(a.key))
            
        
        else 
            if(i < arr2.length) 
                yield * domDIFF(a, arr2[i])
            
        
    
    
    // 需要Insert的VDOM
    for(let i = 0; i <arr2.length; i++) 
        const b = arr2[i]
        
        if(b.key) 
            if(!m1.has(b.key)) 
                yield new InsertUpdate(i, b)
                        
        else 
          if(i >= arr1.length) 
            yield new InsertUpdate(i, arr[2])
          
        
    




class InsertUpdate     
    constructor(pos, to)
        this.pos = pos
        this.to = to
    



class ReplaceUpdate 
    constructor(from, to)
        this.form = from 
        this.to = to
    

1

以上是关于2-2-12 React原理基础 (DOM - DIFF )部分的主要内容,如果未能解决你的问题,请参考以下文章

[react] 你知道Virtual DOM的工作原理吗?

[react] 简要描述下你知道的react工作原理是什么?

[react] 简要描述下你知道的react工作原理是什么?

第1273期React性能优化-虚拟Dom原理浅析

前端技能树,面试复习第 27 天—— React Diff 算法的原理,和 Vue 有什么区别 | 虚拟 DOM | key 的原理,为什么要用

Vue 2.x 的虚拟DOM原理