React源码 React.Children
Children: {
map,
forEach,
count,
toArray,
only,
}
这个 children 是个对象,这个对象里面有 5 个属性,这个 5 个属性看上去就跟我们数组的操作非常的像,前两个是最重要的,也就是 map 和 forEach ,就相当于一个数组的 map 和 forEach 方法,我们可以理解为意义是一样的,但是实际的操作跟数组的 map 和 forEach 会有一点的区别。 这里 map 是所有逻辑里面最复杂的一个。而 map 和 forEach 是差不多的,他们唯一的区别是一个有返回,一个没有返回,map 是通过我们传入一个方法之后,调用出来之后,返回的一个新的数组,而 forEach 是返回原数组的。
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 把他给展开了,然后就变成了一个一维数组,本来是返回一个二维数组,然后这个数组里面有两个一维数组。
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 ,没有返回值。这就是他们一个本质的区别。
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 ,这个东西有什么意义呢?我们直接看下这个方法
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。
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 是什么呢, mapSingleChildIntoContext
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 就是一个映射关系,这个可以忽略。
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 的基础上返回
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节点 。
React源码 React.Children的更多相关文章
- React源码 React.Component
React中最重要的就是组件,写的更多的组件都是继承至 React.Component .大部分同学可能都会认为 Component 这个base class 给我们提供了各种各样的功能.他帮助我们去 ...
- React源码 React ref
ref 的功能,在 react 当中.我们写了一个组件,在这个时候,我们的 render function 里面我们会渲染一系列的子组件或者 dom 节点,有时候我们会希望有这样的需求,就是我们要获取 ...
- 读react源码准备
git源码地址:https://github.com/facebook/react react 里面就是 react源码 react里面的react文件夹就是react源码,react源码非常的少,总 ...
- React源码解析之React.Children.map()(五)
一,React.Children是什么? 是为了处理this.props.children(this.props.children表示所有组件的子节点)这个属性提供的工具,是顶层的api之一 二,Re ...
- React源码解析:ReactElement
ReactElement算是React源码中比较简单的部分了,直接看源码: var ReactElement = function(type, key, ref, self, source, owne ...
- React躬行记(16)——React源码分析
React可大致分为三部分:Core.Reconciler和Renderer,在阅读源码之前,首先需要搭建测试环境,为了方便起见,本文直接采用了网友搭建好的环境,React版本是16.8.6,与最新版 ...
- React源码之组件的实现与首次渲染
react: v15.0.0 本文讲 组件如何编译 以及 ReactDOM.render 的渲染过程. babel 的编译 babel 将 React JSX 编译成 JavaScript. 在 ba ...
- React源码剖析系列 - 生命周期的管理艺术
目前,前端领域中 React 势头正盛,很少能够深入剖析内部实现机制和原理.本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然. 对于 React,其组件生命周期(C ...
- react 源码之setState
今天看了react源码,仅以记录. 1:monorepo (react 的代码管理方式) 与multirepo 相对. monorepo是单代码仓库, 是把所有相关项目都集中在一个代码仓库中,每个mo ...
随机推荐
- cordova开发环境搭建
最近我在尝试了解跨平台技术的发展,首先则是想到了cordova.本文简单记录下cordova环境搭建的过程. 安装cordova 首先是要npm全局安装cordova npm install -g c ...
- vue 2.0 点击添加class,同时删除同级class
<template> <div class="n-header"> <ul class="title-wrapper"> & ...
- Django表单集合----Formset
概述:Formset(表单集)是多个表单的集合.Formset在Web开发中应用很普遍,它可以让用户在同一个页面上提交多张表单,一键添加多个数据,比如一个页面上添加多个用户信息,下面将会详细讲述如何使 ...
- spring boot项目启动报错
在eclipse中运行没有任何问题,项目挪到idea之后就报错 Unable to start EmbeddedWebApplicationContext due to miss EmbeddedSe ...
- 在虚拟机上的关于NFS网络文件系统
小知识: NFS(Network Files System)即网络文件系统,NFS文件系统协议允许网络中的主机通过TCP/IP协议进行资源共享,NFS客户端可以像使用本地资源一样读写远端NFS服务端的 ...
- Jenkins下载地址
http://ftp.yz.yamagata-u.ac.jp/pub/misc/jenkins/war/ Jenkins插件下载: http://updates.jenkins-ci.org/down ...
- 「2019.8.11 考试」一套把OI写的很诗意的题
这次写的更惨了,T2暴力再次挂掉了. 先写了T1的75暴力,然后写了T2的70分暴力(挂成了25),T3啥也不会骗了12分.T3看完题一点思路没有,心态爆炸了,一直在观察数据,忽略的思考的重要性,以至 ...
- formdata,ajax提交数据
var data = document.getElementById("#dataForm"); var formData = new FormData(data); var ac ...
- P3665 [USACO17OPEN]Switch Grass
题目描述 N个点M条边的无向图,每个点有一个初始颜色,每次改变一个点的颜色,求改变后整张图上颜色不同的点之间的距离最小值. 思路 考虑整张图的距离最小值一定是一条边,而不可能是一条路径,那么显然这条边 ...
- 2.基础:Vue组件的核心概念
一.组件基础和注册 组件概念 组件系统是 Vue 的另一个重要概念,他的核心就是封装和复用. 细节 组件的name必须是全局唯一. 二.属性.事件和插槽 组件的三大核心概念:属性.事件和插槽. 属性, ...