zoukankan      html  css  js  c++  java
  • React/Vue里的key到底有什么用?看完这篇你就知道了!(附demo代码)

    网上有很多博客讲到,React、Vue里的key,与 Virtual DOM 及 DOM diff 有关, 可以用来唯一标识DOM节点,提高diff效率,云云。

    这大致是对的,但是,大多讲得语焉不详,像是在背答案。

    具体怎么个提效法?为什么说用数组下标当作key是“反模式”?讲了一堆,能不能来个眼见为实,show me the code?

    本文以React为例,尝试稍微刨一刨,但又不刨到太底层,以足够帮助理解为度。

    1. VNode diff

    首先介绍 Virtual DOM 结点(后续简称Virtual Node, VNode)是如何创建出来的。

    现实中的React项目几乎都会用到JSX,而JSX不能直接执行,需要先经babel编译成js代码,比如:

    <div className="content">Hello world!</div>

    会被编译成

    React.createElement("div", {
        className: "content"
    }, "Hello world!");

    点击这里查看在线编译

    所以,只要调用 React.createElement 这个静态方法,就可以创建出一个VNode。

    无需深入VNode 的具体数据结构,只要看看这个工厂方法的参数,就可以知道 DOM diff 到底 diff 了哪些内容。

    根据React官方文档,该方法可以接收≥3个参数:

    • 第一个参数是type,指定结点类型,如果是HTML原生结点,那么会是一个字符串,比如"div";如果是React组件,那么就会是一个class或function;
    • 第二个参数是props,是一个对象或者null。比如前面的例子中,div标签上的"className"属性就被加到这里来了;
    • 第三(及第四,第五,……)个参数是childNode,该结点的子节点。前面的例子中,div的子节点是一个内容为"Hello world!"的TextNode

    是滴,DOM diff 具体diff 的东西,就是这几个参数。为什么不会有别的?因为那样不符合React的设计理念:Data => UI 单向映射。

    2. 动态列表的diff困局

    我们知道React在调用setState触发render时,会对新旧 Virtual DOM 做比较,力争以最小的代价完成新DOM渲染任务。

    结合上面提到的几个参数,具体比较过程大致是这样的:

    • 首先比较type。如果type不同,那没什么好说的,直接销毁重新create一个;如果type相同,再往后看:
    • 其次比较props,如果有变化,那就把变化的部分update;如果没变化,那就再往后看:
    • 最后比较子节点,同样地,有变化就update,没变化就啥都不做

    这在DOM结构固定的一般情况下是很好用的,但当我们希望从一个list映射出列表、而且这个list里的项随时可能变化时,就有点麻烦了。

    比如说,原本list是这样的:

    [
      {name: 'Smith', job: 'Engineer'},
      {name: 'Alice', job: 'HR'},
      {name: 'Jenny', job: 'Designer'}
    ]

    然后,Jenny被移到了最前面,那么Smith和Alice就相应后移了,变成了

    [
      {name: 'Jenny', job: 'Designer'},
      {name: 'Smith', job: 'Engineer'},
      {name: 'Alice', job: 'HR'}
    ]

    对于React来说,如果它不知道这三个结点“本来”是谁,只是按照位置对应关系逐个去检查,会发现每个结点都变了:

    • Smith => Jenny
    • Alice => Smith
    • Jenny => Alice

    于是React得出结论:列表中的所有结点,全都需要update,重新渲染!

    且慢!有没有更好的方法?

    3. 借助key破局

    如果,React“知道”这三个结点“本来”是谁,那么事情就会简单很多:

    不需要更新任何DOM结点,只需把Jenny对应的结点摘下来,再插入到新的位置,完事。

    但React怎么会知道谁是谁呢?

    这需要我们开发者手动告诉它,于是key出场了。

    在做DOM diff 时,如果同一个父组件下的两个VNode拥有同样的key,就会被视为同一个结点,如果React据此判断出,这个结点在列表中的排位发生了变化,就会像上面说的那样,进行“摘下-插入”处理。

    为了证明这一点,亮代码!

    首先上一个故意整出bug的版本:

    class App extends React.Component {
      state = {
        list: [0, 1, 2]
      }
    
      add() {
        const list = this.state.list;
        this.setState({ list: [list.length, ...list] });
      }
    
      render() {
        return (
          <div className="App">
            <button onClick={() => this.add()}>Input sth below, then click me</button>
            <ul>
              {
    // 注意:这里故意用index作为key,引发bug
    this.state.list.map((item, index) => ( <li key={index}> <span>Item-{item}</span> <input type="text" /> </li> ) ) } </ul> </div> ); } }

    ReactDOM.render(
    <App />,
    document.getElementById('root')
    );

    可以用 create-react-app起个项目,在本地试试这段代码。演示效果如下,先在第二行文本框里输入一些1:

    然后,点击上面的按钮,会发现……

    输入了一串1的文本框没有跟着Item-1走,而是留在了“原位”!

    这就是用数组下标作key引发的典型bug。原因就在于新列表里Item-0和原列表里的Item-1拥有同样的key,被React视为同一个结点,所以只是“就地”更新了子节点(文本),并没有挪动结点的位置。

    而这个bug的巧妙之处就在于使用了<input>,它可以在VNode的type、props、children均无变化的前提下,被用户行为改变其样式(输入的内容),从而让我们直观地看到结点所处位置。感谢React官方提供了这个巧妙的case

    好,下面我们来修复这个bug。

    修复方法很简单:把 key={index} 改成 key={item} 就行了。

    保存,刷新重试,我们就可以得到:

    这下,对应关系正确了,React正确地识别出了3个旧结点,直接把新结点插入到列表开头,而旧结点没有变化。

    看到这里,你应该明白key到底有什么用,以及为什么index不宜做key了吧。

    另外,如果没有指定key,那么React会默认使用index作为key,所以,只要是动态列表,为了性能着想,请尽量用unique id作为key。

  • 相关阅读:
    Struts tags--Data tags
    Java NIO学习笔记七 Non-blocking Server
    Java NIO学习笔记六 SocketChannel 和 ServerSocketChannel
    Java NIO学习笔记五 FileChannel(文件通道)
    Java NIO学习笔记四 NIO选择器
    Java NIO学习笔记 三 散点/收集 和频道转换
    SpringMVC接收集合页面参数
    JAVA NIO学习笔记二 频道和缓冲区
    Java NIO学习笔记一 Java NIO概述
    通过举例了解java中的流
  • 原文地址:https://www.cnblogs.com/leegent/p/14687396.html
Copyright © 2011-2022 走看看