今天开始学习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.createElementReactElement通过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 | 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
 
        