虚拟DOM创建: React.createElement() JSX
DOM-DIFF
DOM-DIFF就是一种比较算法,比较两个虚拟DOM的区别,也就是比较两个对象的区别。
左边的树为 旧的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;
两次节点对比
对比发现: 上下元素节点对比:
- 父元素节点相同
- 元素内部的key均为自身元素 为A B 等
- 自身元素标签一致
- 元素的内容相对之前改变
在真实开发环境中,元素内部的属性、父节点、层级等变化会较大,这个是最简单的一个理解理解一下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,且标签一致,但是内容不同,只需要更新内容即可,
目前更改的权重值为 lastPlacedIndex 为 0 【第一个数据索引】
第二轮循环
遍历virtualDOM,但在遍历之前会根据Fiber链表,创建出 Map 查找映射表 查找映射表, Map={A:旧节点, B:旧节点,...} 以key作为属性名,以旧节点作为属性值 从 virtualDOM 的第一个节点开始遍历【第一轮循环处理过的可以不管】,没处理过的则去Map映射表中,找到 相同的key 值进行对比!===> 第二轮遍历 virtualDOM ,按照key进行比较!!
如果找到相同的key的旧节点:
- 先比较标签和内容
- 然后拿旧节点的权重值(旧索引值)再和全局最高权重值(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;
运行结果:
可以对比key,在数据变更前后 DOM-DIFF遍历: 第一遍:Fiber遍历,找对应位置内容:A一致 第二遍循环:根据key找内容,发现,key一致的位置,但是内容却不相同,不同的内容进行变更,这里变更后数据 索引位置 1、2、3、4变更,原数据位置5删除
只有一个可复用节点
用索引做key,如果一旦遇到数据添加,删除修改等操作,之前的元素的索引都会改变,很难实现数据复用,基本是直接更新!
删除、更新、修改、移动等操作中,更新操作更消耗性能,因为如果有后代元素,新老节点差异较大时,需要渲染的东西很多, 如果不需要更新,移动位置等操作相对性能好很多!
在使用过程中,最好不使用索引 作为key