虚拟DOM-DIFF算法了解

98 阅读4分钟

虚拟dom (virtual dom)(vnode)

虚拟DOM创建: React.createElement() JSX

DOM-DIFF

DOM-DIFF就是一种比较算法,比较两个虚拟DOM的区别,也就是比较两个对象的区别。

图片.png

左边的树为 旧的virtual DOM或者是Fiber链表,右边的为新的 virtual DOM

比较原则:

1.只能比较同级

在比较元素时,先比较树的根组件,div元素相同,继续向下比较

2.深度优先原则

在对比元素时,发现该元素有子元素时,继续向下比较,比较完子元素之后,继续遍历兄弟元素

目的:尽可能减少页面更新和渲染

开发过程中:

1.应减少节点的层级结构变化 元素比较为同级比较,如果层级结构发生变化,即时组件内内容未变化,最后进行的也是旧节点删除,新节点创建的操作,比较消耗性能!

2.节点复用

例子:

Test.jsx

import React, { useState, useEffect } from "react";
import styled from "styled-components";

const TestBox = styled.div`
    display: flex;
    div{
        margin-right: 10px;
        width: 100px;
        height: 100px;
        text-align: center;
        line-height: 100px;
        background: lightpink;
        font-size: 18px;
    }
`;

let n = 0;
const Test = function Test() {
    n++;
    let [state, setState] = useState(['A', 'B', 'C', 'D', 'E', 'F']);
    useEffect(() => {
        setTimeout(() => {
            setState(['A', 'C', 'E', 'B', 'G', 'F']);
        }, 2000);
    }, []);

    return <TestBox>
        {state.map(item => {
            return <div key={item}>
                {n > 1 ? `${item}-NEW` : item}
            </div>;
        })}
    </TestBox>;
};
export default Test;

两次节点对比

图片.png

对比发现: 上下元素节点对比:

  1. 父元素节点相同
  2. 元素内部的key均为自身元素 为A B 等
  3. 自身元素标签一致
  4. 元素的内容相对之前改变

在真实开发环境中,元素内部的属性、父节点、层级等变化会较大,这个是最简单的一个理解理解一下DOM-DIFF

DIFF操作

初始渲染

在第一次渲染为真实DOM,创建成Fiber链表格式的节点,也就是旧节点, A

在两秒后,按照最新的数据,创建出全新的virtualDOM ,A-NEW

由A -> 变成 A-NEW的这个操作过程,其中内部虚拟DOM对比就是通过DIFF算法

具体处理步骤

第一次循环

遍历Fiber列表,去virtualDOM找相同位置的新节点,进行对比【不是按照key对比,按照位置比】 对比的时候,先看key值

key值一样:再看标签名和内容

  • 标签一样,内容也一样,则复用旧节点
  • 标签一样,内容不一样,则把旧节点标记为 4[更新]
  • 标签不一样,则把旧节点标记为 8[删除],新节点标记为 2[新增] key不一样,直接跳出更新

在上图中,Fiber 链表和新的 virtualDOM 对比,发现Fiber的第一个位置和virtualDOM的第一个位置 key均为A,且标签一致,但是内容不同,只需要更新内容即可,

图片.png

目前更改的权重值为 lastPlacedIndex 为 0 【第一个数据索引】

第二轮循环

遍历virtualDOM,但在遍历之前会根据Fiber链表,创建出 Map 查找映射表 查找映射表, Map={A:旧节点, B:旧节点,...} 以key作为属性名,以旧节点作为属性值 从 virtualDOM 的第一个节点开始遍历【第一轮循环处理过的可以不管】,没处理过的则去Map映射表中,找到 相同的key 值进行对比!===> 第二轮遍历 virtualDOM ,按照key进行比较!!

如果找到相同的key的旧节点:

  1. 先比较标签和内容
  2. 然后拿旧节点的权重值(旧索引值)再和全局最高权重值(lastPlacedIndex),决定位置是否挪动,设置 旧索引 N, 全局索引 M 当N >= M 位置不变,让M = N N < M 要挪动旧节点位置[标记为6,并记录挪动位置](一般处于上一个处理后的节点后面)

如果找不到相同key的旧节点:说明词节点是新增的,标记为2

对比

根据上面的例子对比:

第一遍循环,遍历Fiber,按照位置对比

节点A 位置不变 权重值 lastPlacedIndex = 0

第二遍循环,遍历 virtualDOM,按照key对比

1.新的virtualDOM 的第二个节点是 C, 去遍历表里面找key为C的节点,找到的节点C 在map内的索引为2,所以 lastPlacedIndex = 2

同时C的key 、标签、等没有变化,只有内容改变,所以只对C进行修改,状态标记为4,C的位置和之前位置一样,都在A后面(中间没有的元素先跳过)

2.E和C节点类似,找到E,标记状态为4,Fiber内C的索引为4,lastPlacedIndex = 4,E的位置和之前位置一样,都在C后面(中间没有的元素先跳过)

3.key为B的B-NEW节点,在Map内的索引为 1,小于之前的 lastPlacedIndex, lastPlacedIndex不变还是4 ,B的key等没有变化除了内容,进行修改加挪动位置,标记为 6(移动)

4.key为G的G-NEW节点,在Map中找不到相同的key,标记为2[新增],

5.key为F的F-NEW节点,找到的的索引为5,lastPlacedIndex = 5

6.最后将Fiber树内的没有比较过的旧节点设置为8[删除]

经过DOM-DIFF算法对比后,最后对比发现 删除节点:D 更新节点(可复用):A、C、E、F 挪动位置:B

总结

如果组件更新后,节点key值所在顺序没有变化,只需要经过第一轮循环就可以分析节点更新规则!

key值

组件内的key值很重要,当相同的标签分别使用index和id做为key值时,例如:相同的元素:

例子:

import React, { useState, useEffect } from "react";
import styled from "styled-components";

const TestBox = styled.div`
    .box {
        display: flex;
        div{
            width: 100px;
            height: 100px;
            margin: 10px;
            text-align: center;
            line-height: 100px;
            background: lightpink;
        }
    }
`;

let n = 0;
const Test = function Test() {
    n++;
    let [state, setState] = useState(['A', 'B', 'C', 'D', 'E', 'F']);
    let [state1, setState1] = useState(['A', 'E', 'B', 'G', 'F']);

    return <TestBox>
        <div className="box">
            {state.map((item, index) => {
                return <div key={index}>
                    {item} --id: {index}
                </div>;
            })}
        </div>

        <div className="box">
            {state1.map((item, index) => {
                return <div key={index}>
                    {item} --id: {index}
                </div>
            })}
        </div>
    </TestBox>;
};
export default Test;

运行结果:

图片.png

可以对比key,在数据变更前后 DOM-DIFF遍历: 第一遍:Fiber遍历,找对应位置内容:A一致 第二遍循环:根据key找内容,发现,key一致的位置,但是内容却不相同,不同的内容进行变更,这里变更后数据 索引位置 1、2、3、4变更,原数据位置5删除

只有一个可复用节点

用索引做key,如果一旦遇到数据添加,删除修改等操作,之前的元素的索引都会改变,很难实现数据复用,基本是直接更新!

删除、更新、修改、移动等操作中,更新操作更消耗性能,因为如果有后代元素,新老节点差异较大时,需要渲染的东西很多, 如果不需要更新,移动位置等操作相对性能好很多!

在使用过程中,最好不使用索引 作为key