View on GitHub

essay

All about my technical essays and practicals

一道面试题引出的多年误解

用index做列表渲染的key存在什么问题

蚂蚁金服的一个面试题问:“react渲染列表组件时建议传入key。假如用数组下标作为元素的key,会有什么问题?是不是只有性能下降 ,会有其他问题么?” “列表渲染传入key提高性能”,这个问题看上去挺简单的。看过virtual dom简介的开发者都能说出,由于数组的同级比较策略,列表渲染给元素传入key,可以防止由于数组元素顺序改变导致的重新渲染。假如用数组下标作为元素的key,当数组重新排列,增加或者删除某个元素时,元素的下标会改变,导致key改变,导致元素重新渲染。 通常回答到这里就结束了。但细想一下,许多地方都不甚明了:

  1. 元素不变,key改变了就会重新渲染。那么浅拷贝一个元素,赋予同样的key,替换改元素,会发生什么?
  2. 重新渲染是指什么?得益于virtual dom,render函数运行不代表真实的dom会被改变。那么所谓的重新渲染什么情况下只会运行render函数,什么时候会真正修改dom。

    Reconciliation(重新协调)

    为了解决上面的疑惑,需要了解React的更新过程。React在render之后会得到新的虚拟节点树。通过diff新旧两个虚拟节点树,得到最终需要修改的dom。react把这些需要更新的任务做成patch暂存,最后再进行更新。这个过程React称为Reconciliation。其中diff算法是整个更新过程的关键。React基于两点假设,实现了一个启发的O(n)算法:

  3. 两个不同类型的元素将产生不同的树。
  4. 通过渲染器附带key属性,开发者可以示意哪些子元素可能是稳定的。 其中类型或者props不同的diff很好理解,但是在递归子节点时会遇到需要特殊处理的情况。 如react officials website给出的例子。 ```html
```html
<ul>
  <li >Connecticut</li>
  <li >Duke</li>
  <li >Villanova</li>
</ul>

这种情况render出来的v dom tree会导致所有子结点都被认为是需要被patch的。但实际上只要生成一个新的Connecticut,并插入到数组的开始即可。 为此,可以为每个元素添加key:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

这样render出来的新旧两个v dom会比较key相同的节点,发现没变化就不会打上patch。只需更新新增的节点<li key="2014">Connecticut</li>。 而如果用数组下标做key,则会出现下面这种情况:

<ul>
  <li key="0">Duke</li>
  <li key="1">Villanova</li>
</ul>
<ul>
  <li key="0">Connecticut</li>
  <li key="1">Duke</li>
  <li key="2">Villanova</li>
</ul>

key相同的节点进行比较,发现他们都是不同的节点,最终全部节点都会被打上patch。key不起作用。

component与pure component

上面的讨论中涉及到两个概念,render和patch。render是指运行render函数,生成虚拟节点。而Patch实现了将tree diff算法计算出来的差异队列更新到真实的DOM节点上的过程,浏览器渲染出更新的数据。运行render函数不一定会触发patch过程。

component

对于component类型的元素,父元素render后,即使props没变化,子元素也会随之重新render。为了避免数据没变化仍然运行render的情况,可以在shouldComponentUpdate(nextProps, nextState)=>boolean方法中定义自己的比较更新策略,让元素在必要的时候再运行render。

pureComponent

每次都手动实现shouldComponentUpdate比较麻烦。为此,React提供了React.PureComponent,它自带一个shallow comparison策略的shouldComponentUpdate,可以省去不少代码量。 react-redux也利用了PureComponent的特性,经过connect的组件都会被再包裹一层purecomponent。因此redux要求state是immutable,这样才能触发组件更新。

回到最初的疑惑

  1. 浅拷贝一个元素,赋予同样的key,替换改元素,会发生什么?
    • 假如是pureComponent,或者自定义了shouldComponentUpdate则不会更新
    • 假如是普通component,必定会重新render,但是因为构建出来的vdom是一样的,所以不会进行patch
  2. 重新渲染是指什么?得益于virtual dom,render函数运行不代表真实的dom会被改变。那么所谓的重新渲染什么情况下只会运行render函数,什么时候会真正修改dom。
    • React.createElement出来的v dom的类型,属性和子节点和旧的不一样时才会修改真实dom

后续
React.createElement之后发生的事情还不太清楚。抽象语法树到vdom的过程,vdom的生命周期管理,这些需要好好看一下源码。