这篇文章给大家分享的是有关html列表中的key属性有什么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
diff 算法的时间复杂度是 O(n), 它的实现是基于以下两个假设:
两个 type 不同的元素生成不同的 DOM 树;
在两次渲染中,React 能通过 key 属性判断哪一个子元素保持不变;
本文内容围绕「假设 2」展开说明 diff 算法是怎么做到 O(n) 的时间复杂度的。
如果没有 key 属性
当遍历子元素列表的时候,React 会同时遍历新旧子元素列表,一旦遇到两个列表有不同的地方,就会生成一个 mutation。
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
例如,当在子元素列表之前添加一个元素的时候,如果没有 key 属性,React 会同时遍历新旧子元素数组:
第一个 li 标签有变化,更新;
第二个 li 标签有变化,更新;
最后插入第三个 li 标签;
这个算法的时间复杂度是 O(n),但是会导致很多不必要的 DOM 操作,性能低下。如果通过一种列表对比算法能避免掉不必要的 DOM 操作,就能优化性能。
Levenshtein Distance 算法
为了使用算法优化性能,让我们对上面的问题做一个抽象,旧的子元素列表:
[1, 2, 3, 4, 5]
进行了一系列 DOM 节点的删除、插入、修改的操作之后,得到新的列表:
[6, 3, 1, 2, 5, 4]
知道了新旧的顺序求最小的插入、删除、修改操作,这是数组的最小编辑距离问题。最常见的是 Levenshtein Distance 算法。
举一个例子:求 beaut( [公式] ) 和 ebau( [公式] ) 的最小编辑距离,其中 [公式] 是未知字符。动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。定义长度为「i - 1」的字符串 str1 和长度为「j - 1」的字符串 str2 的最小编辑距离为 [公式] ,那么 beaut( [公式] )和 ebau( [公式] ) 的最小编辑记 [公式] ,[公式] 能且只能基于下面三个状态:
「状态 1」 :beaut( [公式] ) 和 ebau 的最小编辑距离已知,记为 [公式] ;
「状态 2」:beaut 和 ebau( [公式] ) 的最小编辑距离已知,记为 [公式] ;
「状态 3」:beaut 和 ebau 的最小编辑距离已知,记为 [公式] ;
[公式] 和三个状态之间的关系分别为下面三个公式:
[公式]
[公式]
[公式]
解释一下第三个公式,在 [公式] 的情况下, [公式] ;如果 [公式] ,那么 [公式] 。那么上面三个公式值最小的那个就是 [公式] 的解。对照着下图看,也就是 [公式] 的值可以通过其上方,左边,左上对角线的值确定,公式如下:
[公式]
[公式]
[公式]
function minDistance(s1, s2) {
const len1 = s1.length
const len2 = s2.length
let matrix = []
for (let i = 0; i <= len1; i++) {
// 构造二维数组
matrix[i] = new Array()
for (let j = 0; j <= len2; j++) {
// 初始化
if (i == 0) {
matrix[i][j] = j
} else if (j == 0) {
matrix[i][j] = i
} else {
// 进行最小值分析
let cost = 0
if (s1[i - 1] != s2[j - 1]) { // 相同为0,不同置1
cost = 1
}
const temp = matrix[i - 1][j - 1] + cost
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, temp)
}
}
}
return matrix[len1][len2] //返回右下角的值
}
从上面的代码可以看出,LD 算法有两个嵌套的 for 循环,所以时间复杂度是 O(n*m),其中 n 为 str1 的长度,m 为 str2 的长度。如果采用 LD 算法,虽然能避免掉不必要的 DOM 操作,但是 diff 算法的时间复杂度就达不到线性了。
key 属性的引入思路
直接应用 LP 算法不能解决我们的问题。我们并不需要找到真正的最小编辑距离,而是需要找到一种算法,这个算法的时间复杂度必须是 O(n),并且能避免掉大部分的 DOM 操作,而前端的列表大部分的操作是子元素的移动。通过引入 key 属性唯一标识子元素,我们可以把最小编辑距离转化成:key 属性是对这个元素的唯一标识,在这个条件下,求新旧子元素列表的最小插入、删除、移动操作。这是一种启发式算法。
旧的子元素列表:
[{ key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 }]
新的子元素列表:
[{ key: 4, val: 4 }, { key: 1, val: 1 }, { key: 3, val: 3 }, { key: 2, val: 2 }]
React 子元素近似最小编辑距离算法
为了寻找上面两个数组的近似最小编辑距离,React 的做法为,正向遍历新的子元素,用新的子元素的 key 值去旧的子元素中查找,如果没找到,就做插入;如果找到了就做移动操作;如果遇到旧的子元素在新的列表中找不到的情况,删除旧的子元素。算法的时间复杂度是 O(n)。 举一个例子,如下图,上一排是旧的子元素列表,下一排是新的子元素列表:
代码如下,正向遍历 nextChildren:
「元素 6」: 是新增元素,新增到 index = 0 的位置;
「元素 3」: 不变;
「元素 1」: 元素 1 在原数组中的位置在元素 3 之前,所以需要移动到元素 3 的后面;
「元素 2」: 元素 2 在原数组中的位置也位于 3 之前,移动到元素 1 的后面;
「元素 5」: 不变;
「元素 4」: 移动到 5 后面;
updateChildren() {
// find removed
const removedNodes = findRemoved();
var updates = [];
var lastIndex = 0;
var nextIndex = 0;
var lastPlacedNode = null;
for (const name in nextChildren) {
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 移动子元素
if (prevChild._mountIndex < lastIndex) {
updates.push({
type: "MOVE_EXISTING",
fromIndex: prevChild._mountIndex,
toIndex: nextIndex,
afterNode: afterNode
})
}
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
}
// 新增子元素
updates.push({
type: "INSERT_MARKUP",
toIndex: nextIndex,
content: nextChild
});
}
nextIndex++;
lastPlacedNode = getNativeNode(nextChild); // 获取 nextChild 对应的 DOM 节点
}
// 删除
for (const name in removedNodes) {
updates.push({
type: "REMOVE_NODE",
content: null,
fromIndex: prevChildren[name]._mountIndex,
fromNode: removedNodes[name]
})
}
}
和 Vue 的比较
Vue 对子元素的 diff 的思路和 React 一样,都引入了「key 是子元素的唯一标识」这一先决条件。在引入这一条件后,比 React 做了更多的优化,Vue 是从新老子元素列表的两头向中间遍历,并多做了一些特殊判断。就列表更新这一块,Vue 的性能高于 React。而在 Fiber 架构中,由于没有反向指针,React 不容易做到通过双向遍历优化子元素 diff 算法。
key 属性最佳实践
key 属性帮助 React 识别哪些元素改变了,哪些元素是新增的,哪些被删除了。元素列表中的元素的 key 值应该是稳定的,能起到唯一标识的作用。下面列举一些最佳实践:
1. 使用能在子元素列表之间能唯一标识这个子元素的字符串做为其 key 属性。用数组的 index 做为 key 值是一种反模式,需要避免。
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
2. key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值。
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
感谢各位的阅读!关于“html列表中的key属性有什么用”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。