最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • react diff算法理解总结

    正文概述 掘金(彬彬有礼5520)   2021-02-05   518

    ! 为了防止概念混淆,强调

    - 1.current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。
    - 2.workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点。
    - 3.DOM节点本身。
    - 4.JSX对象。即ClassComponent的render方法的返回结果,或者FunctionComponent的调用结果,JSX对象中包含描述DOM节点信息。
    
    diff算法的本质上是对比1和4,生成2。
    

    Diff的瓶颈以及React如何应对

    由于diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中
    将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。
    
    如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。
    

    所以为了降低算法复杂度,React的diff会预设3个限制:

     1.同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
     2.不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
     3.者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。
    

    那么我们接下来看一下Diff是如何实现的

    react diff算法理解总结

    我们可以从同级的节点数量将Diff分为两类:
     1.当newChild类型为object、number、string,代表同级只有一个节点
    - 2.当newChild类型为Array,同级有多个节点
    
    以类型Object为例,会进入这个函数reconcileSingleElement
    

    react diff算法理解总结

    这个函数会做如下事情:
    

    react diff算法理解总结

    让我们看看第二步判断DOM节点是否可以复用是如何实现的。
    

    react diff算法理解总结

    从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。

    这里有个细节需要关注下:

    1.当child !== null且key相同且type不同时执行deleteRemainingChildren将child及其兄弟fiber都标记删除。
    
    2.当child !== null且key不同时仅将child标记删除。
    

    react diff算法理解总结

    由于本次更新时只有一个p,属于单一节点的Diff,会走上面介绍的代码逻辑。

    在reconcileSingleElement中遍历之前的3个fiber(对应的DOM为3个li),寻找本次更新的p是否可以复用之前的3个fiber中某个的DOM。
    
    当key相同且type不同时,代表我们已经找到本次更新的p对应的上次的fiber,但是 p 与 li 的type不同,不能复用。
    既然唯一的可能性已经不能复用,则剩下的fiber都没有机会了,所以都需要标记删除。
    
    当key不同时只代表遍历到的该fiber不能被p复用,后面还有兄弟fiber还没有遍历到。所以仅仅标记该fiber删除。
    

    react diff算法理解总结

    习题1: 未设置key prop默认 key = null;,所以更新前后key相同,都为null,但是更新前type为div,更新后为p,type改变则不能复用。
    
    习题2: 更新前后key改变,不需要再判断type,不能复用。
    
    习题3: 更新前后key没变,但是type改变,不能复用。
    
    习题4: 更新前后key与type都未改变,可以复用。children变化,DOM的子元素需要更新。
    

    同级多个节点的Diff,一定属于下面3中情况的一种或多种。

    • 情况1:节点更新

    react diff算法理解总结

    • 情况2:节点新增或减少

    react diff算法理解总结

    • 情况3:节点位置变化

    react diff算法理解总结

    注意在这里diff算法无法使用双指针优化
    在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。
    
    虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber
    同级的Fiber节点是由sibling指针链接形成的单链表。
    
    即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。
    
    所以无法使用双指针优化。
    

    基于以上原因,Diff算法的整体逻辑会经历两轮遍历:

    1.第一轮遍历:处理更新的节点。
    2.第二轮遍历:处理剩下的不属于更新的节点
    

    第一轮遍历:

    第一轮遍历步骤如下:

    let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。
    
    如果可复用,i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历。
    
    如果不可复用,立即跳出整个遍历,第一轮遍历结束。
    
    如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null)
    跳出遍历,第一轮遍历结束。
    
    上面3跳出的遍历
    
    此时newChildren没有遍历完,oldFiber也没有遍历完。
    

    react diff算法理解总结

    前2个节点可复用,遍历到key === 2的节点发现type改变,不可复用,跳出遍历。
    
    此时oldFiber剩下key === 2未遍历,newChildren剩下key === 2、key === 3未遍历。
    
    上面4跳出的遍历
    
    可能newChildren遍历完,或oldFiber遍历完,或他们同时遍历完。
    

    react diff算法理解总结

    带着第一轮遍历的结果,我们开始第二轮遍历。

    第一轮遍历:(4种情况)

    - 1.newChildren与oldFiber同时遍历完 
    
    那就是最理想的情况:只有组件更新。此时Diff结束。
    
    - 2.newChildren没遍历完,oldFiber遍历完
    
    已有的DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入
    我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。
    

    react diff算法理解总结

    - 3.newChildren遍历完,oldFiber没遍历完
    
    意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的oldFiber,依次标记Deletion。
    

    react diff算法理解总结

    - 4.newChildren与oldFiber都没遍历完
    
    这意味着有节点在这次更新中改变了位置。
    
    改变了位置就需要我们处理移动的节点
    
    由于有节点改变了位置,所以不能再用位置索引i对比前后的节点,那么如何才能将同一个节点在两次更新中对应上呢?
    我们需要使用key。
    
    为了快速的找到key对应的oldFiber,我们将所有还未处理的oldFiber存入以key为key,oldFiber为value的Map中。
    

    react diff算法理解总结

    接下来遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber
    
    标记节点是否移动
    

    !既然我们的目标是寻找移动的节点,那么我们需要明确:节点是否移动是以什么为参照物?

    我们的参照物是:最后一个可复用的节点在oldFiber中的位置索引(用变量lastPlacedIndex表示)。
    
    由于本次更新中节点是按newChildren的顺序排列。
    在遍历newChildren过程中,每个遍历到的可复用节点一定是当前遍历到的所有可复用节点中最靠右的那个
    即一定在lastPlacedIndex对应的可复用的节点在本次更新中位置的后面。
    
    那么我们只需要比较遍历到的可复用节点在上次更新时是否也在lastPlacedIndex对应的oldFiber后面
    就能知道两次更新中这两个节点的相对位置改变没有。
    
    我们用变量oldIndex表示遍历到的可复用节点在oldFiber中的位置索引。
    
    如果oldIndex < lastPlacedIndex,代表本次更新该节点需要向右移动。
    
    lastPlacedIndex初始为0,每遍历一个可复用的节点,如果oldFiber >= lastPlacedIndex,则lastPlacedIndex = oldFiber。
    

    // 之前 abcd // 之后 acdb

    a(之后)vs a(之前)  
    
    key不变,可复用
    
    此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0
    
    所以 lastPlacedIndex = 0;
    
    c(之后)vs b(之前)  
    
    key改变,不能复用,跳出第一轮遍历
    
    此时 lastPlacedIndex === 0;
    
    newChildren === cdb,没用完,不需要执行删除旧节点
    
    oldFiber === bcd,没用完,不需要执行插入新节点
    
    将剩余oldFiber(bcd)保存为map
    

    // 当前oldFiber:bcd

    // 当前newChildren:cdb

    key === c 在 oldFiber中存在
    
    const oldIndex = c(之前).index;
    
    此时 oldIndex === 2;  // 之前节点为 abcd,所以c.index === 2
    
    比较 oldIndex 与 lastPlacedIndex;
    
    如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动
    
    并将 lastPlacedIndex = oldIndex;
    
    如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动
    
    在例子中,oldIndex 2 > lastPlacedIndex 0,
    
    则 lastPlacedIndex = 2;
    
    c节点位置不变
    

    // 当前oldFiber:bd

    // 当前newChildren:db

    key === d 在 oldFiber中存在
    
    const oldIndex = d(之前).index;
    
    oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3
    
    则 lastPlacedIndex = 3;
    
    d节点位置不变
    

    // 当前oldFiber:b

    // 当前newChildren:b

    key === b 在 oldFiber中存在
    
    const oldIndex = b(之前).index;
    
    oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1
    
    则 b节点需要向右移动
    

    !最终acd 3个节点都没有移动,b节点被标记为移动

    // 之前 abcd

    // 之后 dabc

    d(之后)vs a(之前)  
    key改变,不能复用,跳出遍历
    
    newChildren === dabc,没用完,不需要执行删除旧节点
    
    oldFiber === abcd,没用完,不需要执行插入新节点
    
    将剩余oldFiber(abcd)保存为map
    
    继续遍历剩余newChildren
    

    // 当前oldFiber:abcd

    // 当前newChildren dabc

    key === d 在 oldFiber中存在
    
    const oldIndex = d(之前).index;
    
    此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3
    
    比较 oldIndex 与 lastPlacedIndex;
    
    oldIndex 3 > lastPlacedIndex 0
    
    则 lastPlacedIndex = 3;
    
    d节点位置不变
    

    // 当前oldFiber:abc

    // 当前newChildren abc

    key === a 在 oldFiber中存在
    
    const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0
    
    此时 oldIndex === 0;
    
    比较 oldIndex 与 lastPlacedIndex;
    
    oldIndex 0 < lastPlacedIndex 3
    
    则 a节点需要向右移动
    

    // 当前oldFiber:bc

    // 当前newChildren bc

    key === b 在 oldFiber中存在
    
    const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1
    
    此时 oldIndex === 1;
    
    比较 oldIndex 与 lastPlacedIndex;
    
    oldIndex 1 < lastPlacedIndex 3
    
    则 b节点需要向右移动
    

    // 当前oldFiber:c

    // 当前newChildren c

    key === c 在 oldFiber中存在
    
    const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2
    
    此时 oldIndex === 2;
    
    比较 oldIndex 与 lastPlacedIndex;
    
    oldIndex 2 < lastPlacedIndex 3
    
    则 c节点需要向右移动
    

    !可以看到,我们以为从 abcd 变为 dabc,只需要将d移动到前面。 !但实际上React保持d不变,将abc分别移动到了d的后面。

    react diff算法理解总结

    react diff算法理解总结 react diff算法理解总结 react diff算法理解总结 react diff算法理解总结


    下载网 » react diff算法理解总结

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元