关注我,持续输出前端干货。
最近文章写的少,主要因为全身心投在公司重构项目上,这几天逛知乎,又被hooks刷了一波,“Dan因为写文档编不下去,不得不发布一个useEvent来圆谎”,前端界的段子写的是越来越离谱,当然,对hooks的两派(学院派和实干派)之争也是很有看头,一会某咖站实干派强调hooks在生产中就一大坑最好遵循xxx最佳实践,一会某红站学院派呼吁遵循单项数据流函数式编程巴拉巴拉,无论疫情怎么样,这边一派喜洋洋不亦乐乎。
在开发里面,我们时常会遇到Warning: Each child in a list should have a unique "key" prop.的警告,在用map来渲染一个列表的时候经常碰到。当然,我们知道这时我们需要通过加一个key来解决这个问题,在react进行diff时,通过这个key来决定该节点在原始列表中的变动。
但是,在不少场景下,我们会犯难,这个key要怎么取呢?举个例子,我们有如下一个数组:
const items = [
{
age: 10,
height: 79,
},
{
age: 11,
height: 81,
},
{
age: 10,
height: 73,
},
]
这样一个数组我们需要通过一个列表将它渲染出来。你一瞅好办,再一瞅,咦,你的id在哪儿,你不得给个user_id之类的字段来让我作为key么?
反正我家二大爷没有强制规定我给的items里面必须有user_id,因为这个字段在实际使用时我并不需要。你就会在心里有那么一些马在奔跑。
如何解决这个问题呢?
我们做前端,有一个知识点很基础,在javascript里面,对象是引用类型值,对象的内容被存放在内存堆结构中,变量只是一个指向该内存地址的一个指针。那么,我们能否像其他语言一样,通过&之类的符合取出指针的值呢?如果可以,那我用这个值(一般是个数字)作为key不就可以了吗?遗憾的是,答案是“臣妾做不到”。
虽然我们没能取出指针,但是也不妨碍我们的知识告诉我们,当数组中的对象不再是之前的那个对象时,意味着数组中该位置的值,作为引用的指针发生了变化。基于这个认知,我们何不自己构造一个存储,把上面那个指针虚拟出来呢?
let pointer = 0
const keys = [++ pointer, ++ pointer, ++ pointer]
我们创建一个pointer变量,每次需要虚拟一个指针值的时候,就++ pointer。这样我们就得到了一个和items长度相同的keys列表。这个列表就是我们需要的用来作为key的列表啦。当我们通过map使用items的元素进行渲染时,通过读取keys[i]就可以得到当前item对应的key啦。
function MyComponent({ items }) {
const keys = useUniqueKeys(items)
return (
<>
{items.map((item, i) => <span key={keys[i]}>{item.age}</span>)}
</>
)
}
接下来,我们就来实现这个神奇的useUniqueKeys。
import { useMemo, useRef } from 'react'
let pointer = 0
const getPointer = () => ++ pointer
export function useUniqueKeys(items) {
const latest = useRef()
const keys = useMemo(() => {
// TODO 实现获得 keys
}, [items])
return keys
}
以上就是我们useUniqueKeys文件的一个大体结构,接下来我们来看useMemo中的实现细节。
// 第一次运行useUniqueKeys(items)时被执行
if (!lastest.current) {
const keys = items.map(() => getPointer())
lastest.current = { items, keys }
return keys
}
当组件第一次被运行时,latest.current还是空的,就会进入这段逻辑,获得了基于items长度而得到的keys列表,并和items一起,被latest所记录,并返回keys。
接下来是比较麻烦的更新时,也就是items发生变化时。当items发生变化时,有如下几种情况:
从对应的位置来看,我们要做的,就是把items里面那些还留下来的元素所对应的key,在keys里面调整他们的位置,把移除掉的key从keys中删掉,把新增的元素通过getPointer赋予新的key与之对应。一切都是位置的调整而已。
我们看看实现方式。
const { items: prevItems, keys: prevKeys } = latest.current
const keys = []
items.forEach((item, index) => {
// 通过一个遍历来找到prevItem在新的items中的位置,并将prevKey放到新位置上
for (let i = 0, len = prevItems.length; i < len; i ++) {
const prevItem = prevItems[i]
if (item !== prevItem) {
continue
}
const prevKey = prevKeys[i]
// 当prevItem对应的prevKey已经被放到新位置后,就不需要再去动它
// TODO 这里有比较大的性能优化空间
if (keys.includes(prevKey)) {
continue
}
// 注意这里使用的是index,即新位置索引
keys[index] = prevKey
break
}
// 如果到了这里,说明item并不在prevItems里面,是一个新元素
if (!keys[index]) {
keys[index] = getPointer()
}
})
latest.current = { items, keys }
return keys
经过上面一通操作,我们发现新一轮的latest.current使用了新的内容,同时也返回了新的keys,那么在组件中keys[i]读到的就是新的key啦,当然,对于那些仍然呆在items里面的老元素,它们对应的key也没有发生变化。
最后我们把全部代码放到一起试试。
import { useMemo, useRef } from 'react';
let pointer = 0
const getPointer = () => ++ pointer
export function useUniqueKeys(items) {
const latest = useRef()
const keys = useMemo(() => {
// 第一次运行useUniqueKeys(items)时被执行
if (!latest.current) {
const keys = items.map(() => getPointer())
latest.current = { items, keys }
return keys
}
const { items: prevItems, keys: prevKeys } = latest.current
const keys = []
items.forEach((item, index) => {
// 通过一个遍历来找到prevItem在新的items中的位置,并将prevKey放到新位置上
for (let i = 0, len = prevItems.length; i < len; i ++) {
const prevItem = prevItems[i]
if (item !== prevItem) {
continue
}
const prevKey = prevKeys[i]
// 当prevItem对应的prevKey已经被放到新位置后,就不需要再去动它
// TODO 这里有比较大的性能优化空间
if (keys.includes(prevKey)) {
continue
}
// 注意这里使用的是index,即新位置索引
keys[index] = prevKey
break
}
// 如果到了这里,说明item并不在prevItems里面,是一个新元素
if (!keys[index]) {
keys[index] = getPointer()
}
})
latest.current = { items, keys }
return keys
}, [items])
return keys
}
这样,当我们下次在没有可用的id的时候,就可以方便的使用一个唯一的key啦。
今天就写这么多。