• 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。

  • 相关阅读:
    centos 安装 TortoiseSVN svn 客户端
    linux 定时任务 日志记录
    centos6.5 安装PHP7.0支持nginx
    linux root 用户 定时任务添加
    composer 一些使用说明
    laravel cookie写入
    laravel composer 安装指定版本以及基本的配置
    mysql 删除重复记录语句
    linux php redis 扩展安装
    linux php 安装 memcache 扩展
  • 原文地址:https://www.cnblogs.com/leegent/p/14687396.html
Copyright © 2020-2023  润新知