react原始碼淺析(三):ReactChildren

hy醬發表於2019-03-01

react原始碼淺析(三):ReactChildren

react相關庫原始碼淺析react ts3 專案

react.children官方API ,閱讀全文,可以瞭解react.children基本原理,掌握react.children各個API的用法,還能瞭解到官方API以外的補充用法。

預備知識: react元素Obj的$$typeof有兩種,可以用ReactElement.js中的isValidElement來區分:

$$typeof: REACT_PORTAL_TYPE ------------------------- isValidElement返回false
$$typeof: REACT_ELEMENT_TYPE ------------------------- isValidElement返回true
複製程式碼

而Obj.type.$$typeof有五種,可以用packages\shared\isValidElementType.js來檢測是否是合法的ElementType,都返回true。

$$typeof: REACT_FORWARD_REF_TYPE
$$typeof: REACT_MEMO_TYPE
$$typeof: REACT_CONTEXT_TYPE
$$typeof: REACT_PROVIDER_TYPE
$$typeof: REACT_LAZY_TYPE
複製程式碼

內部工具函式

traverseContextPool資料結構

//資料結構:context池,大小為10。當做一個棧使用
const POOL_SIZE = 10;
const traverseContextPool = [];
//獲取一個context
//給棧頂的context設定相應屬性值,並彈出返回。
//如果棧中沒有元素,直接返回一個物件,相應的設定了屬性值
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,
    };
  }
}

//如果棧未滿,push一個空context物件
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);
  }
}
複製程式碼

escape

將傳入的key中所有的'='替換成'=0',':'替換成 '=2',並在key之前加上'$'

function escape(key) {
  const escapeRegex = /[=:]/g;
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  const escapedString = ('' + key).replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}
複製程式碼

getComponentKey

如果component存在不為null的key,則返回escape(component.key),否則返回index.toString(36)

function getComponentKey(component, index) {
  // Do some typechecking here since we call this blindly. We want to ensure
  // that we don't blog potential future ES APIs.
  if (
    typeof component === 'object' &&
    component !== null &&
    component.key != null
  ) {
    // Explicit key
    return escape(component.key);
  }
  // Implicit key determined by the index in the set
  // 轉換成36進位制
  return index.toString(36);
}
複製程式碼

traverseAllChildrenImpl

Children不能是一個物件 程式碼有點長,簡述其作用:輸入children樹,返回樹中節點型別是string,number,或者節點的即$$typeof為REACT_ELEMENT_TYPE,REACT_PORTAL_TYPE的節點數量。因此React.Fragment的$$typeof也為REACT_ELEMENT_TYPE,所以React.Fragment為一個節點。如果children是Array或者其他型別的子節點,則遞迴呼叫traverseAllChildrenImpl,直到children的typeof是string,number,或者$$typeof為REACT_ELEMENT_TYPE,REACT_PORTAL_TYPE時,對該children執行callback函式,並返回1。注意:不是對所有的節點遍歷。

callback傳入的引數為traverseContext,children,nameSoFar 其中nameSoFar === '' ? '.' + getComponentKey(children, 0) : nameSoFar。

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,
);
複製程式碼

可以參看blog/D3檔案下的reactchildren.vsdx檔案中的流程圖以及react資料夾下對應的原始碼註釋。

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
){...}
複製程式碼

traverseAllChildren

traverseAllChildrenImpl呼叫封裝,與其功能一樣。

forEachSingleChild

執行bookKeeping.func,並將bookKeeping.count的值加1。func傳入的引數為bookKeeping.context,child以及bookKeeping.count。

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  //執行bookKeeping.func,bookKeeping.count計數增加一
  func.call(context, child, bookKeeping.count++);
}
複製程式碼

forEachChildren

通過呼叫getPooledTraverseContext將傳入的引數forEachFunc以及forEachContext賦值給traverseContext的func與context屬性。 呼叫traverseAllChildren

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

escapeUserProvidedKey

匹配一個或者多個 "/",並用'$&/'替換

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

mapIntoWithKeyPrefixInternal

呼叫escapeUserProvidedKey對傳入的prefix進行處理得到escapedPrefix,載 通過呼叫getPooledTraverseContext將傳入的引數array、escapedPrefix、func以及context賦值給traverseContext的result、keyPrefix、func與context屬性。 呼叫traverseAllChildren。最後清除traverseContext上的屬性,併入棧。

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);
}
複製程式碼

mapSingleChildIntoContext

