React源码理解

不做具体代码分析,只是作为源码思想的解读

虚拟DOM的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return (
<div>
<p>1</p>
<button>+</button>
</div>
);
}
}
let element = <App />
console.log(element);

ReactDOM.render(element, document.getElementById('root'));

打印出来的这个element对象就是虚拟DOM


属性解析:
$$typeof:
Symbol(react.element) 表示这是一个react元素;
标识元素类型的,有:div,Class,Function….
如果说$$typeof是Symbol的话可以仿XSS攻击,因为Symbol是唯一值,如果后台直接返虚拟DOM直接显示在页面上,带Symbol就可以防止后台返回的数据有恶意的虚拟DOM类型,可以避免这个问题,Symbol只有前端有,后台没有。
key: 就是唯一标识
props: 属性,这里没有所以为空
ref:
type: 类型就是这里的这个App Class 如果是函数组件就是函数App
_owner: 它的所有者是谁,谁创建了它 (带下划线的都是内部属性,不是核心属性)
_source: 是源代码哪个文件里的第几行

这就是React元素,它就是一个普通的对象,描述了真实DOM的样子

将上面line3 ~ line13 的代码放到babel中编译,如下:

1
2
3
4
5
6
7
8
9
10
11
12
class App extends React.Component {
render() {
return
React.createElement("div",
null,
React.createElement("p", null, "1"),
React.createElement("button", null, "+")
);
}

}
let element = React.createElement(App, null);

新建./react/ReactElement.js

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
64
65
66
67
68
69
70
71
72
73
74
75
import ReactCurrentOwner from './ReactCurrentOwner';
import { REACT_ELEMENT_TYPE } from '../shared/ReactSymbols';
function hasValidRef(config) {
return config.ref !== undefined;
}
function hasValidKey(config) {
return config.key !== undefined;
}
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
}
//是react-babel 将<span>A<span><span>A<span>变成数组了吗?
//createElement(type,config,spanA,spanB);
export function createElement(type, config, children) {
let propName;//定义一个变量叫属性名
const props = {};//定义一个元素的props对象
let key = null;//在兄弟节点中唯一标识自己的唯一性的,在同一个的不同兄弟之间key要求不同
let ref = null;//ref=React.createRef() "username" this.refs.username {input=>this.username = input} 从而得到真实的DOM元素
let self = null;//用来获取真实的this指针
let source = null;//用来定位创建此虚拟DOM元素在源码的位置 哪个文件 哪一行 哪一列
if (config !== null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
for (propName in config) {
if (!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName]
}
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = 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;//如果说是有多个儿子的话,props.children就是一个数组了
}
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
//只有当属性对象没有此属性对应的值的时候,默认属性才会生效,否则直接忽略
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName]
}
}
}
//ReactCurrentOwner此元素的拥有者
return ReactElement(
type, key, ref, self, source, ReactCurrentOwner.current, props
)
}
function ReactElement(type, key, ref, _self, _source, _owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
_owner,
_self,
_source
}
return element; // 这个最后生成的就是react元素即那个虚拟DOM
}

将上述babel编辑的代码转换成表示虚拟DOM的那个对象

1
Component.prototype.isReactComponent = {};

在React内部是凭借这个变量来判断是不是一个React组件的
因为组件定义有两种方式,一是类组件,一是函数组件,都被babel编译成函数

虚拟DOM的渲染

1
2
3
4
5
import ReactDOM from 'react-dom';
ReactDOM.render(
element,
document.getElementById('root')
)

新建./react-dom/index.js

1
2
3
4
5
6
7
8
9
10
11
12
import { createDOM } from '../react/vdom';
// element 就是那个虚拟DOM的对象,
// container 挂载的容器
function render(element, container) {
//1.要把虚拟 DOM变成真实DOM
let dom = createDOM(element);
//2.把直实DOM挂载到container上
container.appendChild(dom);
}
export default {
render
}

新建./react/vdom.js

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { TEXT, ELEMENT, CLASS_COMPONENT, FUNCTION_COMPONENT } from './constants';
import { onlyOne, setProps, flatten } from './utils';

