'React 源码'
就是在阅读《深入React技术栈》的第三章时,因为版本的问题对于书上的讲解和自己对于代码的阅读出入导致的学习压力,打算放弃参考书上的内容。
参考和引用/使用的链接 ↓
以及本文参考的源码版本是 v16.6 (小部分不是,有标注
JSX 编译 | React.createElement()
JSX 编译
可以在 Babel 在线编译里玩。
JSX 大家都知道是 JavaScript 的语法拓展 ,ReactJS并没有强制必须使用 JSX ,但是它可以定义 结构、样式以及逻辑,我真的很喜欢hhh。
JSX 在实际的使用会被 babel 编译成传统的 JavaScript 语句。
举个例子:
1 | <Demo key="1" style={{color: red}}> |
-> JSX 都会被编译成 React.createElement(type, config [, children])
-> 第一参数为 type 。当是自定义的组件时(大写开头),传入的参数是一个变量,而是 HTML 的标签时(小写开头,虽然 HTML 大小写不敏感),传入的是字符串 。如下面的例子中的 createElement 的第一个参数 是 "p" [字符串],而上面的例子中的第一个参数是 Demo [变量]。这也就是为什么自定义的组件必须大写开头。
1 | <p> |
-> 第二个参数为 config 。是一个对象或者是 null 。内容就是在 ReactJS 中称为 props 的东西(包括 key 和 ref)。
-> 第三个参数为 children 。可以有零个、一个以及多个 。需要注意的是,当有多个 children 时,传入参数并不是一整个数组,而是多个独立的参数,是在函数内部被整合成一个数组的。
React.createElement()
其实这个才是最常用的 React API 。
源码坐标: packages/react/src/ReactElement.js
1 | export function createElement(type, config, children) {} |
可以看到是对外开放的函数,参数有三个,分别对应 jsx 的内容。
type
对于 type 参数没有多做处理,有两个用到的地方
- 对于 defaultProps 处理时,判断了是否有 type 传入 以及该 type 是否有 defaultProps。
- 作为 ReactElement() 参数 , 这里的 ReactElement() 是 createElement() 的返回值。
config
config 的值有两种:null | Object
对 config 的处理的源码:
1 | //仅当 config 不为 null 时进行处理。 |
这里对 ref 和 key 还有 self 和 source 进行了特殊的处理
首先是对 ref 和 key 进行了合法判断。源码如下:
1 | function hasValidRef(config) { |
通過Ref屬性的取值器對象的isReactWarning屬性檢測是否含有合法的Ref,在開發環境下,如果這個props是react元素的props那麼獲取上面的ref就是不合法的,因爲在creatElement的時候已經調用了defineRefPropWarningGetter。生產環境下如果config.ref !== undefined,說明合法。
参考博客
其实上面这段话也挺难理解的。上面的引用里提到了 isReactWarning 这个属性来判断是否合法。在源码的文件里搜索这个关键字可以看到下面的函数。(下面的函数是用于 key 的,对于 ref 是进行的同样的处理。)
1 | function defineKeyPropWarningGetter(props, displayName) { |
可以看到在开发环境下,会给组件的 props 设置 key/ref 属性,属性值为一个对象,具有 get 和 configurable。get 里设有 isReactWaring = true 用于避免子组件通过 props 获取 ref/key。
在这里的理解卡了很久(我好菜),但是现在理解了。
简单的说:key/ref 都是在父组件调用了子组件时,与定义其他常规 props 一样的方式定义的,所以 key/ref 会出现在 createElement() 的 config 参数里。而在子组件里,是不允许子通过 this.props 获取到自己的 ref 和 key 的。 defineKeyPropWarningGetter 为 props 这个对象添加了 key/ref 两个属性,而这两个属性存在一个 getter ,而这个 getter 的 isReactWarning 属性是 true,借用这个 isReactWarning 来达到避免通过 props.ref/props.key 获取到这两个特殊的属性值。
要说起来 self 和 source 也是挺特殊的,但是完全什么用到过,源码里也没特殊的操作,所以就就不来管了。
children
对 JSX 进行编译的时候传入 React.createElement() 的参数可能是两个,可能是三个也可能是四个以上。需要注意的是传入的 children 参数,并不是一个数组,而是多个参数。
对 children 处理的源码:
1 | // Children can be more than one argument, and those are transferred onto |
当 children 参数不存在时,不做处理
当 children 只有一个时,直接作为 props 的 children 属性传入。
当 children 有多个参数时,使用 arguments 关键字,遍历参数数组,将第三个参数开始之后的所有参数整合为一个数组,并把这个新数组作为 props 的 children 属性的值。
对 defaultProps 的处理
子组件定义时,可以设置默认 props ,如果父组件在调用子组件没有重写这个 props ,则就会使用默认值。
而在 createElement() 函数中对于默认的 props 也进行了处理,源码如下:
1 | // Resolve default props |
React.createElement() 的返回值
1 | return ReactElement( |
ReactElement() 是一个内部方法,返回值是一个 element 。
ReactElement()
函数首字母为大写,但是并不是 class 而是普通的 function 。
函数返回一个对象 ,源码如下:
1 | const element = { |
$$typeof 用于标识这个 element 的类型 。通过 React.createElement() 生产的元素的 $$typeof 一定是 REACT_ELEMENT_TYPE
type 用于标识节点类型,是原生 DOM 节点还是类组件还是函数组件以及一些 React 提供的其他 component 类型。
key/ref/props 就是 createElement 里处理的那些内容。
Component
类组件都会继承 React.Component 。
对于 Component 的定义在 packages/react/src/ReactBaseClasses.js
1 | function Component(props, context, updater) { |
Component 就是一个 function ,没有什么特殊地方。
setState()
1 | Component.prototype.setState = function(partialState, callback) { |
源代码里在 Component 的原型上定义了 setState 函数。
接收的参数一个是 particalState ,是更新的目标状态,类型可以是 object 和 function 。( 推荐使用 function
invariant() 用于验证传入的 partialState 是否正常。
而 Component.setState 真正的作用是调用了 updater 的 enqueueSetState 方法 ( 这是 react-dom 里的内容。
选择使用传入的 updater 的 enqueueSetState 方法的原因是 react 是个平台通用的,而渲染是与平台有关的,可以理解为 react-dom 、raect-native 两个平台,因此需要有这个 updater 来根据平台来定制实现方法。
forceUpdate()
如 setState() 一般,也是挂在 Component 的原型上的函数。
功能为强制 Component 更新,即使没有 state 变动。
1 | Component.prototype.forceUpdate = function(callback) { |
PureComponent
1 | function ComponentDummy() {} |
(这语法也太高级了。。。
PureComponent 与 Component 差不多,区别在于最后一行代码。
pureComponentPrototype 有一个 isPureReactCompoent 属性 ,用该属性表示继承 PureComponent 的组件,在之后的更新过程中 react-dom 会根据这个属性来判断一个组件是否是 PureComponent 。
Ref
虽然每一个 ReactJS 的教程里都会介绍到 ref 并且再多说一句不推荐使用,但是真的挺好用的(
而且一些情况下往往不得不用(比如说运用到数据可视化图表的时候。
ref 获取到的是实例,而函数组件是没有实例的。因为函数组件挂载时只是调用了函数,没有创建实例。所以要通过 ref 获取到函数组件需要一些特别的操作。
使用方法
stringRef
是逐渐被淘汰的方式
其实有留意到 Component 定义的时候有一行注释
// If a component has string refs, we will assign a different object later.
但是还不知道为什么 hhh
创建方式相比其他两种方式最为简单
1 | //创建方式 直接在节点的 props 位置设置 ref 属性 并使用一个字符串作为属性值 |
function
给 ref 属性传入的是一个方法
1 | //创建方式 同样直接在节点的 props 位置设置 ref 属性 但是属性值是一个 function ele 对应的就是节点实例 |
createRef()
ReactJS 提供的 API。
★★推荐使用★★
1 | //创建方式 先在 constructor 创建对象 |
createRef() 源码
坐标: packages/react/src/ReactCreateRef.js
1 | // an immutable object with a single mutable value |
除去开发模式的代码不管的话,只是简单的返回了一个对象 对象含有一个值为 null 的 current 属性。
注释里提到的这个对象是不可变的(就是在开发环境下调用了 Object.seal()
forwardRef
两个前提:
在上面的 React.createElement() 里可以看到 ref 是不会作为 props 传入子组件的
ref 获取的是节点的实例 所以引用 function component 的 ref 会报错。
forward-ref 的使用 demo
1 | import React from 'react' |
源码也非常的简单,除去 dev 代码仅仅返回一个对象
1 | import {REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE} from 'shared/ReactSymbols'; |
除去 __DEV__ 代码 ,实现的功能仅为反悔了一个对象,包含 $$typeof 和 render 两个属性。
这里的 $$typeof 是 REACT_FORWARD_REF_TYPE ,而 ReactElement() 里返回的 element 对象的 $$typeof 是 REACT_ELEMENT_TYPE 。
↑ 虽然提了一句两个 $$typeof 的值不同,其实根本就不是一个东西。。 这个 forwardRef 返回的对象在上面的 demo 中是 Target ,挂载时会作为 createElement 的 type 属性的值传入,最终也作为 ReactElement() 里返回的 element 对象 type 的值,不会于 element 对象的 $$typeof 属性冲突。
总而言之,所有通过 createElement() 生成的组件的 $$typeof 都是 REACT_ELEMENT_TYPE !
Context
子组件可以通过 context 获取到好几层之上(中间隔了好几层组件)的祖辈组件的值,反过来就是某一个父组件定义了某个 context 之后其子组件都能获取到。
再简而言之,就是跨多层组件沟通。
有两种使用方式
- childContextType (将淘汰
- createContext (推荐
childContextType
1 | // 在父组件中定义 |
createContext()
1 | // 在上层组件中新建 Context 对象 |
源码
1 | import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; |
就是返回了一个 context 对象。该对象包含两个属性一个 Provider ,其值也是对象,一个 $$tpyeof 标志,另一个 _context 值为 context 本身,而另一个属性 Consumer ,其值为 context 对象本身。
Suspense & Lazy
React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。 –>官方文档
用法
目前,懒加载组件是 <React.Suspense> 支持的唯一用例。
↑ 官方说的,说是只能配合 React.Lazy 使用,但是好像如果只要 Suspense 其中的组件没有加载完成 fallback 就有效。(未测试
(但是这么强的东西,相信一定不会跟 concurrent-mode 一样的结局的!
Suspense 会在其所有的子组件都加载完成前,一直显示 fallback 的内容。
1 | const OtherComponent = React.lazy(() => import('./OtherComponent')); |
来瞅瞅我之前的另外一篇文章
(虽然有意识到写的不是很全面,但是坑嘛hhh总有以后来填的。
源码
1 | import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent'; |
1 | // 地址: react/packages/react/src/React.js |
Hooks
React 2019 人气第一 API 。(个人感觉hhh
用法
很丰富。。
所以。。。
官方文档
源码
这里只是先介绍了 API ,就如同 component 里的 setState() 函数一样,hooks 里的 useContext() 等这些函数也是某一个对象的上的方法。会在渲染过程中应用到。
因为篇幅较长 删减了一些内容(如flow的类型验证)
1 | import type { |
可以看到在这个源码文件里,所有的 hooks 相关的函数都是长下面这样
usexxx(ppp){
const dispatcher = resolveDispatcher();
return dispatcher.usexxx(ppp);
}
dispatcher 是个啥,现在还不知道(dbq
Children
처음 들어봐 !
React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。–> 官方文档
↑ 不明白什么意思其实。。。
React中props.children和React.Children的区别
使用
在官方描述中其实可以理解到 React.Children 用于对 this.props.children 进行一些更复杂的处理。
为什么是更复杂呢?因为通常来说对于 this.props.children 都是直接被用来渲染的,如下:
1 | function Child(props) { |
而某些情况下需要对 children 直接进行一些操作 ( 完全预想不到会是什么情况呢。。。dbq
举个例子
1 | function Child(props) { |
上面的例子中仅仅给每一个 child 添加了一层 span 标签。
使用的 React.Child 的 map 方法,除了 map 还有 foreach / count / toArray / only 共五个方法。
源码
观察了一下 v16.6 和 v16.8 的 ReactChildren.js 是一样的
两个版本(11个月)没有更新哦
在 React.js 里, Children 作为 React 对象的一个属性,其值为一个对象,内容如下:
1 | const React = { |
源码在 packages/react/src/ReactChildren.js 里
输出只有一个
1 | export { |
React.Children.map()
现在最眼熟的 map()
1 | /** |
对于函数的解释 翻译了一下大概就是说 映射了一下 通常来说是 props.children 的子节点, 会为了每个子节点调用传入的 mapfunction 函数(就是第二个参数)。
使用 React.Children.map 遍历子节点 不需要担心子节点是 Object 还是 undefined 还是 null。
这里对于 null 的处理已经出现了。 对于 children 是 null 的情况直接返回 null。
可能对于”为什么子节点会是 null ?”这个情况有所疑问 ( 没错,是我
1 | function hello() { |
可以在这里试试,输入上面的 demo 代码,可以看到 React.createElement 的第二个参数是 null。(但是其实好像两个 children 不是一个东西。。。
注意,这里的 mapChildren 函数有一个 return ,返回了 result 数组,这就是 map 和 foreach 最大的区别。
接下来看看调用的 mapIntoWithKeyPrefixInternal() 函数。
1 | function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { |
函数内部前四行是对返回的子节点的 key 进行操作。
1 | const userProvidedKeyEscapeRegex = /\/+/g; |
这个函数就是把传入的参数转为字符串,并把 / 符号替换成 $&/ 。
接着引用了 getPooledTraverseContext() 函数
1 | const POOL_SIZE = 10; |
传入了四个参数,判断全局变量 traverseContextPool 里是否有元素,有的话就 pop 一个并把参数挂上去,如果 contextPool 为空则返回一个由四个参数加一个 count 属性的对象。
结合之前调用这个函数的 mapIntoWithKeyPrefixInternal() 函数还调用了一个与本函数对应的函数 releaseTraverseContext()。
1 | function releaseTraverseContext(traverseContext) { |
在这个 release 函数里,把 getPooledTraverseContext() 里新建的 traverseContext 的对象内容进行了清空而不是释放对象,当 contextPool 里的元素少于 POOL_SIZE 时,再推回 contextPool,就是对象池的引用,因为对象的声明和释放可能会造成内存抖动。
除去 traverseContext 的新建和 release ,之前的函数在这两步之间还有一个操作,traverseAllChildren()
1 | function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) { |
函数里依据 children 类型进行不同的操作,如果 children 是 null(undefined 和 boolean 也作为 null 处理)、string、number 或者 $$typeof 为 REACT_ELEMENT_TYPE 、REACT_PORTAL_TYPE 时 (而这些种类的 children 特点都是 都不是数组或者说可遍历对象,都是单节点 ),会直接调用传入的 callback ,就是 mapSingleChildIntoContext 这个函数。
而如果 children 是数组,会对 children 进行遍历,并对数组每个元素调用函数自身。
1 | function mapSingleChildIntoContext(bookKeeping, child, childKey) { |
函数接收三个参数,第一个 bookKeeping 就是之前的 traverseContext, 第二个是 children 本身 (单节点), 第三个是节点的 key 。
在这个函数里终于调用了我们最开始传入 React.Children.map 的第二个函数参数,调用完成后判断返回的值是否是数组,如果是数组,会重新调用 mapIntoWithKeyPrefixInternal() ,这里就又是一层递归。
那么又来了,为什么返回值会是一个数组嘞?
1 | function hello (props) { |
然后确定已经是单节点了,判断一下是否是合理的元素,是合理的元素的话,调用 cloneAndReplaceKey() 函数,新建了一个 React 元素,并重置 key 。
1 | // 在 ReactElement.js 里 |
整体来说通过两层递归来实现对 props.children 的处理。
React.Children.forEach()
和 map() 函数里调用的 mapIntoWithKeyPrefixInternal() 长得一模一样。
与 map() 之间的区别在于 forEach() 没有返回值,在上面对 map() 的阅读时,强调了有一个 result 作为 map() 的返回值,强调的原因也就在这里。
1 | function forEachChildren(children, forEachFunc, forEachContext) { |
React.Children.toArray()
调用了 mapIntoWithKeyPrefixInternal() ,除了没有 context 作为参数意外,第四个参数 也是 c => c 。
也是将数组展开。
1 | function toArray(children) { |
React.Children.onlyChild()
就是判断下是否是单节点。。。
1 | function onlyChild(children) { |
React.Children.count()
1 | /** |
函数注释里也说了,就是用来数一下通常是 props.children 的 children 里有多少个子节点。
Memo
作用大概就是说允许函数组件可以像 PureComponent 一样,只有在 props 的内容更新的时候才重新渲染组件。目的也就很清晰,是为了性能优化。
React.memo 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但不适用于 class 组件。 –> React.memo
使用
看一个官方 demo ,就可以清晰的认知到 memo 的作用。
React.memo 就是一个 React 的顶层 api ,第一个参数为函数组件,第二个参数为一个用于比较的函数。
1 | function MyComponent(props) { |
源码
1 | export default function memo<Props>( |
接收两个参数,第一个函数组件,第二个是对比函数,返回值是一个 boolean 。
返回的对象的 type 属性的值就是传入的函数组件。
Fragment
这个就是那个,长得很可爱的 <></>,
就是 return 的时候只能返回一个元素嘛,但是又不想有额外的东西,就可以用这个。
用法
又到了康康栗子就会环节
1 | // 官方 demo |
两个 demo 都到了 Fragment 。区别在于非官方 demo 里 return 没有加分号 用 <> 代替了 <React.Fragment>
源码
hmm 就是一个 symbol。
在 React.js 文件里
1 | import { |
那么第一章节就结束了
主要就是浏览了一下顶层的API
可能上面有提到 react 其实内容很少,主要原因是 react-dom 和 react-native 分别处理了不同的平台的活动,所以 react 里很精简的只有共同用的一些内容。
其实上面的所有内容都是对于 API 有了很浅显的了解 ,更多的在于知道了调用了某个 api 之后会进行怎么样的初始化,而后续的操作都要在 react-dom 里看到 (慢慢来嘛
ㅅㄱ ~