【React源码笔记2】- ReactChildren

React | 2020-06-30 21:19:40 159次 0次

ReactChildren 是一个比较独立的模块,所以理解起来就相对来说轻松很多,参照它的测试用例来看会更加清晰。

一、forEach使用

import React from 'react'

function ChildrenTest(props) {
  React.Children.forEach(props.children, (vnode, index) => {
    console.log(vnode)
  })
  return props.children
}

export default () => (
  <ChildrenTest>
    <span key = "span1">
      <em key = "em">1</em>
    </span>
    <span>2</span>
    <span>3</span>
  </ChildrenTest>
)


二、forEach源码

所有的逻辑都在 ReactChildren.js 中,可以根据 __tests__ 中的测试用例了解更多的用法。

//导出别名
export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

从 forEachChildren 入手,其中为了更清晰的理解,可直接忽略第三个参数:

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

这里用到了对象池的概念,减少垃圾回收,节省 CPU 开销。在进入 getPooled... 之前先看下 releaseTraverseContext 可能会更好的理解这一点,将当前对象中数据重置为 null 然后根据设置的对象池的大小往池中存入对象,这样避免的对象的重复创建和销毁操作,一直保持着固定数量的引用:

//对象池中回收数据
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);
  }
}

getPooledTraverseContext 就是从对象池中取数据了,首先判断如果池子中数据为空则先创建对象,反之取出一个对象并挂载上数据,最大数量为10,超过这个数量后则无法复用数据,只能重新创建一个对象:

//对象池的数据总量 减少GC
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,
    };
  }
}

traverseAllChildren 被上述各个方法调用,里面最终执行的是 traverseAllChildrenImpl

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }
  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
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;
        }
    }
  }
  //只有一个子节点直接触发回调 forEachSingleChild
  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  ...
  return subtreeCount;
}

这个函数有点长,分布拆开来看,invokeCallback 这个判断只有一个节点时会触发,调用 forEachSingleChild,它做的事情就是触发用户传入的回调,第一个参数为子节点,第二个参数为索引。

继续上面的方法,如果有多个子节点则是一个数组类型,直接递归调用就行:

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  ...
  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];
      //key拼接
      nextName = nextNamePrefix + getComponentKey(child, i);
      //递归调用
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } 
  ...
  return subtreeCount;
}

getComponentKey 中调用  escape 将组件的 key 传入,为了安全做出字符转换:

function escape(key) {
  const escapeRegex = /[=:]/g;
  //规则字符转换
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  //转为字符串后 如果规则匹配则替换
  const escapedString = ('' + key).replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}

到这里基本上 forEach 循环就完事了,但是 react 中还支持一个创建节点的操作:

    const threeDivIterable = {
      '@@iterator': function() {
        let i = 0;
        return {
          next: function() {
            if (i++ < 3) {
              return {value: <div key={'#' + i} />, done: false};
            } else {
              return {value: undefined, done: true};
            }
          },
        };
      },
    };
    <div>{ threeDivIterable  }</div>

所以还要继续判断是否是 iterator 方式:

...
// 就是直接取出 @@iterator属性对应的值 Function 
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
  //常量 false
  if (disableMapsAsChildren) {
    ...
  }
  ...
  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,
    );
  }
}
...

最后判断下如果是对象则直接提示错误,不支持。


三、Map 用法

import React from 'react'

function ChildrenTest(props) {
  console.log(React.Children.map(props.children, c => c))
  return props.children
}
//支持扁平化数组
export default () => (
  <ChildrenTest>
    {
      [[1, 2, 3], [4, 5], 6]
    }
  </ChildrenTest>
)


四、Map 源码

有了上面 forEach 的源码解析,再来理解 map 会相对轻松很多,还是忽略掉 context 参数。

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

mapIntoWithKeyPrefixInternal 和 forEachChildren 做的事情类似:

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) {
    // '/test' ==> '//test/' 安全转换
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  //取对象
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  //销毁回归进入对象池
  releaseTraverseContext(traverseContext);
}

区别就是传入的回调函数发生了巨大的改变 mapSingleChildIntoContext,仍然是在 invokeCallbacktrue 是被执行

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  //这里如果用户返回一个数组的话 c => [c, [c]] 则会递归执行
  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    //正常情况下会进入这里 判断是否为 reactElement 对象(节点)
    if (isValidElement(mappedChild)) {
      // 克隆一个对象 通过 ReactElement 创建
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    //存入结果集
    result.push(mappedChild);
  }
}

注意 map 方法中最终返回了 resultforEach 中没有返回值,这是他们两个唯一的区别。


五、toArray源码

function toArray(children) {
  const result = [];
  // 固定了 最后一个函数参数
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}

其余的和 map 方法一致,下一篇继续进入 reactDom.render 方法。

0人赞

分享到: