React源碼解析(二):關於React.Children,你瞭解多少?

前言

上一篇文章說了React的一部分基礎API,今天這一篇文章說一下React.children前端

React.children

在React.js中,摘取出Children,其中羅列了React.children的幾個方法react

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },
}
複製代碼
  • map
  • forEach
  • count
  • toArray
  • only

幾種方法的使用

有這樣一段代碼es6

import React from 'react';

function ChildrenDemo(props) {
    console.log(props.children, 'props.children');
    console.log(React.Children.map(props.children, item => item), 'map');
    console.log(React.Children.map(props.children, item => [item, [item, item]]), 'map');
    console.log(React.Children.forEach(props.children, item => item), 'forEach');
    console.log(React.Children.forEach(props.children, item => [item, [item, item]]), 'forEach');
    console.log(React.Children.toArray(props.children), 'toArray');
    console.log(React.Children.count(props.children), 'count');
    console.log(React.Children.only(props.children[0]), 'only');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

咱們看一下,控制檯的輸出結果:web

咱們看到ajax

  • map方法:根據傳參的函數不一樣,輸出的結果也不一樣,它的結果是一個數組,裏面包含了各個節點的信息
  • forEach方法:不管傳遞什麼參數,返回的結果都是undefined
  • toArray方法:返回了一個數組,裏面包含着各個節點的信息
  • count方法:返回了一個數字,這個數字是節點的個數
  • only方法:咱們傳了一個節點給only方法,同時返回了節點信息

看到這裏,你們內心確定有一個疑問,爲何會返回這些結果呢?下面,咱們經過分析源碼來得到咱們想要的答案json

源碼分析

咱們經過斷點的方式一步一步地分析源碼流程,並在最後繪製出流程圖,加深理解數組

PS:引用react的時候引入react打包以後的文件(react.development.js)bash

import React from './react.development.js';
複製代碼

map方法(item => item)

import React from './react.development.js';

function ChildrenDemo(props) {
    console.log(React.Children.map(props.children, item => item), 'map');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

在react.development.js中,找到關於map方法的全部函數,在須要的地方打上斷點,咱們看它是如何執行的。閉包

經過斷點,map方法先執行mapChildren函數app

function mapChildren(children, func, context) {
    //判斷傳入的children是否爲null
    if (children == null) {
        // 爲null,直接返回children
        return children;
    }
    // 不爲null,定義一個result
    var result = [];
    // 調用函數,並傳入相應的五個參數
    mapIntoWithKeyPrefixInternal(children, result, null, func, context);
    // 返回result
    return result;
}
複製代碼
  • 調用的mapIntoWithKeyPrefixInternal方法
// 方法接收五個參數
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context){
    // 定義escapedPrefix,方便後面傳參
    var escapedPrefix = '';
    // 判斷傳入的參數prefix不爲null
    if (prefix != null) {
        // 若是prefix不爲空,則調用escapeUserProvidedKey方法,傳入prefix,在得到的結果後加上'/'
        escapedPrefix = escapeUserProvidedKey(prefix) + '/';
    } 
    // 調用getPooledTraverseContext方法傳入四個參數,將得到的結果賦值爲traverseContext,方便爲下面函數傳參
    var traverseContext = getPooledTraverseContext(array, escapedPrefix, func, context);
    // 調用traverseAllChildren方法,傳入三個參數,其中mapSingleChildIntoContext是一個函數,這個函數的做用就是將嵌套的數組展開
    traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
    // 調用releaseTraverseContext方法,傳入參數
    releaseTraverseContext(traverseContext);
}
複製代碼
  • 調用的escapeUserProvidedKey方法
const userProvidedKeyEscapeRegex = /\/+/g;
// 匹配連續的'\'並替換爲'$&/'
function escapeUserProvidedKey(text) {
    return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}
複製代碼
  • 接下來,分析調用的getPooledTraverseContext方法

該方法的主要做用是:建立一個對象池,複用Object,從而減小不少對象建立帶來的內存佔用和gc(垃圾回收)的損耗

// 這裏定義了一個size爲10的緩衝池
const POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(mapResult, keyPrefix, mapFunction, mapContext) {
  if (traverseContextPool.length) { // 若是緩衝池中有值,則取出一個值使用
    var 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
    };
  }
}
複製代碼