對children執行func(func為傳入的React.Children.map中的func), 如果返回了一個陣列,則對這個陣列呼叫mapIntoWithKeyPrefixInternal目的是新增特定的key 克隆以child節點為根節點的樹中的所有child,替換掉每個新child元素的key,push到bookKeeping中的result

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

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
      //如果child中包含多個child,則返回的mappedChild是一個陣列,則遞迴呼叫mapIntoWithKeyPrefixInternal
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
      //如果child中只有一個child,並且是合法的react元素,
      // 則將mappedChild的key屬性值替換掉
    //  最後將新的react元素push到bookKeeping.result
    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);
  }
}
複製程式碼

對外介面

mapChildren

React.Children.map(children, func, context)
複製程式碼

引數描述:

children:
	可以是一個物件,但是必須具備屬性$$typeof為Symbol.for('react.portal')或者Symbol.for('react.element'),可以稱其為類reactChild物件,否則報錯。
	children為null或者undefined就返回null或者undefined,children中的Fragment為一個子元件。
func:
	對符合規定的children執行的函式,func會被傳入兩個引數,符合規定的children以及到當前children的數量。所有執行func返回的children都會新增到一個陣列中,沒有巢狀。
context:
	一般都為null,如果傳入context則func執行中的this為context,看例2
複製程式碼

返回值: 返回一個平面陣列,看例1

例子相關程式碼,見runLogic資料夾的index.js:

//
<App>
    {/*測試*/}
    <Header/>
    <Content/>
    string 1
    <React.Fragment>
        Some text.
        <h2>A heading</h2>
    </React.Fragment>
    <Footer>覆蓋</Footer>
    string 2
</App>

//
class App extends React.Component{
    render(){
		例1程式碼
		例2程式碼
        return (
            <div>
				...
            </div>
        )
    }
}																
複製程式碼

例1:測試children是一個巢狀結構,返回的陣列是否是一個平面陣列:

let reactChildLike = {$$typeof:Symbol.for('react.element')}
let complexChildren = [reactChildLike,[reactChildLike,this.props.children]]
console.log(React.Children.map(complexChildren,(children)=>[children,children,children]))
複製程式碼

結果:

this.props.children為:
(6) [{…}, {…}, "string 1", {…}, {…}, "string 2"]

complexChildren中符合規定的child為1+1+6=8,所以輸出的result為3*8=24個元素的平面陣列

(24) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, "string 1", "string 1", "string 1", {…}, {…}, {…}, {…}, {…}, {…}, "string 2", "string 2", "string 2"]
複製程式碼

例2:測試context的作用

	let reactChildLike = {$$typeof:Symbol.for('react.element')}
    let func = function (child) {
        console.log(this)
        this.a=1000;
        return child
    }
    let contextTest = {a:1}
    console.log("React.Children.map test",React.Children.map(reactChildLike,func,contextTest))
    console.log("contextTest.a",contextTest.a)
複製程式碼

結果:

	React.Children.map test 
		[{…}]
		0: {$$typeof: Symbol(react.element), type: undefined, key: ".0", ref: undefined, props: undefined, …}length: 1__proto__: Array(0)
	contextTest.a 1000
複製程式碼

func給this.a賦值為1000,在傳入context的時候,外部的context.a變成了1000。

原始碼:

function mapChildren(children, func, context) {
//children為null或者undefined就返回null或者undefined
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
複製程式碼

執行邏輯:


1. 初始呼叫

mapChildren函式中:mapChildren傳入(children,func,context),呼叫mapIntoWithKeyPrefixInternal(children, [], null, func, context)


2. 構建traverseContext

mapIntoWithKeyPrefixInternal函式中:prefix為傳入的第三個引數null,此時traverseContextPool=[],直接返回

traverseContext={
  result: [],
  keyPrefix: null,
  func: func,
  context: context,
  count: 0,
}
複製程式碼

呼叫traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)


3. children構建key

traverseAllChildren函式中:呼叫traverseAllChildrenImpl(children, '', mapSingleChildIntoContext, traverseContext)

traverseAllChildrenImpl函式中: 如果children是符合規定的則呼叫mapSingleChildIntoContext(traverseContext,children,'.’+children的key)。


4. 對children執行func,並新增到結果陣列

mapSingleChildIntoContext函式中:traverseContext.func為mapChildren函式接收到的func,呼叫traverseContext.func.call(context, children, traverseContext.count++)。如果傳入mapChildren的func對該children進行了改造。並返回了一陣列的新children,則呼叫mapIntoWithKeyPrefixInternal為符合規定的成員新增特定的Key之後,新增到result陣列中。


toArray

利用mapChildren也能實現toArray的功能,只需要func為child => child即可

function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}
複製程式碼

onlyChild

判斷children是否是單個React element child

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

countChildren

計算children個數

function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}
複製程式碼

相關文章