react解析 React.Children(二)

HerryLo發表於2019-05-12

感謝 yck: 剖剖析 React 原始碼解析,本篇文章是在讀完他的文章的基礎上,將他的文章進行拆解和加工,加入我自己的一下理解和例子,便於大家理解。覺得yck寫的真的很棒 。React 版本為 16.8.6,關於原始碼的閱讀,可以移步到yck react原始碼解析

本文永久有效連結: react解析 React.Children(二) 

在React實際開發中, React.Children 這個API我們雖然使用的比較少, 但是我們通過這個API可以操作children, 可以檢視文件

我們來看下這個API的神奇用法

React.Children.map(this.props.children, c => [[c, c]])複製程式碼

下面可以看一下它在專案中的實際用法

控制檯列印渲染的節點和props,如下圖 從上圖可以得知,通過 c => [[c, c]] 轉換以後節點變為了:

// 通過 c => [[c, c]] 轉換以後
<div>
    <p>1</p>
    <p>1</p>
    <p>2</p>
    <p>2</p>
</div>複製程式碼

我們需要定位到 ReactChildren.js 檔案,檢視程式碼, React.Children.map 方法實際就是mapChildren函式,讓我們來看看 mapChildren 內部到底是如何實現的吧!

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 這裡是處理 key,不關心也沒事
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}複製程式碼

getPooledTraverseContext 和 releaseTraverseContext 中的程式碼, 引入了物件重用池的概念。這個概念的用處就是維護一個大小固定的物件重用池,每次從這個池子裡取一個物件去賦值,用完之後就將物件上的屬性清空然後丟回池子。維護這個池子的用意就是提高效能,避免頻繁建立銷燬多屬性物件。

雖然在呼叫了traverseAllChildren函式,實際呼叫的是traverseAllChildrenImpl方法。

function traverseAllChildrenImpl( children, nameSoFar, callback,traverseContext ) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    children = null;
  }

  let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch (children.$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }
  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0;
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    }
  }

  return subtreeCount;
}複製程式碼

這個函式首先 判斷 children 的型別, 若children為陣列,繼續遞迴呼叫traverseAllChildrenImpl,直到處理成單個可渲染的節點,然後呼叫才能呼叫callback,也就是mapSingleChildIntoContext

最後讓我們來讀一下 mapSingleChildIntoContext 函式的實現。

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}複製程式碼

mapSingleChildIntoContext函式其實就是呼叫React.Children.map(children, callback)中的callback. 如果map之後還是陣列, 那麼再次進入mapIntoWithKeyPrefixInternal, 那麼這個時候我們就會再次從物件重用池裡面去獲取context, 而物件重用池的意義也就是在這裡, 如果迴圈巢狀多了, 可以減少很多物件建立和gc的損耗. 如果不是陣列, 判斷返回值是否是有效的 Element, 驗證通過的話就 clone 一份並且替換掉 key, 最後把返回值放入 result 中, result 其實也就是 mapChildren 的返回值.

下面是程式碼的呼叫順序:

mapChildren 函式
     |
    \|/
mapIntoWithKeyPrefixInternal 函式     
     |
    \|/
traverseAllChildrenImpl函式(迴圈成單個可渲染的節點,如果不是遞迴)
     |    
     |單個節點
    \|/mapSingleChildIntoContext函式(判斷是否是有效Element, 驗證通過就 clone 並且替換掉 key,
並將值放入result,result就是map的返回值)複製程式碼

更多內容:

react解析: React.createElement(一)

參考:

yck: 剖剖析 React 原始碼

Jokcy 的 《React 原始碼解析》: react.jokcy.me/


相關文章