React源碼 React.Children

children是什麼意思呢?就是咱們拿到組件內部的props的時候,有props.children這麼一個屬性,大部分狀況下,咱們直接把 props.children 渲染到 JSX 裏面就能夠了。不多有狀況咱們須要去操做這個 children 。 可是若是一旦你須要去操做這個 children 。咱們推薦使用 React.children 的 api , 而不是直接去操做他。

 

雖說咱們大部分狀況下拿到的 children 是合理的 react element 或者是一個數組,可是 React 有提供 api 去操做他,那麼他必定是有一個合理的緣由的。

 

打開 React.js 源碼,找到 children
Children: {
    map,
    forEach,
    count,
    toArray,
    only,
}

這個 children 是個對象,這個對象裏面有 5 個屬性,這個 5 個屬性看上去就跟咱們數組的操做很是的像,前兩個是最重要的,也就是 map 和 forEach ,就至關於一個數組的 map 和 forEach 方法,咱們能夠理解爲意義是同樣的,可是實際的操做跟數組的 map 和 forEach 會有一點的區別。 這裏 map 是全部邏輯裏面最複雜的一個。而 map 和 forEach 是差很少的,他們惟一的區別是一個有返回,一個沒有返回,map 是經過咱們傳入一個方法以後,調用出來以後,返回的一個新的數組,而 forEach 是返回原數組的。react

 

咱們先看一個 demo,

 

import React from 'react'

function ChildrenDemo(props) {
  console.log(props.children)
  console.log(React.Children.map(props.children, c => [c, [c, c]]))
  return props.children
}

export default () => (
  <ChildrenDemo>
    <span>1</span>
    <span>2</span>
  </ChildrenDemo>
)

咱們建立了一個組件,叫 ChildrenDemo ,而後裏面包裹了兩個 span 做爲他的 children ,而後在他的 props.children 裏面就能夠拿到,第一個打印出來的就是 props.children,咱們看到就是兩個 element 節點。 就跟以前的 React Element 屬性是同樣的。第二個是經過 React.Children.map 這個 api ,而後傳入 props.children 。 而且傳入了一個 callback ,這個 callback 返回的是一個嵌套兩層的數組,可能比較迷茫,能夠先當作 c => [c, c],而後打印出來的children分別是1,1,2,2,咱們是否能夠理解爲他最終返回的是一個展開的,由於這總共是兩個節點,props.children 是兩個節點,每一個節點經過 map function 以後返回的是一個數組,而後 React.Children 把他給展開了,而後就變成了一個一維數組,原本是返回一個二維數組,而後這個數組裏面有兩個一維數組。api

 

也就是這裏無論嵌套幾層,返回的一維數組。這就是 React.Children.map 跟 數組 .map 的一個本質的區別,數組map第一個參數是子元素,這裏的第一個參數是要遍歷的數組,而後返回一個一維數組。咱們接下來看下他的源碼是如何作的。打開 ReactChildren.js 。翻到最下面,看到

 

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

mapChildren as map,這裏 export 出去的 map ,在裏面就是 mapChildren 這個方法數組

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

找到 mapChildren 這個方法,這裏面先判斷這個 children 是否等於 null ,若是等於 null ,就直接返回了。而後聲明瞭一個 result ,最終 return 的也是這個 result 。這個時候咱們再去看看 forEachChildren瀏覽器

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

這裏面的 forEachChildren 沒有result ,沒有返回值。這就是他們一個本質的區別。app



mapChildren 裏面調用了 mapIntoWithKeyPrefixInternal,傳入了 children , result 是咱們剛剛聲明的,第三個是 null, function 是咱們傳入的第二個參數,context 就是 this.object ,通常咱們不用他,就無論這個東西 
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

mapIntoWithKeyPrefixInternal 裏面咱們看到他先處理了一下 key,這個 key 先忽略,由於他就是一個字符串處理相關的東西,沒有什麼特別的,下面再調用這個三個函數,跟 forEachChildren 對比下,是差很少的。經過調用 getPooledTraverseContext ,而後去獲取了一個 traverseContext ,這個東西有什麼意義呢?咱們直接看下這個方法 ide

 

const traverseContextPool = [];
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}

咱們看到這個方法其實沒有什麼特別的處理,他首先判斷一下這個全局變量 traverseContextPool ,他是不是已經存在的一個節點,若是有的話,從這個 traverseContextPool 裏面 pop 一個,pop後,再把傳入進來的直接掛載到他上面,沒有作任何其餘的操做,其實就是用來記錄的對象,若是沒有,就 return 一個新的對象,這有什麼意義呢?回過頭去看一下,後續兩個方法調用了 traverseContext, 最後一句話代碼是 releaseTraverseContext(traverseContext) , releaseTraverseContext 他又是什麼意思呢?函數

 

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}