export function createDOM(element) {
element = onlyOne(element);//为什么要这么写? children是一个数组
let { $$typeof } = element;
let dom = null;
if (!$$typeof) {// element 是一个字符串或者数字
dom = document.createTextNode(element);
} else if ($$typeof == TEXT) {//对象{$$typeof:TEXT}
dom = document.createTextNode(element.content);
} else if ($$typeof == ELEMENT) {
//如果此虚拟DOM是一个原生DOM节点
dom = createNativeDOM(element);
} else if ($$typeof == FUNCTION_COMPONENT) {
//如果此虚拟DOM是一个函数组件,就渲染此函数组件
dom = createFunctionComponentDOM(element);
} else if ($$typeof == CLASS_COMPONENT) {
//如果此虚拟DOM是一个类组件,就渲染此类组件
dom = createClassComponentDOM(element);
}
element.dom = dom;//不管是什么类型的元素,都让它的dom属性指向他创建出来的直实DOM元素
return dom;
}
//创建函数组件对应的真实的DOM对象
function createFunctionComponentDOM(element) {
let { type: FunctionCounter, props } = element;//type = FunctionCounter
let renderElement = FunctionCounter(props);//返回要渲染的react元素
element.renderElement = renderElement;//需要缓存,方便下次对比
let newDOM = createDOM(renderElement);
//虚拟DOM的dom属性指向它创建出来的真实DOM
renderElement.dom = newDOM;//我们从虚拟DOMReact元素创建出真实DOM,创建出来以后会把真实DOM添加到虚拟DOM的dom属性上
return newDOM;
//element.renderElement.dom=DIV真实DOM元素
}
function createClassComponentDOM(element) {
let { type: ClassCounter, props } = element;
let componentInstance = new ClassCounter(props);//创建一个ClassCounter组件的实例
//当创建类组件实例 后,会在类组件的虚拟DOM对象上添一个属性componentInstance,指向类组件实例
element.componentInstance = componentInstance;//以后组件运行当中componentInstance是不变的
let renderElement = componentInstance.render();
//在类组件实例上添加renderElement,指向上一次要渲染的虚拟DOM节点
//因为后面组件更新的,我们会重新render,然后跟上一次的renderElement进行dom diff
componentInstance.renderElement = renderElement;
let newDOM = createDOM(renderElement);
renderElement.dom = newDOM;
// element.componentInstance.renderElement.dom=DIV真实DOM元素
return newDOM;
}
/**
let element = React.createElement('button',
{ id: 'sayHello', onClick },
'say', React.createElement('span', { color: 'red' }, 'Hello')
);
*/
function createNativeDOM(element) {
let { type, props } = element;// span button div
let dom = document.createElement(type);// 真实的BUTTON DOM对象
//1.创建此虚拟DOM节点的子节点
createDOMChildren(dom, element.props.children);
setProps(dom, props);
//2.给此DOM元素添加属性
return dom;
}
function createDOMChildren(parentNode, children) {
children && flatten(children).forEach((child, index) => {
//child其实是虚拟DOM,我们会在虚拟DOM加一个属性_mountIndex,指向此虚拟DOM节点在父节点中的索引
//在后面我们做dom-diff的时候会变得非常非常重要
child._mountIndex = index;
let childDOM = createDOM(child);//创建子虚拟DOM节点的真实DOM元素
parentNode.appendChild(childDOM);
});
}


export function ReactElement($$typeof, type, key, ref, props) {
let element = {
$$typeof, type, key, ref, props
}
return element;
}

对$$typeof的判断,来创建真实DOM元素

合成事件

react本身模拟了一套事件机制

index.js

1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDOM from 'react-dom';

let onClick = (event)=>{alert( event + 'hello')};
let element = React.createElement('button',
{id: 'sayHello', onClick},
'say', React.createElement('span',{ style: { color: 'red' } }, 'hello');
)
ReactDOM.render(element, document.getElementById('root'));

分析:一个button按钮,id为sayHello,上面有一个onClick事件,包含say内容和一个span标签,span标签内容为hello,颜色为红色

新建./react/event.js

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { updateQueue } from './component';

/*
* 在React中并不是把事件绑在要绑定的DOM节点上,而是绑定到document上,类似于事件委托
* 1、因为合成事件可以屏蔽浏览器的差异,不同浏览器绑定事件和触发的方式不一样
* 2、合成事件可以实现事件对象复用,重用,减少垃圾回收,提高性能
* 3、因为默认我们要实现批量更新,setState 两个setState会合并成一次更新
*
* @params dom 要绑定事件的DOM节点
* @params eventType 事件的类型 onClick onChange
* @params listener 事件处理函数
*
*/
export function addEvent(dom, eventType, listener) {
eventType = eventType.toLowerCase(); // onClick =>onclick
// 在要绑定的DOM节点上挂载一个对象,准备存放监听函数
let eventStore = dom.eventStore || (dom.eventStore = {});
// eventStore.onclick = ()=>{ alert('hello') };
eventStore[eventType] = listener;
// document.addEventListener('click', dispatchEvent);
// dispatchEvent 事件处理,向上冒泡,最终是document处理事件
// 第一阶段是捕获,第二阶段是冒泡,false冒泡阶段,true捕获阶段
document.addEventListener(eventType.slice(2), dispatchEvent, false);
// 磨平差异
if('chrome'){
document.addEventListener();
}else if('ie6'){
document.attachEvent();
}
}
let syntheticEvent;

