packages/react/ReactChildren解析

ReactChildren

React.Children 提供了用于處理 this.props.children 不透明數(shù)據(jù)結(jié)構(gòu)的實(shí)用方法逝段。

React.Children.map

React.Children.map(children, function[(thisArg)])

children 里的每個(gè)直接子節(jié)點(diǎn)上調(diào)用一個(gè)函數(shù),并將 this 設(shè)置為 thisArg豁生。如果 children 是一個(gè)數(shù)組酸纲,它將被遍歷并為數(shù)組中的每個(gè)子節(jié)點(diǎn)調(diào)用該函數(shù)捣鲸。如果子節(jié)點(diǎn)為 null 或是 undefined,則此方法將返回 null 或是 undefined闽坡,而不會(huì)返回?cái)?shù)組栽惶。

注意

如果 children 是一個(gè) Fragment 對(duì)象,它將被視為單一子節(jié)點(diǎn)的情況處理疾嗅,而不會(huì)被遍歷外厂。

React.Children.forEach

React.Children.forEach(children, function[(thisArg)])

React.Children.map() 類似,但它不會(huì)返回一個(gè)數(shù)組代承。

React.Children.count

React.Children.count(children)

返回 children 中的組件總數(shù)量汁蝶,等同于通過(guò) mapforEach 調(diào)用回調(diào)函數(shù)的次數(shù)。

React.Children.only

React.Children.only(children)

驗(yàn)證 children 是否只有一個(gè)子節(jié)點(diǎn)(一個(gè) React 元素)论悴,如果有則返回它掖棉,否則此方法會(huì)拋出錯(cuò)誤。

注意:

React.Children.only() 不接受 React.Children.map() 的返回值膀估,因?yàn)樗且粋€(gè)數(shù)組而并不是 React 元素啊片。

React.Children.toArray

React.Children.toArray(children)

children 這個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)以數(shù)組的方式扁平展開(kāi)并返回,并為每個(gè)子節(jié)點(diǎn)分配一個(gè) key玖像。當(dāng)你想要在渲染函數(shù)中操作子節(jié)點(diǎn)的集合時(shí)紫谷,它會(huì)非常實(shí)用,特別是當(dāng)你想要在向下傳遞 this.props.children 之前對(duì)內(nèi)容重新排序或獲取子集時(shí)捐寥。

注意:

React.Children.toArray() 在拉平展開(kāi)子節(jié)點(diǎn)列表時(shí)笤昨,更改 key 值以保留嵌套數(shù)組的語(yǔ)義。也就是說(shuō)握恳,toArray 會(huì)為返回?cái)?shù)組中的每個(gè) key 添加前綴瞒窒,以使得每個(gè)元素 key 的范圍都限定在此函數(shù)入?yún)?shù)組的對(duì)象內(nèi)。


源碼解析

some utils:

const SEPARATOR = '.';
const SUBSEPARATOR = ':';

/**
 * Escape and wrap key so it is safe to use as a reactid
 *
 * @param {string} key to be escaped.
 * @return {string} the escaped key.
 */
function escape(key) {
  const escapeRegex = /[=:]/g;
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  const escapedString = ('' + key).replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}

/**
 * TODO: Test that a single child and an array with one item have the same key
 * pattern.
 */

let didWarnAboutMaps = false;

const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}

const POOL_SIZE = 10;
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,
    };
  }
}

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);
  }
}
  • SEPARATOR:react key 的分隔符
  • SUBSEPARATOR: react sub-children key 的分隔符
  • escapeescapeUserProvidedKey: 轉(zhuǎn)譯key
  • getPooledTraverseContext: 如果traverseContextPool中有元素乡洼,就從traverseContextPool中拿出一個(gè)元素崇裁,這樣做的好處是可以減少內(nèi)存的占用匕坯。
    否則就創(chuàng)建一個(gè)新的元素,來(lái)存儲(chǔ)context的信息:result,keyPrefix,func,context,count;
  • releaseTraverseContext:釋放并初始化某個(gè)context拔稳,如果traverseContextPool的大小<10的話葛峻,這個(gè)context會(huì)被push進(jìn)去。

關(guān)鍵API和函數(shù):

traverseAllChildrenImpl()

/**
 * @param {?*} children Children tree container.
 * @param {!string} nameSoFar Name of the key path so far.
 * @param {!function} callback Callback to invoke with each child found.
 * @param {?*} traverseContext Used to pass information throughout the traversal
 * process.
 * @return {!number} The number of children in this subtree.
 */
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;
}

