React的基本认知
React是一个函数
构建页面应用
function App() {
return (
<div>
App
</div>
);
}
调用<App/>, 返回一个嵌套对象Object(Fiber节点),对象的构成如下所示:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
// ... ... 省略其他属性 ... ... //
}
通过sibling、child构建树结构,也就是currentTree
ReactDOM将对象节点,渲染到页面上
ReactDOM.render(<App />, 'root');
如何渲染的呢?具体React中通过函数[legacyRenderSubtreeIntoContainer](),将树结构渲染到页面上。
如何渲染的,这个过程是可以猜测的,无非就是对对象进行遍历解析,然后进行dom更新操作,所以没必要进行深究。如果仅仅到这里,React还不能算是一个完整的框架,因为这只是渲染一个静态的html,没有动态的交互,那么React是如何处理更新操作的?
React通过this.setState/hooks中setState进行更新操作。
所以到这里,我们可以看一下React的整个机制。
在动态更新中,无非对html需要做的就是增加、更新、删除等操作,所以最重要的是途中标红的区域做了什么?
React Fiber的更新逻辑
首先说明下,为什么会有两个Tree:current和workInProcessTree,current就是当前渲染在界面上的FiberTree,workInProcessTree是接下来进行渲染的tree,当更新结束,workInprocessTree就变成currentTree。从程序的角度来讲,当你更新一个变量,就需要存储一个中间变量,然后优化下计算过程,最后再更新,workInProcressTree就是这个中间变量。
我们看一下Fiber内部是如何进行更新的。先直接上图,然后再慢慢解释。
- step1: 更新进入一个while循环,执行当前更新单元
- step2: 更新单元,执行更新beginWork,判断当前节点的类型,执行不同的更新:
1)如果是类组件,先从getStateFromUpdate中获得最新的state,然后执行类组件的render函数,返回当前节点;
// getStateFromUpdate
// prevState就是workInProgress中的state
// 这就是为什么setState中是一个函数,会返回最新的,因为取的tree是workInprocessTree中的state,而workInProcessTree是最新的state
if (typeof payload === 'function') {
if (__DEV__) {
enterDisallowedContextReadInDEV();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
) {
payload.call(instance, prevState, nextProps);
}
}
const nextState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
exitDisallowedContextReadInDEV();
}
return nextState;
}
2)如果是函数组件,就执行对应的函数,如果遇到hooks就执行updateHooks
3)... ...
执行结束后,返回当前最新节点,newChild
- step3: 对节点,进行打标签,决定是更新还是删除,还是新增,这些例子通过effectList链表的形式存在(链表的形式存在)
- step4: 完成工作,返回下一个节点。
到这里,reactFiber的更新逻辑基本上讲清楚了,但是还存在几个小地方没讲清楚,也是我们经常面临的几个问题。
1、react的生命周期是如何执行的?
- a、class组件的生命周期是写在class的原型上的
- b、要执行生命周期,需要在不同阶段执行实例(也就是Fiber节点)的原型方法
- c、在初始生成的时候,执行componentDidMount
- d、在更新的时候,执行shouldComponentUpdate、componentDidUpdate等
- e、在删除节点的时候,执行componentWillUnMount
- f、还有其他生命周期,这里就不做赘述了
但是大家通常还有一个疑问,嵌套组件的生命周期如何执行?比如下面的两个组件
// Component
class Son extends React.Component {
componentWillUnMount() {
console.log('son unmount');
}
componentDidMount() {
console.log('son mount');
}
componentDidUpdate() {
console.log('son update);
}
}
class Father extends React.Component {
componentWillUnMount() {
console.log('father unmount');
}
componentDidMount() {
console.log('father mount');
}
componentDidUpdate() {
console.log('father update);
}
}
// App
function App() {
return (
<Fahter>
<Son/>
</Father>
);
}
输出结果
son mount
father mount
// 如果父组件更新
son update
father update
// 如果组件卸载
father unmount
son unmount
为什么update和mount,子组件优先于父组件?而unmount父组件优先于子组件?
原理解释:
1、节点的遍历是深度优先遍历,可以通过performUnitOfWorkcompleteWork看到,首先是子节点->兄弟节点->父节点
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
workInProgress = unitOfWork;
do {
// 执行React内部逻辑判断,do some work
// 完成后,如果有兄弟节点,返回兄弟节点
const siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {
return siblingFiber;
}
// 没有兄弟节点,返回父节点
workInProgress = returnFiber;
} while (workInProgress !== null);
// 如果到根节点了
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
2、整个过程会构建EffectList(副作用)列表,构建的列表如下所示:
a) Fiber节点中有三个节点:
fiberNode {
firstEffect,
nextEffect,
lasteffect
}
通过相互关系,构建了如图所示的链表结构。
nextEffect是从第一个更新的子节点,不停回溯到最后一个子节点,而lastEffect是逆向从第一个删除的节点->最后一个删除的节点。
b)commit阶段,会经历两个阶段:
- 阶段一:执行更新的生命周期,然后执行删除的生命周期,你会看到unmount一定在更新之后执行,整个过程是这样的,通过nextEffect寻找更新的节点,到达最后一个节点后,通过lastEffect.next执行删除的操作
- 阶段二:执行完对应的生命周期后,操作DOM结构,增、删、更新。
- 阶段三:执行componentDidMount/componentDidUpdate/useEffect/的回调函数
2、多次执行一个更新,内部机制是怎么运行的
比如:在class组件中执行两次setState
state = {
count: 0
};
// click中执行如下代码
this.setState({
count: this.state.count + 1
}, () => {
console.log('first update');
});
this.setState({
count: this.state.count + 1
}, () => {
console.log('second update');
});
最终结果:
count: 1
原理解释:
1、在click中的函数执行是batchUpdate的,所以执行的时候,拿到的this,还是前一个节点的current,所以当时的count是0,无论多少次都是一样的。并不是之前的合并的概念,是每次都会执行,当具有对应回调的时候,会执行两次回调。
2、但是,如果是
this.setState(c => c + 1);
这样的,取得就是workInprogressTree,获取的是最新的
3、最后都更新后,执行commit
3、异步代码中执行多个更新?
state = {
count: 0
};
// 在异步代码中执行
setTimeout(() => {
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
}, 1000);
输出结果:
count: 2
原理解释:
1、异步代码执行的时候,不会批量执行,在每次执行的时候,都会执行commit
2、整体流程就是,第一次update -> commit -> 第二次update -> commit,所以每次更新拿到的this.state都是最新的。
4、如果调用ReactDOM.unstable_batchedUpdates,执行是怎么样的?
state = {
count: 0
};
// 在异步代码中执行
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
})
}, 1000);
输出结果:
count: 1
原理解释:
1、批量更新,流程同click事件的处理
5、hook的内部执行逻辑是怎么样的?
原理解释:
hooks其实是个状态机,触发React更新,然后执行函数,内部再拿到最新的状态
可以通过下面函数进行模拟一个useState函数
function useState(initialState) {
function* dispatchState() {
let state = initialState;
while(true) {
state = yield state;
}
}
const dispatch = dispatchState();
const { value, done } = dispatch.next();
const setState = (newState) => {
dispatch.next(newState);
};
return [value, setState];
}
但是useState被多次调用,会存在一个问题:
- 在同一个函数中多次调用,不同调用方,useState怎么管理怎么管理?
既然有多个,我们可以开辟一个数组,然后把状态存储起来:
function useState(initialState) {
function* dispatchState() {
let pointer = 0;
const stateArr = [];
stateArr[pointer] = initialState;
while(true) {
pointer++;
state = yield stateArr[pointer - 1];
}
}
const dispatch = dispatchState();
const { value, done } = dispatch.next();
const setState = (newState) => {
dispatch.next(newState);
};
return [value, setState];
}
上面就可以解决,多次调用useState,状态存储的问题,虽然还存在二次更新的问题没有解决,也算是基本模拟了。在React官方文档中,hooks是不能被放在条件判断中的,必须放在函数组件的顶层作用域。
当然React官方不是通过数组来存储对应的状态,而是通过链表的形式。
假如,有下面的Function Component节点:
function useMyHook(initial) {
const [my, setMy] = useState(initial);
const [self, setSelf] = useState('self');
const name = useMemo(() => 'name', []);
return my + self + name;
}
function hookChild1() {
const [tag, setTag] = useState('hook');
const [name, setName] = useState('child1');
const myHook = useMyHook('my');
const hook = (
<div>
{`${tag} ${name} ${myHook}`}
</div>
);
console.log('hook child1', hook);
return hook;
}
hooks的链表是如何存储的呢,如下图所示?
可以看到,React hooks是通过链表的形式链接在一起,当每次初始化或者更新hooks的时候,
const [state, setState] = useState('state');
都会从链表中,获得对应的值。
通常来讲,链表的数据结构,会存在查找对应节点值的效率问题,但是Hooks不存在,hooks定义在顶层作用域,每次一定会执行一遍,那自然而然,React hooks去更新对应的值的时候,会更新链表节点的方式,读者你觉得会是怎么样呢?
注意
千万不要试图去记住这些函数,要理解整个流程框架,因为函数名字会经常变化的,但是机制一般是不会变化的。
React的事件机制
文中没有对React的事件机制进行一个说明,主要是觉得和本文相关,但是后面不太想写一个事件的主题,所以就简略的写在下面了。
- React事件是通过委托的形式实现的(依赖于浏览器原生的冒泡事件)
- React在初始化开始的时候,进行事件注册
- 当发生事件的时候,冒泡到document,通过一个分配器,查找注册的方法,形成处理事件的一个数组
- 批处理数组中的事件,批处理事件中的方法
说一个我实际编程中遇到的坑吧:React的事件是所有的都可以冒泡,包括原生不能冒泡的blur事件。
参考文档中有篇还不错的关于事件的文章,大家可以看下。以后如果遇到问题了再去查看具体的内容。和大家说一句,一定要有目的,带着问题去读源码。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。