上面的兩種狀況的返回值就是調用函數時接收的變量值。

  • 在取到traverseContext值後,開始調用traverseAllChildren方法
// 函數傳入了三個參數,第一個參數時children,第二個參數是一個mapSingleChildIntoContext函數,第三個參數時咱們通過上面方法返回的值
function traverseAllChildren(children, callback, traverseContext) {
    // 假設子節點爲空,直接返回0
    if (children == null) {
        return 0;
    }
    // 不然調用traverseAllChildrenImpl函數
    return traverseAllChildrenImpl(children, '', callback, traverseContext);
}


// mapSingleChildIntoContext函數 
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    // 判斷mappedChild是否是一個數組,若是是,再次調用mapIntoWithKeyPrefixInternal函數
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {// 若是不是數組,而且mappedChild部位null
    // 判斷mappedChild在isValidElement函數中的返回值是否是true,是才能夠調用cloneAndReplaceKey方法,傳入了mappedChild節點的key值
    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中加入處理好的mappedChild節點,result是咱們在控制檯打印出來的值
    result.push(mappedChild);
  }
}
複製代碼
  • 調用的traverseAllChildrenImpl方法(核心方法)
// 函數接收了四個參數
function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
    // 首先會判斷children的類型
    var type = typeof children;
    // 若是類型爲undefined或者是布爾類型,讓children爲null
    if (type === 'undefined' || type === 'boolean') {
        // All of the above are perceived as null.
        children = null;
    }
    // 定義一個布爾類型變量,用來判斷是否要調用傳進來的callback
    var invokeCallback = false;
    
    if (children === null) { // 假如子節點爲null,讓標識變量變爲true-
        invokeCallback = true;
    } else { // 假如與上相反,會判斷type的具體類型
        switch (type) {
            case 'string':
            case 'number': // 是數字,將標識變爲true
                invokeCallback = true;
                break;
            case 'object': // 是object,繼續判斷子節點中的$$typeof
                switch (children.$$typeof) {
                    // 是'REACT_ELEMENT_TYPE'類型,不作任何處理
                    case REACT_ELEMENT_TYPE:
                    case REACT_PORTAL_TYPE: // 是REACT_PORTAL_TYPE將標誌改成true
                        invokeCallback = true;
                }
        }
    }
    // 假如invokeCallback爲true,會調用傳進來的callback,也就是mapSingleChildIntoContext函數,這裏會返回新的參數
    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.
            // SEPARATOR是key最開始有的'.',這裏是當傳入的nameSoFar爲空時,要調用getComponentKey方法,
            nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
        return 1;
    }

    var child = void 0;
    var nextName = void 0;
    var subtreeCount = 0; // Count of children found in the current subtree.
    // 定義一個nextNamePrefix,這裏是第二層子節點的key值處理,SUBSEPARATOR初值爲':'
    var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
    // 判斷children是否是一個數組
    if (Array.isArray(children)) { // 是數組,會循環子節點
        for (var i = 0; i < children.length; i++) {
            child = children[i];
            // 這裏調用了getComponentKey方法,處理節點的key值
            nextName = nextNamePrefix + getComponentKey(child, i);
            // 統計子節點個數,會繼續調用traverseAllChildrenImpl,此次給函數傳遞的是子節點,也就是json類型的數據,運用了遞歸的思想
            subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
        }
    } else { // 不是數組
        var iteratorFn = getIteratorFn(children);
        if (typeof iteratorFn === 'function') {
            {
                // Warn about using Maps as children
                if (iteratorFn === children.entries) { // 報警告
                    !didWarnAboutMaps ? warning$1(false, 'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.') : void 0;
                    didWarnAboutMaps = true;
                }
            }

            var iterator = iteratorFn.call(children);
            var step = void 0;
            var 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') {
            var addendum = '';
            {
                addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + ReactDebugCurrentFrame.getStackAddendum();
            }
            var childrenString = '' + children;
            (function () {
                {
                    {
                        throw ReactError(Error('Objects are not valid as a React child (found: ' + (childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString) + ').' + addendum));
                    }
                }
            })();
        }
    }
    // 返回了子節點數量
    return subtreeCount;
}
複製代碼
  • 在最後調用了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);
    }
}
複製代碼