traverseAllChildrenImpl函數(shù)中巴比,首先對(duì)children的類型做了判斷术奖。

  • 如果不是數(shù)組、map轻绞、普通對(duì)象類型采记,就直接執(zhí)行傳入的callback函數(shù),并返回1政勃。其中callback函數(shù)執(zhí)行的時(shí)候傳入的key是通過(guò)getComponentKey并轉(zhuǎn)譯得到的唧龄。
  • 如果children是數(shù)組對(duì)象,就遞歸調(diào)用自己奸远,且在遞歸調(diào)用中选侨,分隔符變成了SUBSEPARATOR;
  • 如果是Map對(duì)象,React會(huì)拋出警告日志然走,因?yàn)镸ap對(duì)象的迭代器是不保證順序的。
  • 如果是普通的Object對(duì)象戏挡,就會(huì)拋出錯(cuò)誤芍瑞,并不會(huì)執(zhí)行下去。

forEach

/**
 * Traverses children that are typically specified as `props.children`, but
 * might also be specified through attributes:
 *
 * - `traverseAllChildren(this.props.children, ...)`
 * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
 *
 * The `traverseContext` is an optional argument that is passed through the
 * entire traversal. It can be used to store accumulations or anything else that
 * the callback might find relevant.
 *
 * @param {?*} children Children tree object.
 * @param {!function} callback To invoke upon traversing each child.
 * @param {?*} traverseContext Context for traversal.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

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

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

/**
 * Iterates through children that are typically specified as `props.children`.
 *
 * See https://reactjs.org/docs/react-api.html#reactchildrenforeach
 *
 * The provided forEachFunc(child, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} forEachFunc
 * @param {*} forEachContext Context for forEachContext.
 */
function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

forEachChildren即是forEachAPI褐墅,在這個(gè)函數(shù)中拆檬,會(huì)拿到一個(gè)初始化后的context,并在這個(gè)context中綁定開(kāi)發(fā)者自定義的forEachFuncforEachContext妥凳,之后會(huì)執(zhí)行traverseAllChildren竟贯,traverseAllChildren執(zhí)行完畢后,會(huì)釋放context逝钥。

map

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);
  }
}

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);
}

/**
 * Maps children that are typically specified as `props.children`.
 *
 * See https://reactjs.org/docs/react-api.html#reactchildrenmap
 *
 * The provided mapFunction(child, key, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} func The map function.
 * @param {*} context Context for mapFunction.
 * @return {object} Object containing the ordered map of results.
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

mapChildren即是mapAPI屑那,它會(huì)調(diào)用mapIntoWithKeyPrefixInternal,并把返回的結(jié)果存入result并返回艘款。與forEach不同的是持际,mapChildren會(huì)為每一個(gè)component創(chuàng)建一個(gè)新的key。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哗咆,一起剝皮案震驚了整個(gè)濱河市蜘欲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晌柬,老刑警劉巖姥份,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件郭脂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡澈歉,警方通過(guò)查閱死者的電腦和手機(jī)展鸡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闷祥,“玉大人娱颊,你說(shuō)我怎么就攤上這事】常” “怎么了箱硕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)悟衩。 經(jīng)常有香客問(wèn)我剧罩,道長(zhǎng),這世上最難降的妖魔是什么座泳? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任惠昔,我火速辦了婚禮,結(jié)果婚禮上挑势,老公的妹妹穿的比我還像新娘镇防。我一直安慰自己,他們只是感情好潮饱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布来氧。 她就那樣靜靜地躺著,像睡著了一般香拉。 火紅的嫁衣襯著肌膚如雪啦扬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天凫碌,我揣著相機(jī)與錄音扑毡,去河邊找鬼。 笑死盛险,一個(gè)胖子當(dāng)著我的面吹牛瞄摊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苦掘,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼泉褐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鸟蜡?” 一聲冷哼從身側(cè)響起膜赃,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎揉忘,沒(méi)想到半個(gè)月后跳座,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體端铛,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年疲眷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了禾蚕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狂丝,死狀恐怖换淆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情几颜,我是刑警寧澤倍试,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蛋哭,受9級(jí)特大地震影響县习,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谆趾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一躁愿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沪蓬,春花似錦彤钟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至性芬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剧防,已是汗流浹背植锉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留峭拘,地道東北人俊庇。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸡挠,于是被迫代替她去往敵國(guó)和親辉饱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 源碼基于React@16.8.4 版本 React.Children提供了處理this.props.childre...
    konnga閱讀 969評(píng)論 0 0
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,272評(píng)論 0 2
  • 深入JSX date:20170412筆記原文其實(shí)JSX是React.createElement(componen...
    gaoer1938閱讀 8,070評(píng)論 2 35
  • 以下內(nèi)容是我在學(xué)習(xí)和研究React時(shí)拣展,對(duì)React的特性彭沼、重點(diǎn)和注意事項(xiàng)的提取、精練和總結(jié)备埃,可以做為React特性...
    科研者閱讀 8,234評(píng)論 2 21
  • 今天是情人節(jié)姓惑,一個(gè)來(lái)自西方的節(jié)日褐奴,如今在國(guó)內(nèi)被炒得越來(lái)越熱鬧,尤其在年輕的少男少女中間更甚于毙《囟看著這天微信,微博上被...
    任云閱讀 249評(píng)論 0 2