咱們發現,他就是一個把 traverseContext 這個對象的內容都給清空了,而後判斷 traverseContextPool 是否小於 POOL_SIZE 這個最大的限制大小,就是10,若是沒有大於,就往裏面 push 這個對象。那這有什麼意義呢?這就是一個很簡單的一個對象池的一個概念,就是我這個 map function 極可能是我常常要調用的方法,若是他展開層數比較多,那麼我這個 traverseContextPool 聲明的對象也會比較多,若是這個聲明的對象比較多,我每次調用 map functioin, 都要聲明這麼多對象,而後調用完了以後,又要把這麼多對象釋放,這實際上是一個很是消耗性能的一個操做,由於一個聲明對象,和一個刪除對象,那麼他極可能會形成內存抖動的問題,而後讓咱們總體看到的瀏覽器內頁面的性能會比較差,因此在這裏他設置了這麼一個 traverseContextPool 。而後總共的長度給他一個10,而後是個漸進的過程,一開始他是一個空數組,隨着對象一個個建立,他會把他推動去,而後又把他拿出來複用,由於咱們知道 js 是一個單線程的咱們在執行某一個 props.children 的時候,能夠複用空間。而後咱們繼續看 traverseAllChildren 性能

 

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }


  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

這個方法其實沒有什麼特別的東西,而後他 return 的實際上是一個 traverseAllChildrenImpl。this

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


  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    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,
      // If it's the only child, treat the name as if it was wrapped in an array
      // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }


  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  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') {
      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === children.entries) {
          warning(
            didWarnAboutMaps,
            'Using Maps as children is unsupported and will likely yield ' +
              'unexpected results. Convert it to a sequence/iterable of keyed ' +
              'ReactElements instead.',
          );
          didWarnAboutMaps = true;
        }
      }


      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,
        );
      }
    } else if (type === 'object') {
      let addendum = '';
      if (__DEV__) {
        addendum =
          ' If you meant to render a collection of children, use an array ' +
          'instead.' +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }


  return subtreeCount;
}

而後這個方法就是一個重點了,若是這裏的 children 是一個單個的節點,他不是一個數組,那麼這個時候他會進入這裏面的判斷,是undefined, string , number , object 等等。object 是一個 REACT_ELEMENT_TYPE 或者 REACT_PORTAL_TYPE 。他們都是合理的 react 能夠渲染的節點,而後直接調用 invokeCallback 賦值爲 true, invokeCallback 爲 true 是,直接調用 callback,這裏的 callback 是傳入的 mapSingleChildIntoContext 這樣的一個方法,他們都有一個共同的特色,就是他們不是數組或者可遍歷的對象,因此他們是單個節點,因此對於單個節點,就能夠直接調用 callback。若是是個數組怎麼辦,若是是個數組,他就會去循環遍歷這個數組,而後他再調用他自身,而後再把這個 child 傳進去,這就是一個遞歸的過程,直到單個的時候,再去調用這個 callback,那麼這個 callback 是什麼呢, mapSingleChildIntoContextspa

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,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}

這個方法裏面作了什麼呢,傳入了三個參數,第一個參數就是 bookKeeping, 也就是 traverseContext,也就是 pool裏面的東西。第二個參數 child 就是 traverseAllChildrenImpl 裏面的直到單個的 children, childKey 就是一個映射關係,這個能夠忽略。

 

而後裏面調用了 bookKeeping 裏面的 func ,就是 traverseContext 裏面的 func,就是那個回調的函數。而後把節點傳入,再傳入一個 index, 調用了以後返回給 mappedChild,而後判斷是不是一個數組,若是是一個數組,會再次調用 mapIntoWithKeyPrefixInternal ,這就是一個大的遞歸,這時候遞歸的時候就不調用 func ,而是本身的 c=>c,直接返回這個節點,否則就無限循環了,對於 map 過的 chilren , 直接返回當前節點就能夠了。最終,會判斷下 mappedChild 是不是合理的 element, 也就是 isValidElement ,判斷以後會判斷 cloneAndReplaceKey,

 

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );


  return newElement;
}

這裏 cloneAndReplaceKey 也很是簡單,他其實就是 return 了一個新的 react element。除了一個 newKey 以外,其餘都是在原來的 react element 的基礎上返回

 

這就是 map function 兩層嵌套遞歸的過程,把全部數組遞歸,而後把他展開返回的一個過程,因此這裏 getPooledTraverseContext 裏面的 pool ,有多少個數組,就有多少個對象,這就是 pool 設置在這裏的含義,他不只僅就是一層的狀況下,永遠都只有一個對象,若是數組裏面是有嵌套的,並且嵌套是比較多的狀況下,那麼他的意義就比較大了。這就是 React.Children.map的實現過程。 forEachChildren 跟 mapChildren 相似。下面還有幾個, toArray



function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}

toArray就比較簡單了,只是 mapChildren 裏面的一層。

 

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}

onlyChild 其實就是判斷是不是單個合理的 react element節點 。

相關文章
相關標籤/搜索