今天开始学习react源码相关的内容,源码基于版本v16.6.0
。React16相较于之前的版本是核心上的一次重写,虽然主要的API都没有变化,但是增加了很多能力。并且首次引入了Fiber
的概念,之后新的功能都是围绕Fiber
进行实现,比如AsyncMode
,Profiler
等。
React与ReactDom的区别
问题:react仅仅1000多行代码,而react-dom却将近2w行
答: React主要定义基础的概念,比如节点定义和描述相关,真正的实现代码都是在ReactDom里面的,也就是“平台无关”概念,针对不同的平台有不同的实现,但 基本的概念都定义在React里。
React16.6.0使用FlowType做类型检查
Flow 是 facebook 出品的 JavaScript 静态类型检查⼯具。所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,⼜不影响代码运⾏(不需要运⾏时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语⾔相近的体验。
简单示例🌰
1
2
3npm install -g flow-bin
flow init
touch index.js
1 | // index.js 进行类型注释 |
React暴露的Api
1 | const React = { |
JSX转换成什么
- 核心
React.createElement
ReactElement
通过createElement
创建,调用该方法需要传入三个参数- type
- config
- children
type指代这个ReactElement的类型
- 字符串比如
div
原生DOM,称为HostComponent
首字母小写 - 自定义组件变量(
functional Component
/ClassComponent
)首字母大写不大写会识别为原生DOM解析 - 原生提供的组件这四个都是
1
2
3
4Fragment: 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
。
config
react会把关键参数解析出来,例如key
、ref
,在createElement
中识别分离,这些参数不会和其他参数一起处理而是单独作为变量出现在
ReactElement
上。
children
第三个参数就是children,而且可以有任意多的参数,表示兄弟节点。可以通过this.props.children
访问到。
相关源码以及注解⬇️
1 | export function createElement(type, config, children) { |
ReactElement
ReactElement
只是一个用来承载信息的容器,它会告诉后续的操作这个节点的以下信息:
- type类型,用于判断如何创建节点
- key和ref这些特殊信息
- props新的属性内容
- $$typeof用于确定是否属于
ReactElement
React通过提供这种类型的数据,来脱离平台的限制。
$$typeof
在最后创建ReactElement
我们👀看到了这么一个变量$$typeof
。这是啥呢?React元素,会有一个$$typeof
来表示该元素是什么类型。当本地有
Symbol
,则使用Symbol
生成,没有时使用16进制。但有一个特例:ReactDOM.createPortal
的时候是REACT_PORTAL_TYPE
,不过它不是通过
createElement
创建的,所以它也不属于ReactElement
1 | export let REACT_ELEMENT_TYPE = 0xeac7; |
cloneElement
1 | // 第一个参数传入ReactElement,第二、三个参数和createElement一致 |
React.cloneElement()几乎等同于
1 <element.type {...element.props} {...props}>{children}</element.type>
劫持组件
1 | function AddStyle({children}) { |
Component和PureComponent两个基类(ReactBaseClasses.js)
以element为样板克隆返回新的React元素,返回的props为新和旧的props进行浅层合并后的结果。新的子元素会替代旧的子元素,但是key和ref会保留。
源码解析↓
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50function Component(props, context, updater) {
this.props = props;
this.context = context;
// 使用string ref
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
// 校验第一个参数
// partialState: 要更新的对象
// 新的react版本推荐setState使用方法 => this.setState((preState) => ({count:preState.count+1}))
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
// 更新队列 实现在react-dom里 整个Component的初始化入口
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
// 强制更新
Component.prototype.forceUpdate = function(callback) {
// 同样也在react-dom里实现
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
/**
* 和Component一致
*/
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
// 继承
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// 复制拷贝 唯一的区别是标记上的区别 isPureReactComponent
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
有标识则会进行浅比较state和props。
React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate方法,二就是这里的PureComponent判断
1
2
3
4
5if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
设计思想
- 平台思想(React和ReactDOM分包) 抽象出概念,彻底剥离实现层,react只是处理了类型和参数的转换,不具体的实现任何业务。各个平台的实现放到ReactDom里处理。
未完待续…
createRef & ref
核心:Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
三种使用方式
- string ref 即将抛弃不推荐
- obj
- function
1 | class App extends React.Component { |
createRef
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组 件中引用它们。
如果想使用ref,只需要拿current对象即可,
源码
1 | export function createRef(): RefObject { |
访问Refs
当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。
1
const node = this.myRef.current;
- 当 ref 属性用于 HTML 元素时,current 属性为底层 DOM 元素。
- 当 ref 属性用于自定义 class 组件时,current 属性为接收组件的挂载实例。
- 不能在函数组件上使用 ref 属性,因为它们没有实例。可以通过
useRef
可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件
forwardRef
forwardRef是用来解决HOC组件传递ref的问题的。
1 | const TargetComponent = React.forwardRef((props, ref) => ( |
这也是为什么要提供createRef作为新的ref使用方法的原因,如果用string ref就没法当作参数传递了。
源码
1 | export function forwardRef<Props, ElementType: React$ElementType>( |
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件间进行数据传递的方法。
老api -> childContextType 17大版本移除 老api性能差会多次渲染
1 | // Parent |
新api -> createContext
使用
1 | // createContext的Provider和Consumer是一一对应的 |
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
源码
1 | //calculateChangedBits方法,使用Object.is()计算新老context变化 |
ConcurrentMode
ConcurrentMode有一个特性,在一个子树当中渲染了ConcurrentMode之后,它下面的所有节点产生的更新都是一个低优先级的更新。方便react区分一 些优先级高低的任务,在进行更新的过程中,优先执行一些较高的任务。
使用
1 | <ConcurrentMode> |
源码
1 | // React.js |
suspense & lazy
使用
1 | import React, { lazy, Suspense } from "react"; |
在 Suspense 内部有多个组件,它要等所有组件都 resolve 之后,它才会把 fallback 去掉,然后显示出这里面的内容,有任何一个还处于 pending 状态的,那么它还是会显示 fallback的内容.
源码
1 | Suspense: REACT_SUSPENSE_TYPE, // Suspense也是Symbol 也是一个标识 |
1 | import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent'; |
㊗️💐恭喜初中毕业了😃❀❀❀
Children详解
children由map
, forEach
, count
, toArray
, only
组成。看起来和数组的方法很类似,用于处理this.props.children
这种不透
明数据结构的应用程序。由于children几个方法的核心都是mapIntoArray
,因此这里只对map做分析,其他的可以自己去查看。
React.Children 提供了用于处理 props.children 不透明数据结构的实用方法。
- React.Children.map
- React.Children.forEach
- React.Children.count
- React.Children.only: 验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。
- React.Children.toArray: 将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。
react.children.map
map的使用实例,虽然处理函数给的是多维数组,但是通过map处理后,返回的结果其实被处理成为了一维数组。
- 如果是fragment,将会被视为一个子组件,不会被遍历。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class Child extends React.Component {
render() {
console.log(React.Children.map(this.props.children, c => [[c],[c],[c]]));
return (
<div>{
React.Children.map(this.props.children, c => [[c],[c],[c]])
}</div>
)
}
}
class App extends React.Component {
render() {
return(
<div>
<Child><p>hello1</p><p>hello2</p></Child>
</div>
)
}
}
// 渲染结果:
<p>hello1</p>
<p>hello1</p>
<p>hello1</p>
<p>hello2</p>
<p>hello2</p>
<p>hello2</p>
打印dom结构,发现每个节点都各自生成了一个key,下面会解析生成该key的步骤。
memo
与 React.PureComponent 非常相似,适用于函数组件,但不适用于 class 组件。
since React 16.6
memo用法
1 | function MyComponent(props) { |
memo源码
1 | // * react/packages/react/src/memo.js |
Fragment
不额外创建 DOM 元素的情况下,让 render() 方法中返回多个元素。
1 | // * react/packages/react/src/React.js |
StrictMode
用于检查子节点有没有潜在的问题。严格模式检查仅在开发模式下运行;它们不会影响生产构建。
1 | // * 不会对 Header 和 Footer 组件运行严格模式检查。但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查。 |
1 | // * react/packages/react/src/React.js |
参考文档
https://juejin.im/post/6855129007852109837