react原理解析
react分为render阶段和commit阶段
render阶段
render阶段的主要工作是构建Fiber树和生成effectList
performSyncWorkOnRoot或者performConcurrentWorkOnRoot是render阶段的开始标志,内部调用performUnitOfWork函数
performUnitOfWork进行深度优先遍历将已创建的fiber节点连接,构建workInProgress树。
render阶段又分为两个阶段,beginWork(捕获阶段)和completeWork(冒泡阶段)。
beginwork 捕获阶段
从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork函数,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。beginwork函数主要的工作是创建或复用子fiber节点。
diff算法
diff算法是发生在render阶段的beginwork阶段调用的reconcileChildFibers函数中,通过对比current Fiber和jsx对象构建workInProgress Fiber。 该函数根据newChild的类型来进入单节点的diff或者多节点diff。
react为了降低复杂度,提出了三个前提:
- 只对同级比较,跨层级的dom不会进行复用
- 不同类型节点生成的dom树不同,此时会直接销毁老节点及子孙节点,并新建节点
- 可以通过key来对元素diff的过程提供复用的线索
单节点diff
单点diff有如下几种情况:
- key和type相同表示可以复用节点
- key不同直接标记删除节点,然后新建节点
- key相同type不同,标记删除该节点和兄弟节点,然后新创建节点
completework 冒泡阶段
在捕获阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork函数,当全部兄弟节点执行完之后,会向上‘冒泡’到父节点执行completeWork,直到rootFiber,最后调用commitRoot函数进入commit阶段。
completework函数主要工作是处理fiber的props、创建dom、创建effectList,并将beginwork阶段被打上effectTag的节点存入effectList中,方便commit阶段的遍历执行(以空间换时间),这一过程发生completeUnitOfWork函数中,其作用就是向上合并effectList。
commit阶段
调用commitRoot(root)进入commit阶段,这里的root指的就是fiberRoot,然后会遍历render阶段生成的effectList,effectList上的Fiber节点保存着对应的props变化。之后会遍历effectList进行对应的dom操作和生命周期、hooks回调或销毁函数。
commit阶段分为三个阶段,分别是pre-commit,mutation阶段,mutation后
pre-commit
- 调用flushPassiveEffects执行完所有effect的任务
- 初始化相关变量
- 赋值firstEffect给后面遍历effectList用
mutation阶段
遍历effectList分别执行三个方法commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects执行对应的dom操作和生命周期
- commitBeforeMutationEffects该函数主要做了如下两件事
-
执行getSnapshotBeforeUpdate
因为commit阶段是同步的,所以getSnapshotBeforeUpdate也同步执行
-
调度useEffect
componentDidUpdate或componentDidMount会在commit阶段同步执行,而useEffect会在commit阶段异步调度,所以适用于数据请求等副作用的处理。
- commitMutationEffects
- 调用commitDetachRef解绑ref
- 根据effectTag执行对应的dom操作
- useLayoutEffect销毁函数在UpdateTag时执行
-
commitLayoutEffects
在commitMutationEffects之后所有的dom操作都已经完成,可以访问dom了,commitLayoutEffects主要做了以下两件事
-
调用commitLayoutEffectOnFiber执行相关生命周期函数或者hook相关callback
在源码中commitLayoutEffectOnFiber函数的别名是commitLifeCycles,在简化后的代码中可以看到,commitLifeCycles会判断fiber的类型,SimpleMemoComponent会执行useLayoutEffect的回调,然后调度useEffect,ClassComponent会执行componentDidMount或者componentDidUpdate,this.setState第二个参数也会执行,HostRoot会执行ReactDOM.render函数的第三个参数。
在flushPassiveEffects函数中调用flushPassiveEffectsImpl遍历pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount,执行对应的effect回调和销毁函数,而这两个数组是在commitLayoutEffects函数中赋值的,mutation后effectList赋值给rootWithPendingPassiveEffects,然后scheduleCallback调度执行flushPassiveEffects。
所以useEffect是异步执行,而useLayoutEffect是同步执行。
-
执行commitAttachRef为ref赋值
commitAttacRef中会判断ref的类型,执行ref或者给ref.current赋值
mutation后
- 根据rootDoesHavePassiveEffects赋值相关变量
- 执行flushSyncCallbackQueue处理componentDidMount等生命周期或者useLayoutEffect等同步任务
各阶段生命周期执行情况
- render阶段:
- mount时:constructor、getDerivedStateFromProps、render
- update时:getDerivedStateFromProps、shouldComponentUpdate、render
- error时:调用getDerivedStateFromError
- commit阶段
- mount时:componnetDidMount
- update时:调用getSnapshotBeforeUpdate、componnetDidUpdate
- unMount时:调用componnetWillUnmount
- error时:调用componnetDidCatch
在react中触发状态更新的几种方式:
- ReactDOM.render
- this.setState
- this.forceUpdate
- useState
- useReducer
hook
hook存在于Dispatcher中,Dispatcher就是一个对象,不同hook 调用的函数不一样,全局变量ReactCurrentDispatcher.current会根据是mount还是update赋值为HooksDispatcherOnMount或HooksDispatcherOnUpdate
hook数据结构
在FunctionComponent中,多个hook会形成hook链表,保存在Fiber的memoizedState的上,而需要更新的Update保存在hook.queue.pending中
(待补充)
context
- 在render阶段调用updateContextProvider的时候会执行pushProvider,将新的值push进valueStack中
- 在commit阶段调用completeWork的时候会执行popProvider,将栈顶context pop出来,
为什么会有这样一个机制呢,因为我们的context是跨层级的,在之前讲到render阶段和commit阶段的时候,我们会以深度优先遍历的方式遍历节点,如果涉及跨层级读取状态就有点力不从心了,就需要一层一层往下传递我们的props,所以我们可以用一个stack记录我们的context,在render阶段pushProvider,在commit阶段popProvider,在每个具体的层级能根据valueCursor取当前value。