// 真正事件触发的回调统一是这个dispatchEvent方法
// event是原生DOM事件对象,但是传递给我们的监听函数并不是它
// 所有的事件处理函数都会进入dispatchEvent
function dispatchEvent(event) {
let { type, target } = event; // type=click , target=button
let eventType = 'on' + type;
syntheticEvent = getSyntheticEvent(event);
//在事件监听函数执行前先进入批量更新模式
updateQueue.isPending = true;
// 模拟事件冒泡
while (target) {
let { eventStore } = target;
let listener = eventStore && eventStore[eventType];
if (listener) {
listener.call(target, syntheticEvent);
}
target = target.parentNode;
}
// 等所有的监听函数都执行完了,就可以清掉所有的属性了,供下次复用次syntheticEvent对象
for (let key in syntheticEvent) {
if (syntheticEvent.hasOwnProperty(key))
delete syntheticEvent[key];
}
//当事件处理函数执行完成后,把批量更新模式改为false
updateQueue.isPending = false;
//执行批量更新,就是把缓存的那个updater全部执行了
updateQueue.batchUpdate();
}
// 持久化 异步调用事件会有问题,事件被清空了,用 事件对象.persist(); 持久化
// 如果执行了persist,就让syntheticEvent指向了新对象,while循环结束之后再清除的是新对象的属性
function persist() {
syntheticEvent = {};
Object.setPrototypeOf(syntheticEvent, {
persist
});
}
function getSyntheticEvent(nativeEvent) {
// 第一次才会创建,以后不再创建,始终会用同一个
if (!syntheticEvent) {
persist();
}
syntheticEvent.nativeEvent = nativeEvent;
syntheticEvent.currentTarget = nativeEvent.target;
// 把原生事件上的方法和属性都拷贝到了合成对象上
for (let key in nativeEvent) {
if (typeof nativeEvent[key] == 'function') {
syntheticEvent[key] = nativeEvent[key].bind(nativeEvent);
} else {
syntheticEvent[key] = nativeEvent[key];
}
}
return syntheticEvent;
}

unstable_batchedUpdates 批量强制更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 情况一
setTimeout(()=>{
this.setState({number: this.state.number+1});
console.log(this.state.number); // 1
this.setState({number: this.state.number+1});
console.log(this.state.number); // 2
})

// 情况二 批量强制更新
import ReactDOM, {unstable_batchedUpdates} from 'react-dom';
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({number: this.state.number+1});
console.log(this.state.number); // 0
this.setState({number: this.state.number+1});
console.log(this.state.number); // 0
})

})

原理

1
2
3
4
5
6
7
import { updateQueue } from './component';
function unstable_batchedUpdates(fn){
updateQueue.isPending = true; // 强行处理批量更新模式
fn();
updateQueue.isPending = false;
updateQueue.batchUpdate();
}

补充一下updateQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export let updateQueue = {
updaters: [], // 这里面放着将要执行的更新器对象
isPending: false, // 是否批量更新,如果 isPending=true 则处于批量更新模式
add(updater) { // 放进去就完事,不进行真正的更新
this.updaters.push(updater);
},
// 需要有人调用batchUpdate方法才会真正更新
batchUpdate() {// 强行全部更新 执行真正的更新
if (this.isPending) {
return;
}
this.isPending = true; // 进入批量更新模式
let { updaters } = this;
let updater;
while ((updater = updaters.pop())) {
updater.updateComponent(); // 更新所有脏 dirty 组件
}
this.isPending = false; // 改为非批量更新
},
};

DOM-DIFF

  • DOM 节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,它们可以通过唯一key进行区分
  • DIFF算法在执行时有三个维度,分别是Tree DIFF、Component DIFF和Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为DIFF粒度不同、执行先后顺序不同

Tree DIFF

  • Tree DIFF是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁

  • 当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建

Component DIFF

  • 如果是同一类型的组件,按照原策略继续比较
  • 类型不同则直接替换

原理分析

老节点 A B C D
新节点 A C B E F

