前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >都是hooks的错,非要让我造个轮来解决list should have a unique "key" prop问题

都是hooks的错,非要让我造个轮来解决list should have a unique "key" prop问题

作者头像
否子戈
发布2022-06-24 15:35:15
5710
发布2022-06-24 15:35:15
举报
文章被收录于专栏:

关注我,持续输出前端干货。

最近文章写的少,主要因为全身心投在公司重构项目上,这几天逛知乎,又被hooks刷了一波,“Dan因为写文档编不下去,不得不发布一个useEvent来圆谎”,前端界的段子写的是越来越离谱,当然,对hooks的两派(学院派和实干派)之争也是很有看头,一会某咖站实干派强调hooks在生产中就一大坑最好遵循xxx最佳实践,一会某红站学院派呼吁遵循单项数据流函数式编程巴拉巴拉,无论疫情怎么样,这边一派喜洋洋不亦乐乎。

在开发里面,我们时常会遇到Warning: Each child in a list should have a unique "key" prop.的警告,在用map来渲染一个列表的时候经常碰到。当然,我们知道这时我们需要通过加一个key来解决这个问题,在react进行diff时,通过这个key来决定该节点在原始列表中的变动。

但是,在不少场景下,我们会犯难,这个key要怎么取呢?举个例子,我们有如下一个数组:

代码语言:javascript
复制
const items = [
  {
    age: 10,
    height: 79,
  },
  {
    age: 11,
    height: 81,
  },
  {
    age: 10,
    height: 73,
  },
]

这样一个数组我们需要通过一个列表将它渲染出来。你一瞅好办,再一瞅,咦,你的id在哪儿,你不得给个user_id之类的字段来让我作为key么?

反正我家二大爷没有强制规定我给的items里面必须有user_id,因为这个字段在实际使用时我并不需要。你就会在心里有那么一些马在奔跑。

如何解决这个问题呢?

我们做前端,有一个知识点很基础,在javascript里面,对象是引用类型值,对象的内容被存放在内存堆结构中,变量只是一个指向该内存地址的一个指针。那么,我们能否像其他语言一样,通过&之类的符合取出指针的值呢?如果可以,那我用这个值(一般是个数字)作为key不就可以了吗?遗憾的是,答案是“臣妾做不到”。

虽然我们没能取出指针,但是也不妨碍我们的知识告诉我们,当数组中的对象不再是之前的那个对象时,意味着数组中该位置的值,作为引用的指针发生了变化。基于这个认知,我们何不自己构造一个存储,把上面那个指针虚拟出来呢?

代码语言:javascript
复制
let pointer = 0
const keys = [++ pointer, ++ pointer, ++ pointer]

我们创建一个pointer变量,每次需要虚拟一个指针值的时候,就++ pointer。这样我们就得到了一个和items长度相同的keys列表。这个列表就是我们需要的用来作为key的列表啦。当我们通过map使用items的元素进行渲染时,通过读取keys[i]就可以得到当前item对应的key啦。

代码语言:javascript
复制
function MyComponent({ items }) {
  const keys = useUniqueKeys(items)
  return (
    <>
      {items.map((item, i) => <span key={keys[i]}>{item.age}</span>)}
    </>
  )
}

接下来,我们就来实现这个神奇的useUniqueKeys。

代码语言:javascript
复制
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中的实现细节。

代码语言:javascript
复制
// 第一次运行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与之对应。一切都是位置的调整而已。

我们看看实现方式。

代码语言:javascript
复制
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也没有发生变化。

最后我们把全部代码放到一起试试。

代码语言:javascript
复制
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啦。

今天就写这么多。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 唐霜 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档