# React源码解析
# 1、整体结构
- react:核心类库,hooks的api、element、context、ref、Component、lazy、memo等
- react-dom:csr和ssr相关dom渲染以及test相关api,render、hydrate、renderToString等
- react-recondiler:fiber架构核心
重点:fiber架构、diff算法
# 2、react
# 1、createElement
jsx通过babel的插件(babel-plugin-jsx-transform)转换为createElement函数,createElement函数最终输出Vnode节点对象
- 头文件需引入React库(import React from 'react'),由于babel转换后会使用React.createElement函数
- React17+版本后可不引入React库
TIP
jsx与模版的对比:
jsx较复杂无法进行编译优化,只能进行简单转译,委托给babel进行编译未集成至框架侧
vue框架使用模版语法,进行编译预处理优化提升性能,集成在框架侧
发展历程:
react采用自顶向下进行状态更新(无编译预处理优化),通过shouldComponentUpdate的api向开发者提供是否向下进行更新开关进行性能优化 -> Fiber架构(利用时间切片保证渲染不卡顿)-> Streaming Server Rendering with Suspense流式渲染
vue通过模版预处理进行不端优化,添加动静分离处理进行靶向更新,添加事件缓存等处理
//1、转换编译原理
/*
const title = <div className="title">Hello world!</div>
const title = React.createElement('div', {className: 'title'}, 'Hello world!')
*/
//2、用法:最终输出vnode节点
/*
React.createElement(
type, //标签名字符串、react组件类型、React fragment类型
[props],
[...children]
)
{
$$typeof: Symbol(react.element),
type: 'div',
props:{
className: 'div',
children: [
{
type: '',
props: {
nodeValue: 'Hello world!'
}
}
]
},
key: null,
ref: null
}
*/
//3、对于嵌套组件、类组件(本质也是函数组件)、函数组件
/*
const test = <div>
<Container/>
<div>age: 18</div>
</div>
vnode对象为:
{
$$typeof: Symbol(react.element),
type: 'div',
props:{
className: 'div',
children: [
{
$$typeof: Symbol(react.element),
type: container,
key: null,
ref: null,
props: {}
},
{
$$typeof: Symbol(react.element),
type: 'div',
key: null,
ref: null,
props: 'age: 18'
}
]
},
key: null,
ref: null
}
*/
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
- createElement简易实现
const isArry = Array.isArray
const isText = txt => typeof txt === 'string' || txt === 'number'
//数组转换
//解析:??类似||,不会进行控制转换
//1. obj || {}, 0\""\false\null\undefined为{}
//2. obj ?? {}, null\undefined为{}
const toArray = arr => isArry(arr ?? []) ? arr : [arr]
//数组扁平化
const flatten = arr =>
[...arr.map(ar =>
isArry(ar) ? [...flatten(ar)]
: isText(ar) ? createTextVNode(ar) : ar)]
//正常节点转换
function createVNode(type, props, key, ref) {
return {
type, props, key, ref
}
}
//文本节点转vnode
function createTextVNode(text) {
return {
type: '',
props: {
nodeValue: text + ""
}
}
}
export function createElement(type, props, ...kids) {
props = props ?? {}
kids = flatten(toArray(props.children ?? kids)).filter(Boolean)
if (kids.length) props.children = kids.length === 1 ? kids[0] : kids
const key = props.key ?? null
const ref = props.ref ?? null
delete props.key
delete props.ref
return createVNode(type, 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
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
- Fragment原理
//Fragment原理:<>,</> -> <Fragment></Fragment>
export function Fragment(props){
return props.children
}
1
2
3
4
2
3
4
# 2、cloneElement
以 element
元素为样板克隆并返回新的 React 元素
config
中应包含新的 props,key
或ref
。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。- 新的子元素将取代现有的子元素,如果在
config
中未出现key
或ref
,那么原始元素的key
和ref
将被保留 - 常用在路由组件拦截中
<element.type {...element.props} {...props}>{children}</element.type>
1
//源码查看直接查看github,node_modules中为打包代码,简化代码
//https://github.com/facebook/react/blob/v16.8.6/packages/react/src/ReactElement.js
export function cloneElement(element, config, children) {
let propName;
const props = Object.assign({}, element.props);
let key = element.key;
let ref = element.ref;
const self = element._self;
const source = element._source;
let owner = element._owner;
if (config != null) {
for (propName in config) {
props[propName] = config[propName];
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return ReactElement(element.type, key, ref, self, source, owner, props);
}
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
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
# 3、PureComponent
React.PureComponent
中以浅层对比 prop 和 state 的方式来实现了shouldComponentUpdate
函数
//https://github.com/facebook/react/blob/v16.8.6/packages/react/src/ReactBaseClasses.js
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
//寄生组合继承
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
//https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberClassComponent.js
//先执行shouldComponentUpdate函数,没有的话自动执行判断
if (typeof instance.shouldComponentUpdate === 'function') {
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
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
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
# 3、Fiber架构
# 1、调度器scheduler
利用时间切片轮转,将任务放在空闲时间执行,使用idle执行机制,在浏览器空闲时间执行
- 浏览器自带idle的api(requestIdleCallback),react未使用,react采用Messagechannel自己实现了idle机制,因为兼容性以及50ms渲染问题
- 不使用generator实现scheduler,原因:改动较大,以及generator有状态
- 不使用web-worker实现scheduler,原因:web-worker属于结构化克隆为深拷贝,消耗性能
- 为什么使用Messagechannel?不使用setTimeout、raf等
- 事件循环中执行顺序为:task-queue(执行栈) -> micro-tasks(微任务) -> render -> macro-tasks(宏任务),为了不影响渲染需使用宏任务
- setTimeout、setInterval等宏任务,时间有延迟,后期执行间隔为4ms-5ms
- requestAnimationFrame由浏览器决定渲染时间,且递归调用有重复调用问题
scheduler干的事情:调度控制任务的进出
- 把任务放进一个队列,然后开始(以某种节奏ric(requestIdleCallback)执行)
- 判断是否可以执行:shouldYield -> should yield -> generator yield(本质返回true和false函数)
解析:当状态更新时,放入队列,等待调度器执行,调度器有序有节奏的执行任务队列,采用时间分片,在浏览器空闲时间执行
/* 解析:总体实现ric,每一帧16.667空闲时间执行任务
schedule:放入任务,开始执行startTranstion
startTranstion:transtions放入flush,触发宏任务,在宏任务中执行flush
flush:取出任务,判断是否超时及输入
无,循环执行任务,清空队列
有,不执行任务,执行startTranstion(flush),在下一个宏任务中执行任务
*/
const queue = []
const threshold = 1000 / 60 //浏览器1帧16.667ms
// git transtions
const transtions = []
let deadline = 0
const now = () => performance.now()
const peek = arr => arr[0]
export function startTranstion(cb) {
transtions.push(cb) && postMessage()
}
// 二合一,push / exec
export function schedule(cb) {
queue.push({ cb })
startTranstion(flush)
}
//触发宏任务,渲染之后,执行宏任务
const postMessage = (() => {
const cb = () => transtions.splice(0, 1).forEach(c => c())
const { port1, port2 } = new MessageChannel()
port1.onmessage = cb
return () => port2.postMessage(null)
})()
export function shouldYield () {
return navigator.scheduling.isInputPending() || now() >= deadline
}
function flush() {
deadline = now() + threshold
let task = peek(queue)
while(task && !shouldYield()) {
const { cb } = task
task.cb = null
//调度器只执行next,与fiber架构解耦
const next = cb()
if (next && typeof next === 'function') {
task.cb = next
} else { //任务执行完
queue.shift()
}
task = peek(queue)
}
task && startTranstion(flush)
}
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
50
51
52
53
54
55
56
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
50
51
52
53
54
55
56
# 4、Diff算法
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。