React源码解析——ReactAPI
一、API背景
api的具体转化关系
可以通过到https://babeljs.io/repl/网站去将我们创建的Jsx进行实时的转译
const React = {
Children: {
map,
forEach,
count,
toArray,
only,
}, createRef,
Component,
PureComponent, createContext,
forwardRef, Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement, version: ReactVersion, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};
请先无视__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED这个属性引用得是ReactSharedInternals
,其中包括了ReactCurrentOwner
和ReactDebugCurrentFrame
(仅dev),ReactCurrentOwner是fiber算法用到的,这样是把renderer
完全独立,所以以后即使换个render算法也没有问题,ReactDebugCurrentFrame则是用来调试render过程的,
Children
这个对象提供了一堆帮你处理props.children
的方法,因为children
是一个类似数组但是不是数组的数据结构,如果你要对其进行处理可以用React.Children
外挂的方法。
createRef
新的ref
用法,React即将抛弃<div ref="myDiv" />
这种string ref
的用法,将来你只能使用两种方式来使用ref
class App extends React.Component{ constructor() {
this.ref = React.createRef()
} render() {
return <div ref={this.ref} />
// or
return <div ref={(node) => this.funRef = node} />
} }
Component & PureComponent
这两个类基本相同,唯一的区别是PureComponent
的原型上多了一个标识
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
这是检查组件是否需要更新的一个判断,ctor
就是你声明的继承自Component or PureComponent
的类,他会判断你是否继承自PureComponent
,如果是的话就shallowEqual
比较state
和props
。
顺便说一下:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate
方法,二就是这里的PureComponent
判断
createContext
createContext
是官方定稿的context
方案,在这之前我们一直在用的老的context API
都是React不推荐的API,现在新的API释出,官方也已经确定在17大版本会把老API
去除。
新API的使用方法:
const { Provider, Consumer } = React.createContext('defaultValue') const ProviderComp = (props) => (
<Provider value={'realValue'}>
{props.children}
</Provider>
) const ConsumerComp = () => (
<Consumer>
{(value) => <p>{value}</p>}
</Consumber>
)
后面讲context
会专门比较新老的API的差异,提前说一句,老API的性能不是一般的差
forwardRef
forwardRef
是用来解决HOC组件传递ref
的问题的,所谓HOC就是Higher Order Component
,比如使用redux
的时候,我们用connect
来给组件绑定需要的state,这其中其实就是给我们的组件在外部包了一层组件,然后通过...props
的方式把外部的props
传入到实际组件。forwardRef
的使用方法如下:
const TargetComponent = React.forwardRef((props, ref) => (
<TargetComponent ref={ref} />
))
这也是为什么要提供createRef
作为新的ref
使用方法的原因,如果用string ref
就没法当作参数传递了。
这里只是简单说一下使用方法,后面讲ref
的时候会详细分析。
类型
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,
这四个都是React提供的组件,但他们呢其实都只是占位符,都是一个Symbol
,在React实际检测到他们的时候会做一些特殊的处理,比如StrictMode
和AsyncMode
会让他们的子节点对应的Fiber的mode
都变成和他们一样的mode
createElement & cloneElement & createFactory & isValidElement
createElement
可谓是React中最重要的API了,他是用来创建ReactElement
的,但是很多同学却从没见过也没用过,这是为啥呢?因为你用了JSX,JSX并不是标准的js,所以要经过编译才能变成可运行的js,而编译之后,createElement
就出现了:
// jsx
<div id="app">content</div> // js
React.createElement('div', { id: 'app' }, 'content')
cloneElement
就很明显了,是用来克隆一个ReactElement
的
createFactory
是用来创建专门用来创建某一类ReactElement
的工厂的,
export function createFactory(type) {
const factory = createElement.bind(null, type);
factory.type = type;
return factory;
}
他其实就是绑定了第一个参数的createElement
,一般我们用JSX进行编程的时候不会用到这个API
二、ReactElement
ReactElement
通过createElement
创建,调用该方法需要传入三个参数:
- type
- config
- children
type
指代这个ReactElement
的类型
- 字符串比如
div
,p
代表原生DOM,称为HostComponent
- Class类型是我们继承自
Component
或者PureComponent
的组件,称为ClassComponent
- 方法就是
functional Component
- 原生提供的
Fragment
、AsyncMode
等是Symbol,会被特殊处理 - TODO: 是否有其他的
从源码可以看出虽然创建的时候都是通过config
传入的,但是key
和ref
不会跟其他config
中的变量一起被处理,而是单独作为变量出现在ReactElement
上。
在最后创建ReactElement
我们看到了这么一个变量$$typeof
,这是个啥呢,在这里可以看出来他是一个常量:REACT_ELEMENT_TYPE
,但有一个特例:ReactDOM.createPortal
的时候是REACT_PORTAL_TYPE
,不过他不是通过createElement
创建的,所以他应该也不属于ReactElement
export function createElement(type, config, children) {
// 处理参数 return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
} const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props, // Record the component responsible for creating this element.
_owner: owner,
}; return element
}
ReactElement
只是一个用来承载信息的容器,他会告诉后续的操作这个节点的以下信息:
type
类型,用于判断如何创建节点key
和ref
这些特殊信息props
新的属性内容$$typeof
用于确定是否属于ReactElement
这些信息对于后期构建应用的树结构是非常重要的,而React通过提供这种类型的数据,来脱离平台的限制
二、React Children
最开始
React.Children
这个 API 是不想讲的,一方面平时不怎么用,另一方面跟数组处理功能差不多,不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是map
和forEach
,我们就按照map
的流程来看一下,forEach
其实差不多,只是没有返回新的节点。
先来看一下流程图:
开始源码
function mapChildren(children, func, context) {
if (children == null) {
return children
}
const result = []
mapIntoWithKeyPrefixInternal(children, result, null, func, context)
return 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)
}
map
和forEach
的最大区别就是有没有return result
。
getPooledTraverseContext
就是从pool
里面找一个对象,releaseTraverseContext
会把当前的context
对象清空然后放回到pool
中。
const POOL_SIZE = 10
const traverseContextPool = []
function getPooledTraverseContext() {
// args
if (traverseContextPool.length) {
const traverseContext = traverseContextPool.pop()
// set attrs
return traverseContext
} else {
return {
/* attrs */
}
}
} function releaseTraverseContext(traverseContext) {
// clear attrs
if (traverseContextPool.length < POOL_SIZE) {
traverseContextPool.push(traverseContext)
}
}
那么按照这个流程来看,是不是pool
永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到React.Children.map
的一个特性了,那就是对每个节点的map
返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。
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') {
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,
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') {
// iterator,和array差不多
} else if (type === 'object') {
// 提醒不正确的children类型
}
} return subtreeCount
}
这里就是一层递归了,对于可循环的children
,都会重复调用traverseAllChildrenImpl
,直到是一个节点的情况,然后调用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,
keyPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/'
: '') +
childKey,
)
}
result.push(mappedChild)
}
}
mapSingleChildIntoContext
这个方法其实就是调用React.Children.map(children, callback)
这里的callback
,就是我们传入的第二个参数,并得到map
之后的结果。注意重点来了,如果map
之后的节点还是一个数组,那么再次进入mapIntoWithKeyPrefixInternal
,那么这个时候我们就会再次从pool
里面去context
了,而pool
的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和gc
的损耗。
而如果不是数组并且是一个合规的ReactElement
,就触达顶点了,替换一下key
就推入result
了。
React 这么实现主要是两个目的:
- 拆分
map
出来的数组 - 因为对
Children
的处理一般在render
里面,所以会比较频繁,所以设置一个pool
减少声明和gc
的开销
这就是Children.map
的实现,虽然不算什么特别神奇的代码,但是阅读一下还是挺有意思的。
react性能优化之ConcurrentMode 和 flushSync
使用 ConcurrentMode 组件包裹的子组件的渲染过程的优先级会被降低,react 会先渲染优先级高的,然后将js线程空闲出来先干其他的事,如动画的渲染,完了之后再渲染优先级低的,当我们想提高子组件渲染的优先级的时候,可以使用flushSync方法来包裹需要进行的操作。
React源码解析——ReactAPI的更多相关文章
- 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源码解析:setState
先来几个例子热热身: ......... constructor(props){ super(props); this.state = { index: 0 } } componentDidMount ...
- React源码解析-Virtual DOM解析
前言:最近一直在研究React,看了陈屹先生所著的深入React技术栈,以及自己使用了这么长时间.对React应该说有比较深的理解了,正好前阵子也把两本关于前端设计模式的书看完了,总感觉有一种知识错综 ...
- React源码解析——创建更新过程
一.ReactDOM.render 创建ReactRoot,并且根据情况调用root.legacy_renderSubtreeIntoContainer或者root.render,前者是遗留的 API ...
- React躬行记(16)——React源码分析
React可大致分为三部分:Core.Reconciler和Renderer,在阅读源码之前,首先需要搭建测试环境,为了方便起见,本文直接采用了网友搭建好的环境,React版本是16.8.6,与最新版 ...
- React的React.createContext()源码解析(四)
一.产生context原因 从父组件直接传值到孙子组件,而不必一层一层的通过props进行传值,相比较以前的那种传值更加的方便.简介. 二.context的两种实现方式 1.老版本(React16.x ...
- React的React.createRef()/forwardRef()源码解析(三)
1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...
- React的Component,PureComponent源码解析(二)
1.什么是Component,PureComponent? 都是class方式定义的基类,两者没有什么大的区别,只是PureComponent内部使用shouldComponentUpdate(nex ...
随机推荐
- 华为的Java面试题,仅供参考。
IP地址的编码分为哪俩部分? IP地址由两部分组成,网络号和主机号.不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位. 2.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该 ...
- Day06 - Fetch、filter、正则表达式实现快速古诗匹配
Day06 - Fetch.filter.正则表达式实现快速古诗匹配 作者:©liyuechun 简介:JavaScript30 是 Wes Bos 推出的一个 30 天挑战.项目免费提供了 30 个 ...
- 大厂常问iOS面试题--性能优化篇
1.造成tableView卡顿的原因有哪些? 1.最常用的就是cell的重用, 注册重用标识符 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell 如果有很多数据的时候 ...
- 初窥构建之法——记2020BUAA软工个人博客作业
项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 完成一次完整的软件开发经历并以博客的方式记录开发过程的心得掌握 ...
- jsp内置对象(三)-----response对象
response对象 response对象包含了响应客户端请求的有关信息,但在JSP中很少直接用到它.他是HttpServletResponse类的实例,response对象具有页面作用域,即访问一 ...
- ysoserial源码结构分析
1.前言 之前也花了几天晚上熟悉了一下commonscollections系列的构造,那么学习一下这个项目是如何设计的也挺重要,多学习大佬如何写代码应该也能对自己的代码能力有提升吧~2333 2.项目 ...
- 使用java短信验证
package cn.geekss.util; import java.io.BufferedReader;import java.io.InputStreamReader;import java.i ...
- 教你用纯Java实现一个网页版的Xshell(附源码)
前言 最近由于项目需求,项目中需要实现一个WebSSH连接终端的功能,由于自己第一次做这类型功能,所以首先上了GitHub找了找有没有现成的轮子可以拿来直接用,当时看到了很多这方面的项目,例如:Gat ...
- CVPR 2020 全部论文 分类汇总和打包下载
CVPR 2020 共收录 1470篇文章,根据当前的公布情况,人工智能学社整理了以下约100篇,分享给读者. 代码开源情况:详见每篇注释,当前共15篇开源.(持续更新中,可关注了解). 算法主要领域 ...
- Python基础篇(一)_基本语法元素
Python基础篇——基本语法元素 缩进:体现强制可读性,一般缩进4个空格.一个或多个Tab 注释:单行注释----以 # 开头 多行注释----每行以 # 开头,以 # 结束 变量:无须提前声明.可 ...