(1) 针对老节点做一个map映射,属性是老节点的key,值是老节点(即React元素节点)(A:A元素,B:B元素,C:C元素,D:D元素)
(2) lastIndex 指向老节点中可复用,上一个不需要移动的元素的索引
(3) _mountIndex 挂载索引,表示这个节点在兄弟节点中的位置 (0 1 2 3)

1、开始遍历新节点的数组
1.1、在map里找有没有可复用的老节点,通过key去查找,如果新老两个元素key一样,type类型也是一样的,那就可以复用

【比较A节点】
通过key去查找老节点,key、type都一样,找到A
可复用的A节点 _mountIndex < lastIndex 则需要移动,否则不用移动可复用,现在对于A节点来说,_mountIndex 为0,lastIndex 为0,所以DOM节点A不用移动,复用老的DOM节点,用新属性更新这个DOM节点


如图,此时lastIndex还是指向0,_mountIndex 指向新的A节点的下标 0

【比较C节点】
继续,通过key去查找老节点,key、type都一样,找到C
可复用的老节点的_mountIndex:2, lastIndex:0 不移动,可复用,复用老的DOM节点,用新属性更新这个DOM节点,下标改为1
并且lastIndex = Math.max(lastIndex,老节点的挂载点_mountIndex 即2),这里lastIndex为2
每次比较都会取最大值给lastIndex,并且更新挂载点的索引

【比较B节点】
继续,通过key去查找老节点,key、type都一样,找到B
老的B节点的_mountIndex: 1,现在lastIndex: 2, _mountIndex < lastIndex ,移动节点,一定到新节点B的同下标位置 2

【比较节点E】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 3

【比较节点F】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 4

新数组遍历完了,将老节点中没用到的节点删除

【小结】
1、把老节点放在map里,属性为key,值为虚拟DOM
2、遍历新子元素数组,先去map里找有没有能复用的,如果找到了就更新DOM属性,并且判断_mountIndex是否小于lastIndex,如果小于则需要移动,从老节点原来的位置删除,并插入新数组的当前位置,然后把老节点的挂载点_mountIndex等于此节点在新数组中的索引,另外更新lastIndex为lastIndex和老挂载点的较大值
3、如果找不到可以复用的老节点,则直接在当前索引位置插入新节点
4、新数组遍历完成后,如果还有没有用到的老节点,删除。

深度优先遍历,如果节点下还有子节点,会先去遍历子节点
这里的类型比较是type,$$type大类型,可能都是element,而type则是比较的是div、span这种

【示例】
如果 A 下面有节点 B C,改成 A C B,会先去更新B C 下面的子节点,再整体移动 B 放到 C 的后边



部分代码实现

更新子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let diffQueue = [];// 这是一个补丁包,记录了哪些节点需要删除,哪些节点需要添加
function updateChildren(dom, oldChildrenElements, newChildrenElements){
updateDepth++; // 每进入一个新的子层级,就让updateDepth++
diff(dom, oldChildrenElements, newChildrenElements, diffQueue);
updateDepth--; // 每比较完一层,返回上一级的时候,就updateDepth--
if(updateDepth == 0){ // updateDepth = 0,就说明到最上面一层了,整个更新对比就完事了
path(diffQueue); // 把收集到的差异补丁传给path方法进行更新
diffQueue.length = 0;
}
}
// 这里diffQueue的结构
diffQueue.push({
parentNode: parentNode, // 我要移动哪个父节点下的元素
type: 'MOVE', // INSERT REMOVE
fromIndex: oldChildrenElement._mountIndex, // 从哪里移动
toIndex: i,// 移动到哪里
})

开始打包,这里操作的是真实的DOM元素

  • 第一步,把该删除的删除 MOVE REMOVE 移动的和删除的
  • 第二步,插入移动的

这里指的是移动的B 和 删除的 D,其中要把移动的B放在一个Map里面缓存起来后面复用

先更新父节点的属性,再更新子节点的属性
先移动子节点,再移动父节点

相关文章:
http://www.zhufengpeixun.cn/2020/html/96.1.react16.html

生命周期

context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createContext(defaultValue){
Provider.value = defaultValue;
function Provider(props){
Provider.value = props.value;
return props.children;
}
function Consumer(props){
// return props.children(Provider.value);
return onlyOne(props.children)(Provider.value);
}
return {Provider, Consumer};
}

const React = {
createElement,
Component,
createRef,
createContext
}
export React;
1
2
3
export function onlyOne(obj){
return Array.isArray(obj) ? obj[0] : obj;
}