小前端读源码 - React(浅析Keys原理)

小前端读源码 - React(浅析Keys原理)

在使用React的时候,我们经常无法避免使用循环去渲染元素。例如我们有一个商品列表,我们就需要根据后端提供的接口(一般是一个数组)循环渲染出商品信息。在渲染的商品组件中,如果不填写一个key给循坏渲染的组件,那么React将会提示一个警告。

在React的官网文档中有说道,循坏渲染组件需要为组件添加一个兄弟组件之间唯一的key作为标识。

相信很多人都知道,React会根据这个key去决定是否重复使用组件。那么我们就看看在React内部,他是如何去判断这个Key,以及如何去重用组件的。


本篇文章都会基于以下demo展开:

class App extends React.Component {
    constructor() {
        super();
        this.state = {
            divList: [
                    id: 'a1',
                    text: '1'
                    id: 'a2',
                    text: '2'
                    id: 'a3',
                    text: '3'
    render() {
        return (
            <div id='a1'>
                <button onClick={() => {
                    this.setState({status: 2})
                }}>setState</button>
                    this.state.divList.map((item, key) => {
                        return <div>{item.text}</div>

React是什么时候验证我们的循环渲染组件没有添加keys呢?

首先我们编写循环渲染的组件一边都是这样写的:

this.state.divList.map((item, key) => {
  return <div>{item.text}</div>

经过babel编译之后是这样的:

_createClass(App, [{
    key: "render",
    value: function render() {
      var _this2 = this;
      return __WEBPACK_IMPORTED_MODULE_5_react___default.a.createElement("div", {
        id: "a1"
      }, __WEBPACK_IMPORTED_MODULE_5_react___default.a.createElement("button", {
        onClick: function onClick() {
          _this2.setState({
            status: 2
      }, "setState"), this.state.divList.map(function (item, key) {
        return __WEBPACK_IMPORTED_MODULE_5_react___default.a.createElement("div", null, item.text);

在初次渲染的时候,会对App类的div进行实例,通过react.createElement对App类的div转为一个ReactDOM对象。在转换的时候,会对div的children也转化,当碰到map渲染的时候,那么div的其中一个children的类型就为数组了,那么在转换div的时候发现有其中一个children是一个数组,那么React就会对数组进行验证是否带有keys。

function validateChildKeys(node, parentType) {
  if (typeof node !== 'object') {
    return;
  // 检查数组中的item是否有keys
  if (Array.isArray(node)) {
    for (var i = 0; i < node.length; i++) {
      var child = node[i];
      if (isValidElement(child)) {
        validateExplicitKey(child, parentType);
  } else if (isValidElement(node)) {
    // This element was passed in a valid location.
    if (node._store) {
      node._store.validated = true;
  } else if (node) {
    var iteratorFn = getIteratorFn(node);
    if (typeof iteratorFn === 'function') {
      // Entry iterators used to provide implicit keys,
      // but now we print a separate warning for them later.
      if (iteratorFn !== node.entries) {
        var iterator = iteratorFn.call(node);
        var step = void 0;
        while (!(step = iterator.next()).done) {
          if (isValidElement(step.value)) {
            validateExplicitKey(step.value, parentType);

React是如何利用Keys的?

我们修改一下demo。

class App extends React.Component {
    constructor() {
        super();
        this.state = {
            divList: [
                    id: 'a1',
                    text: '1'
                    id: 'a2',
                    text: '2'
                    id: 'a3',
                    text: '3'
    render() {
        return (
            <div id='a1'>
                <button onClick={() => {
                    this.setState({
                        divList: [
                                id: 'a2',
                                text: '2'
                                id: 'a1',
                                text: '1'
                                id: 'a3',
                                text: '3'
                }}>setState</button>
                    this.state.divList.map((item, key) => {
                        return <div>{item.text}:<input defaultValue='' /></div>

在demo中我们先不为每个item添加key。

我先填入一些数据。

点击setState。

不知道大家发现问题没有,顺序是调转了,但是input的内容并没有根据顺序变化而变化,还是没有改变顺序。

如果我们为每个循环渲染的组件叫上key,在进行顺序变化会发现input也会跟着顺序变化。

这是为什么呢?通过阅读源码以及断点查看,我们看看带上key的组件在改变顺序后重新渲染会是如何进行的。

首先在beginWork的时候可以看到,因为当前处理的Fiber节点是一个数组,所以会当成Fragment来进行处理。通过断点观看,可以看到传入的组件位置已经根据state的不同进行了修改。

可以看到当前数组的child还没有发生变化!

当前的workInProgress.child是key为a1的div。

React会对当前数组进行第一次循环,获取每个子节点的key值生成一个Set数据 knownKeys

{
      // First, validate keys.
      var knownKeys = null;
      for (var i = 0; i < newChildren.length; i++) {
        var child = newChildren[i];
        knownKeys = warnOnInvalidKey(child, knownKeys);
      // Set(2) {"a2", "a1"}

接着react会调用 updateSlot 函数,会对旧的数组的第一个子元素和新数组的第一个子元素传入进行对比。

 {
    // key是否相同
    if (newChild.key === key) {
        // 是否为多维数组
        if (newChild.type === REACT_FRAGMENT_TYPE) {
            return updateFragment(returnFiber, oldFiber, newChild.props.children, expirationTime, key);
        // 更新组件
        return updateElement(returnFiber, oldFiber, newChild, expirationTime);
    } else {
        // 当前因为keys从a1变成了a2,所以会返回null
        return null;

接着react会调用 mapRemainingChildren 函数。

function mapRemainingChildren(returnFiber, currentFirstChild) {
    // Add the remaining children to a temporary map so that we can find them by
    // keys quickly. Implicit (null) keys get added to this set with their index
    var existingChildren = new Map();
    var existingChild = currentFirstChild;
    while (existingChild !== null) {
      // 是否有key
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      // 没有key的情况下,使用元素所在下标作为key
      } else {
        existingChildren.set(existingChild.index, existingChild);
      existingChild = existingChild.sibling;
    return existingChildren;// Map(2) {"a1" => FiberNode, "a2" => FiberNode}

从这里我们知道,其实就算我们不传入key,在更新的时候,React也会帮我们设置key到对应的元素中。

然后进入另外一个循环,这个循环会循环执行 updateFromMap 函数,分别会传入existingChildren(根据旧数组得出的Map数据), returnFiber, newIdx, newChildren[newIdx](新数组中,当前下标的子节点), expirationTime这些参数。

React会根据旧数据中当前循环的item和新数据的item进行对比,最终决定如何更新。

function updateElement(returnFiber, current$$1, element, expirationTime) {
    // 新旧数据的元素类型是否一致
    if (current$$1 !== null && current$$1.elementType === element.type) {
      // 使用旧的Fiber,更新旧的fiber中的props和对应的数据。
      var existing = useFiber(current$$1, element.props, expirationTime);
      existing.ref = coerceRef(returnFiber, current$$1, element);
      existing.return = returnFiber;
        existing._debugSource = element._source;
        existing._debugOwner = element._owner;
      return existing;
    } else {
      // Insert
      var created = createFiberFromElement(element, returnFiber.mode, expirationTime);