而後返回的result就是咱們在控制檯輸出的結果。

map方法(item => [item, [item, item])

import React from '../react.development.js';

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

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

這時候,傳入的展開數組函數方法變得不同了,咱們仍是按照上述的方法再走一遍流程,看看二者之間有什麼不一樣和相同。

咱們經過debugger發現,函數的執行流程和上述並無什麼不用,只是某幾個函數執行的次數發生了變化。

  • traverseAllChildrenImpl函數
  • mapSingleChildIntoContext函數

forEach方法

咱們知道在es6語法中map和forEach方法都是遍歷一個數組,在這裏邊也是一樣的,只是map方法有返回,而forEach方法沒有,因此這篇文章再也不對forEach進行講解。

toArray

import React from '../react.development.js';

function ChildrenDemo(props) {
    console.log(React.Children.toArray(props.children), 'toArray');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

經過的debugger,咱們發現它接下來要走的流程和map函數是同樣的。

count

import React from '../react.development.js';

function ChildrenDemo(props) {
    console.log(React.Children.count(props.children), 'count');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

count的入口函數

function countChildren(children) {
    return traverseAllChildren(children, function () {
        return null;
    }, null);
}
複製代碼

它調用了展平數組的函數,咱們在上面寫源碼過程的時候,在traverseAllChildrenImpl函數中計算了節點的個數。

only

import React from '../react.development.js';

function ChildrenDemo(props) {
    console.log(React.Children.only(props.children[0]), 'only');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>
)
複製代碼

在上面,咱們在控制檯打印的結果是一個json數據,包括了咱們傳進去節點的信息。下面,咱們來看一下,它的源碼部分

function onlyChild(children) {
    // 一個閉包函數,假設傳入的不是一個節點,那麼會拋出異常
    (function () {
        if (!isValidElement(children)) {
            {
                throw ReactError(Error('React.Children.only expected to receive a single React element child.'));
            }
        }
    })();
    // 若是沒問題,就會返回傳過來的節點信息
    return children;
}
複製代碼

咱們把React.children的五個方法都一一分析完畢,咱們發現除了onlyChild的方法都執行了共同的方法。因此,爲了能更加清晰和更好地的理解React.children方法的流程,咱們畫一張流程圖感覺一下(這裏只畫map方法的)

在es五、es6中數組展平的應用

  • es5
function mapChildren(array) {
    var result = [];
    for(var i = 0;i < array.length; i++) {
        if (Array.isArray(array[i])) {
            // 遞歸思想
            result = result.concat(mapChildren(array[i]))
        } else {
            result.push(array[i])
        }
    } 
    return result;
}

const result = mapChildren([1,[1,2,[3,4,5]]])
console.log(result);   // [1,1,2,3,4,5]
複製代碼
  • es6
function mapChildren(array) {
    while(array.some(item => Array.isArray(item)))
    array = [].concat(...array);
    return array
}

const result = mapChildren([1,[1,2,[3,4,5]]])
console.log(result);   // [1,1,2,3,4,5]
複製代碼

總結

React.children的源碼至此所有分析完畢,咱們要學習到框架的思想,拓展咱們的思惟,將這些思想運用到實戰中,而且改善編碼習慣,寫出高質量的代碼~

上述文章若有不對之處,還請你們指點出來,咱們共同窗習,共同進步~

最後,分享一下個人公衆號【web前端日記】,關注後有資料能夠領取(通常人我不告訴哦)~

往期推薦

相關文章
相關標籤/搜索