前言
明天咱们来一期不同寻常的React进阶文章,本文咱们通过一些不同寻常的景象,以探案的流程剖析起因,找到后果,从而意识React,走进React的世界,揭开React的面纱,我坚信,更深的了解,方可更好的应用。
我抵赖起这个名字可能有点题目党了,灵感来源于小时候央视有一个叫做《走进迷信》的栏目,天天介绍各种超自然的灵异景象,搞的神乎其神,最初揭秘的时候原来是各种小儿科的问题,当初想想都感觉搞笑😂😂。然而我明天介绍的这些React ‘灵异’景象实质可不是小儿科,每一个景象后都走漏出 React 运行机制和设计原理。(咱们讲的react版本是16.13.1
)
好的,废话不多说,我的大侦探们,are you ready ? 让咱们开启明天的揭秘之旅把。
案件一:组件莫名其妙反复挂载
接到报案
之前的一位同学遇到一个诡异状况,他心愿在组件更新,componentDidUpdate
执行后做一些想要做的事,组件更新源来源于父组件传递 props
的扭转。然而父组件扭转 props
发现视图渲染,然而componentDidUpdate
没有执行,更怪异的是componentDidMount
执行。代码如下:
// TODO: 反复挂载
class Index extends React.Component{
componentDidMount(){
console.log('组件初始化挂载')
}
componentDidUpdate(){
console.log('组件更新')
/* 想要做一些事件 */
}
render(){
return <div>《React进阶实际指南》 👍 { this.props.number } + </div>
}
}
成果如下
componentDidUpdate
没有执行,componentDidMount
执行,阐明组件基本没有走更新逻辑,而是走了反复挂载。
逐个排查
子组件一头雾水,基本不找起因,咱们只好从父组件动手。让咱们看一下父组件如何写的。
const BoxStyle = ({ children })=><div className='card' >{ children }</div>
export default function Home(){
const [ number , setNumber ] = useState(0)
const NewIndex = () => <BoxStyle><Index number={number} /></BoxStyle>
return <div>
<NewIndex />
<button onClick={ ()=>setNumber(number+1) } >点赞</button>
</div>
}
从父组件中找到了一些端倪。在父组件中,首先通过BoxStyle
做为一个容器组件,增加款式,渲染咱们的子组件Index
,然而每一次通过组合容器组件造成一个新的组件NewIndex
,真正挂载的是NewIndex
,水落石出。
注意事项
造成这种状况的实质,是每一次 render
过程中,都造成一个新组件,对于新组件,React 解决逻辑是间接卸载老组件,从新挂载新组件,所以咱们开发的过程中,留神一个问题那就是:
- 对于函数组件,不要在其函数执行上下文中申明新组件并渲染,这样每次函数更新会促使组件反复挂载。
- 对于类组件,不要在
render
函数中,做如上同样的操作,否则也会使子组件反复挂载。
案件二:事件源e.target离奇失踪
突发案件
化名(小明)在一个月黑风高的夜晚,突发奇想写一个受控组件。写的什么内容具体如下:
export default class EventDemo extends React.Component{
constructor(props){
super(props)
this.state={
value:''
}
}
handerChange(e){
setTimeout(()=>{
this.setState({
value:e.target.value
})
},0)
}
render(){
return <div>
<input placeholder="请输出用户名?" onChange={ this.handerChange.bind(this) } />
</div>
}
}
input
的值受到 state
中value
属性管制,小明想要通过handerChange
扭转value
值,然而他冀望在setTimeout
中实现更新。能够当他想要扭转input值时候,意想不到的事件产生了。
控制台报错如上所示。Cannot read property 'value' of null
也就是说明e.target
为null
。事件源 target
怎么说没就没呢?
线索追踪
接到这个案件之后,咱们首先排查问题,那么咱们先在handerChange
间接打印e.target
,如下:
看来首先排查不是 handerChange
的起因,而后咱们接着在setTimeout
中打印发现:
果然是setTimeout
的起因,为什么setTimeout
中的事件源 e.target 就莫名的失踪了呢? 首先,事件源必定不是莫名的失踪了,必定 React 底层对事件源做了一些额定的解决,首先咱们晓得React采纳的是事件合成机制,也就是绑定的 onChange
不是实在绑定的 change
事件,小明绑定的 handerChange
也不是真正的事件处理函数。那么也就是说React底层帮咱们解决了事件源。这所有可能只有咱们从 React 源码中找到线索。通过对源码的排查,我发现有一处线索非常可疑。
react-dom/src/events/DOMLegacyEventPluginSystem.js
function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
batchedEventUpdates(handleTopLevel, bookKeeping);
}
dispatchEventForLegacyPluginEventSystem
是legacy
模式下,所有事件都必然通过的次要函数,batchedEventUpdates
是解决批量更新的逻辑,外面会执行咱们真正的事件处理函数,咱们在事件原理篇章讲过 nativeEvent
就是真正原生的事件对象 event
。targetInst
就是e.target
对应的fiber
对象。咱们在handerChange
外面获取的事件源是React合成的事件源,那么理解事件源是什么时候,怎么样被合成的? 这对于破案可能会有帮忙。
事件原理篇咱们将介绍React采纳事件插件机制,比方咱们的onClick事件对应的是 SimpleEventPlugin
,那么小明写onChange
也有专门 ChangeEventPlugin
事件插件,这些插件有一个至关重要的作用就是用来合成咱们事件源对象e,所以咱们来看一下ChangeEventPlugin
。
react-dom/src/events/ChangeEventPlugin.js
const ChangeEventPlugin ={
eventTypes: eventTypes,
extractEvents:function(){
const event = SyntheticEvent.getPooled(
eventTypes.change,
inst, // 组件实例
nativeEvent, // 原生的事件源 e
target, // 原生的e.target
);
accumulateTwoPhaseListeners(event); // 这个函数依照冒泡捕捉逻辑解决真正的事件函数,也就是 handerChange 事件
return event; //
}
}
咱们看到合成事件的事件源handerChange
中的 e,就是SyntheticEvent.getPooled
创立进去的。那么这个是破案的关键所在。
legacy-events/SyntheticEvent.js
SyntheticEvent.getPooled = function(){
const EventConstructor = this; // SyntheticEvent
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(instance,dispatchConfig,targetInst,nativeEvent,nativeInst,);
return instance;
}
return new EventConstructor(dispatchConfig,targetInst,nativeEvent,nativeInst,);
}
番外:在事件零碎篇章,文章的事件池感怀,讲的比拟仓促,抽象,这篇这个局部将具体补充事件池感怀。<br/>
getPooled
引出了事件池的真正的概念,它次要做了两件事:
- 判断事件池中有没有空余的事件源,如果有取出事件源复用。
- 如果没有,通过
new SyntheticEvent
的形式创立一个新的事件源对象。那么SyntheticEvent
就是创立事件源对象的构造函数,咱们一起钻研一下。
const EventInterface = {
type: null,
target: null,
currentTarget: function() {
return null;
},
eventPhase: null,
...
};
function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst; // 组件对应fiber。
this.nativeEvent = nativeEvent; // 原生事件源。
this._dispatchListeners = null; // 寄存所有的事件监听器函数。
for (const propName in Interface) {
if (propName === 'target') {
this.target = nativeEventTarget; // 咱们真正打印的 target 是在这里
} else {
this[propName] = nativeEvent[propName];
}
}
}
SyntheticEvent.prototype.preventDefault = function (){ /* .... */ } /* 组件浏览器默认行为 */
SyntheticEvent.prototype.stopPropagation = function () { /* .... */ } /* 阻止事件冒泡 */
SyntheticEvent.prototype.destructor = function (){ /* 状况事件源对象*/
for (const propName in Interface) {
this[propName] = null
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
}
const EVENT_POOL_SIZE = 10; /* 最大事件池数量 */
SyntheticEvent.eventPool = [] /* 绑定事件池 */
SyntheticEvent.release=function (){ /* 清空事件源对象,如果没有超过事件池下限,那么放回事件池 */
const EventConstructor = this;
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
我把这一段代码精炼之后,假相也就慢慢浮出水面了,咱们先来看看 SyntheticEvent
做了什么:
- 首先赋予一些初始化的变量
nativeEvent
等。而后依照EventInterface
规定把原生的事件源上的属性,复制一份给React 事件源。而后一个重要的就是咱们打印的e.target就是this.target,在事件源初始化的时候绑定了真正的e.target->nativeEventTarget
- 而后React事件源,绑定了本人的阻止默认行为
preventDefault
,阻止冒泡stopPropagation
等办法。然而这里有一个重点办法就destructor
,这个函数置空了React本人的事件源对象。那么咱们终于找到了答案,咱们的事件源e.target隐没大概率就是因为这个destructor
,destructor
在release
中被触发,而后将事件源放进事件池,期待下一次复用。
当初所有的锋芒都指向了release
,那么release
是什么时候触发的呢?
legacy-events/SyntheticEvent.js
function executeDispatchesAndRelease(){
event.constructor.release(event);
}
当 React 事件零碎执行完所有的 _dispatchListeners
,就会触发这个办法 executeDispatchesAndRelease
开释以后的事件源。
水落石出
回到小明遇到的这个问题,咱们下面讲到,React最初会同步的置空事件源,而后放入事件池,因为setTimeout
是异步执行,执行时候事件源对象曾经被重置并开释会事件池,所以咱们打印 e.target = null
,到此为止,案件水落石出。
通过这个案件咱们明确了 React 事件池的一些概念:
- React 事件零碎有独特合成事件,也有本人的事件源,而且还有对一些非凡状况的解决逻辑,比方冒泡逻辑等。
- React 为了避免每次事件都创立事件源对象,节约性能,所以引入了事件池概念,每一次用户事件都会从事件池中取出一个e,如果没有,就创立一个,而后赋值事件源,等到事件执行之后,重置事件源,放回事件池,借此做到复用。
用一幅流程图示意:
案件三:虚实React
案发现场
这个是产生在笔者身上的事儿,之前在开发 React 我的项目时候,为了逻辑复用,我把一些封装好的自定义 Hooks 上传到公司公有的 package 治理平台上,在开发另外一个 React 我的项目的时候,把公司的包下载下来,在组件外部用起来。代码如下:
function Index({classes, onSubmit, isUpgrade}) {
/* useFormQueryChange 是笔者写好的自定义hooks,并上传到公有库,次要是用于对表单控件的对立治理 */
const {setFormItem, reset, formData} = useFormQueryChange()
React.useEffect(() => {
if (isUpgrade) reset()
}, [ isUpgrade ])
return <form
className={classes.bootstrapRoot}
autoComplete='off'
>
<div className='btnbox' >
{ /* 这里是业务逻辑,曾经省略 */ }
</div>
</form>
}
useFormQueryChange
是笔者写好的自定义 hooks
,并上传到公有库,次要是用于对表单控件的对立治理,没想到引入就间接爆红了。谬误内容如下:
逐个排查
咱们依照 React 报错的内容,逐个排查问题所在:
- 第一个可能报错起因
You might have mismatching versions of React and the renderer (such as React DOM)
,意思是React
和React Dom
版本不统一,造成这种状况,然而咱们我的项目中的React
和React Dom
都是v16.13.1
,所以排除这个的嫌疑。 - 第二个可能报错起因
You might be breaking the Rules of Hooks
意思是你突破了Hooks 规定,这种状况也是不可能的,因为笔者代码里没有毁坏hoos
规定的行为。所以也排除嫌疑。 - 第三个可能报错起因
You might have more than one copy of React in the same app
意思是在同一个利用外面,可能有多个 React。目前来看所有的嫌疑都指向第三个,首先咱们援用的自定义 hooks,会不会外部又存在一个React 呢?
依照下面的提醒我排查到自定义hooks对应的node_modules
中果然存在另外一个React,是这个假React
(咱们权且称之为假React)搞的鬼。咱们在Hooks原理 文章中讲过,React Hooks
用ReactCurrentDispatcher.current
在组件初始化,组件更新阶段赋予不同的hooks对象,更新结束后赋予ContextOnlyDispatcher
,如果调用这个对象上面的hooks,就会报如上谬误,那么阐明了这个谬误是因为咱们这个我的项目,执行上下文引入的React是我的项目自身的React,然而自定义Hooks援用的是假React Hooks中的ContextOnlyDispatcher
接下来我看到组件库中的package.json
中,
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
原来是React作为 dependencies
所以在下载自定义Hooks
的时候,把React
又下载了一遍。那么如何解决这个问题呢。对于封装React组件库,hooks库,不能用 dependencies
,因为它会以以后的dependencies
为依赖下载到自定义hooks库上面的node_modules
中。取而代之的应该用peerDependencies
,应用peerDependencies
,自定义hooks
再找相干依赖就会去咱们的我的项目的node_modules
中找,就能基本上解决这个问题。
所以咱们这么改
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8",
},
就完满的解决了这个问题。
拨开迷雾
这个问题让咱们明确了如下:
- 对于一些hooks库,组件库,自身的依赖,曾经在我的项目中存在了,所以用
peerDependencies
申明。 - 在开发的过程中,很可能用到不同版本的同一依赖,比如说我的项目引入了 A 版本的依赖,组件库引入了 B 版本的依赖。那么这种状况如何解决呢。在
package.json
文档中提供了一个resolutions配置项能够解决这个问题,在resolutions
中锁定同一的引入版本,这样就不会造成如上存在多个版本的我的项目依赖而引发的问题。
我的项目package.json
这么写
{
"resolutions": {
"react": "16.13.1",
"react-dom": "16.13.1"
},
}
这样无论我的项目中的依赖,还是其余库中依赖,都会应用对立的版本,从根本上解决了多个版本的问题。
案件四:PureComponet/memo性能生效问题
案情形容
在 React 开发的时候,但咱们想要用 PureComponent
做性能优化,调节组件渲染,然而写了一段代码之后,发现 PureComponent
性能居然生效了,具体代码如下:
class Index extends React.PureComponent{
render(){
console.log('组件渲染')
const { name , type } = this.props
return <div>
hello , my name is { name }
let us learn { type }
</div>
}
}
export default function Home (){
const [ number , setNumber ] = React.useState(0)
const [ type , setType ] = React.useState('react')
const changeName = (name) => {
setType(name)
}
return <div>
<span>{ number }</span><br/>
<button onClick={ ()=> setNumber(number + 1) } >change number</button>
<Index type={type} changeType={ changeName } name="alien" />
</div>
}
咱们原本冀望:
- 对于 Index 组件,只有
props
中name
和type
扭转,才促使组件渲染。然而理论状况却是这样:
点击按钮成果:
上不着天;下不着地
为什么会呈现这种状况呢? 咱们再排查一下Index
组件,发现 Index
组件上有一个 changeType
,那么是不是这个的起因呢? 咱们来剖析一下,首先状态更新是在父组件 Home
上,Home
组件更新每次会产生一个新的changeName
,所以Index
的PureComponent
每次会浅比拟,发现props
中的changeName
每次都不相等,所以就更新了,给咱们直观的感觉是生效了。
那么如何解决这个问题,React hooks
中提供了 useCallback
,能够对props
传入的回调函数进行缓存,咱们来改一下Home
代码。
const changeName = React.useCallback((name) => {
setType(name)
},[])
成果:
这样就基本解决了问题,用 useCallback
对changeName
函数进行缓存,在每一次 Home
组件执行,只有useCallback
中deps
没有变,changeName
内存空间还指向原来的函数,这样PureComponent
浅比拟就会发现是雷同changeName
,从而不渲染组件,至此案件已破。
持续深刻
大家用函数组件+类组件开发的时候,如果用到 React.memo React.PureComponent
等api,要留神给这些组件绑定事件的形式,如果是函数组件,那么想要持续保持纯组件的渲染管制的个性的话,那么请用 useCallback
,useMemo
等api解决,如果是类组件,请不要用箭头函数绑定事件,箭头函数同样会造成生效的状况。
上述中提到了一个浅比拟shallowEqual
,接下来咱们重点剖析一下 PureComponent
是如何shallowEqual
,接下来咱们在深入研究一下shallowEqual
的神秘。那么就有从类租价的更新开始。
react-reconciler/src/ReactFiberClassComponent.js
function updateClassInstance(){
const shouldUpdate =
checkHasForceUpdateAfterProcessing() ||
checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
);
return shouldUpdate
}
我这里简化updateClassInstance
,只保留了波及到PureComponent
的局部。updateClassInstance
这个函数次要是用来,执行生命周期,更新state,判断组件是否从新渲染,返回的 shouldUpdate
用来决定以后类组件是否渲染。checkHasForceUpdateAfterProcessing
查看更新起源是否起源与 forceUpdate , 如果是forceUpdate
组件是肯定会更新的,checkShouldComponentUpdate
查看组件是否渲染。咱们接下来看一下这个函数的逻辑。
function checkShouldComponentUpdate(){
/* 这里会执行类组件的生命周期 shouldComponentUpdate */
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
/* 这里判断组件是否是 PureComponent 纯组件,如果是纯组件那么会调用 shallowEqual 浅比拟 */
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
}
checkShouldComponentUpdate
有两个至关重要的作用:
- 第一个就是如果类组件有生命周期
shouldComponentUpdate
,会执行生命周期shouldComponentUpdate
,判断组件是否渲染。 - 如果发现是纯组件
PureComponent
,会浅比拟新老props
和state
是否相等,如果相等,则不更新组件。isPureReactComponent
就是咱们应用PureComponent
的标识,证实是纯组件。
接下来就是重点shallowEqual
,以props
为例子,咱们看一下。
shared/shallowEqual
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) { // is能够 了解成 objA === objB 那么返回相等
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
} // 如果新老props有一个不为对象,或者不存在,那么间接返回false
const keysA = Object.keys(objA); // 老props / 老state key组成的数组
const keysB = Object.keys(objB); // 新props / 新state key组成的数组
if (keysA.length !== keysB.length) { // 阐明props减少或者缩小,那么间接返回不想等
return false;
}
for (let i = 0; i < keysA.length; i++) { // 遍历老的props ,发现新的props没有,或者新老props不同等,那么返回不更新组件。
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true; //默认返回相等
}
shallowEqual
流程是这样的,shallowEqual
返回 true
则证实相等,那么不更新组件;如果返回false
证实不想等,那么更新组件。is
咱们暂且能够了解成 ===
- 第一步,间接通过 === 判断是否相等,如果相等,那么返回
true
。失常状况只有调用React.createElement
会从新创立props
,props
都是不相等的。 - 第二步,如果新老
props
有一个不为对象,或者不存在,那么间接返回false
。 - 第三步,判断新老
props
,key
组成的数组数量等不想等,阐明props
有减少或者缩小,那么间接返回false
。 - 第四步,遍历老的
props
,发现新的props
没有与之对应,或者新老props
不同等,那么返回false
。 - 默认返回
true
。
这就是shallowEqual
逻辑,代码还是非常简单的。感兴趣的同学能够看一看。
案件五: useState更新雷同的State,函数组件执行2次
接到报案
这个问题理论很悬,大家可能平时没有留神到,引起我的留神的是掘金的一个掘友问我的一个问题,问题如下:
首先非常感谢这位仔细的掘友的报案,我在 React-hooks 原理 中讲到过,对于更新组件的办法函数组件 useState
和类组件的setState
有肯定区别,useState
源码中如果遇到两次雷同的state
,会默认阻止组件再更新,然而类组件中setState
如果没有设置 PureComponent
,两次雷同的state
也会更新。
咱们回顾一下 hooks
中是怎么样阻止组件更新的。
react-reconciler/src/ReactFiberHooks.js -> dispatchAction
if (is(eagerState, currentState)) {
return
}
scheduleUpdateOnFiber(fiber, expirationTime); // 调度更新
如果判断上一次的state
-> currentState
,和这一次的state
-> eagerState
相等,那么将间接 return
阻止组件进行scheduleUpdate
调度更新。所以咱们想如果两次 useState
触发同样的state,那么组件只能更新一次才对,然而事实真的是这样吗?。
立案考察
顺着这位掘友提供的线索,咱们开始写 demo
进行验证。
const Index = () => {
const [ number , setNumber ] = useState(0)
console.log('组件渲染',number)
return <div className="page" >
<div className="content" >
<span>{ number }</span><br/>
<button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
<button onClick={ () => setNumber(2) } >将number设置成2</button><br/>
<button onClick={ () => setNumber(3) } >将number设置成3</button>
</div>
</div>
}
export default class Home extends React.Component{
render(){
return <Index />
}
}
如上demo,三个按钮,咱们冀望间断点击每一个按钮,组件都会仅此渲染一次,于是咱们开始试验:
成果:
果然,咱们通过 setNumber
扭转 number
,每次间断点击按钮,组件都会更新2次,依照咱们失常的了解,每次赋予 number
雷同的值,只会渲染一次才对,然而为什么执行了2次呢?
可能刚开始会陷入困境,不晓得怎么破案,然而咱们在想 hooks
原理中讲过,每一个函数组件用对应的函数组件的 fiber
对象去保留 hooks
信息。所以咱们只能从 fiber
找到线索。
顺藤摸瓜
那么如何找到函数组件对应的fiber对象呢,这就顺着函数组件的父级 Home
动手了,因为咱们能够从类组件Home
中找到对应的fiber对象,而后依据 child
指针找到函数组件 Index
对应的 fiber
。说干就干,咱们将上述代码革新成如下的样子:
const Index = ({ consoleFiber }) => {
const [ number , setNumber ] = useState(0)
useEffect(()=>{
console.log(number)
consoleFiber() // 每次fiber更新后,打印 fiber 检测 fiber变动
})
return <div className="page" >
<div className="content" >
<span>{ number }</span><br/>
<button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
</div>
</div>
}
export default class Home extends React.Component{
consoleChildrenFiber(){
console.log(this._reactInternalFiber.child) /* 用来打印函数组件 Index 对应的fiber */
}
render(){
return <Index consoleFiber={ this.consoleChildrenFiber.bind(this) } />
}
}
咱们重点关怀fiber上这几个属性,这对破案很有帮忙
Index fiber
上的memoizedState
属性,react hooks
原理文章中讲过,函数组件用memoizedState
保留所有的hooks
信息。Index fiber
上的alternate
属性Index fiber
上的alternate
属性上的memoizedState
属性。是不是很绕😂,马上会揭晓是什么。Index
组件上的useState
中的number
。
首先咱们讲一下 alternate
指针指的是什么?
说到alternate
就要从fiber
架构设计说起,每个React
元素节点,用两颗fiber树保留状态,一颗树保留以后状态,一个树保留上一次的状态,两棵 fiber
树用 alternate
互相指向。就是咱们耳熟能详的双缓冲。
初始化打印
效果图:
初始化实现第一次render后,咱们看一下fiber树上的这几个状态
第一次打印后果如下,
fiber
上的memoizedState
中baseState = 0
即是初始化useState
的值。fiber
上的alternate
为null
。Index
组件上的number
为 0。
初始化流程:首先对于组件第一次初始化,会和谐渲染造成一个fiber树(咱们简称为树A)。树A的alternate
属性为 null
。
第一次点击 setNumber(1)
咱们第一次点击发现组件渲染了,而后咱们打印后果如下:
- 树A上的
memoizedState
中 **baseState = 0
。 - 树A上的
alternate
指向 另外一个fiber
(咱们这里称之为树B)。 Index
组件上的number
为 1。
接下来咱们打印树B上的 memoizedState
后果咱们发现树B上 memoizedState
上的 baseState = 1
。
得出结论:更新的状态都在树B上,而树A上的 baseState还是之前的0。
咱们大胆猜想一下更新流程:在第一次更新渲染的时候,因为树A中,不存在alternate
,所以间接复制一份树A作为 workInProgress
(咱们这里称之为树B)所有的更新都在以后树B中进行,所以 baseState 会被更新成 1,而后用以后的树B进行渲染。完结后树A和树B通过alternate
互相指向。树B作为下一次操作的current
树。
第二次点击 setNumber(1)
第二次打印,组件同样渲染了,而后咱们打印fiber对象,成果如下:
- fiber对象上的
memoizedState
中baseState
更新成了 1。
而后咱们打印一下 alternate
中 baseState
也更新成了 1。
第二次点击之后 ,树A和树B都更新到最新的 baseState = 1
首先咱们剖析一下流程:当咱们第二次点击时候,树A中的状态没有更新到最新的状态,组件又更新了一次。接下来会以current树(树B)的 alternate
指向的树A作为新的workInProgress
进行更新,此时的树A上的 baseState 终于更新成了 1 ,这就解释了为什么上述两个 baseState 都等于 1。接下来组件渲染实现。树A作为了新的 current 树。
在咱们第二次打印,打印进去的理论是交替后树B,树A和树B就这样交替着作为最新状态用于渲染的workInProgress
树和缓存上一次状态用于下一次渲染的current
树。
第三次点击(三者言其多也)
那么第三次点击组件没有渲染,就很好解释了,第三次点击上一次树B中的 baseState = 1
和 setNumber(1)
相等,也就间接走了return逻辑。
揭开谜底(咱们学到了什么)
- 双缓冲树:React 用
workInProgress
树(内存中构建的树) 和current
(渲染树) 来实现更新逻辑。咱们console.log打印的fiber都是在内存中行将workInProgress
的fiber树。双缓存一个在内存中构建,在下一次渲染的时候,间接用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样能够避免只用一颗树更新状态的失落的状况,又放慢了dom
节点的替换与更新。 - 更新机制:在一次更新中,首先会获取current树的
alternate
作为以后的workInProgress
,渲染结束后,workInProgress
树变为current
树。咱们用如上的树A和树B和曾经保留的baseState模型,来更形象的解释了更新机制。
咱们用一幅流程图来形容整个流程。
此案已破,通过这个容易疏忽的案件,咱们学习了双缓冲和更新机制。
案件六:useEffect批改DOM元素导致怪异闪现
阴差阳错
小明(化名)在动静挂载组件的时候,遇到了灵异的Dom闪现景象,让咱们先来看一下景象。
闪现景象:
代码:
function Index({ offset }){
const card = React.useRef(null)
React.useEffect(()=>{
card.current.style.left = offset
},[])
return <div className='box' >
<div className='card custom' ref={card} >《 React进阶实际指南 》</div>
</div>
}
export default function Home({ offset = '300px' }){
const [ isRender , setRender ] = React.useState(false)
return <div>
{ isRender && <Index offset={offset} /> }
<button onClick={ ()=>setRender(true) } > 挂载</button>
</div>
}
- 在父组件用
isRender
动静加载Index
,点击按钮管制Index
渲染。 - 在
Index
的承受动静的偏移量offset
。并通过操纵用useRef
获取的原生dom
间接扭转偏移量,使得划块滑动。然而呈现了如上图的闪现景象,很不敌对,那么为什么会造成这个问题呢?
深刻理解
初步判断产生这个闪现的问题应该是 useEffect
造成的,为什么这么说呢,因为类组件生命周期 componentDidMount
写同样的逻辑,然而并不会呈现这种景象。那么为什么useEffect
会造成这种状况,咱们只能顺藤摸瓜找到 useEffect
的 callback
执行机会说起。
useEffect
,useLayoutEffect
, componentDidMount
执行机会都是在 commit
阶段执行。咱们晓得 React 有一个 effectList
寄存不同effect
。因为 React
对不同的 effect
执行逻辑和机会不同。咱们看一下useEffect
被定义的时候,定义成了什么样类型的 effect
。
react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps){
return mountEffectImpl(
UpdateEffect | PassiveEffect, // PassiveEffect
HookPassive,
create,
deps,
);
}
这个函数的信息如下:
useEffect
被赋予PassiveEffect
类型的effect
。- 小明改原生dom地位的函数,就是
create
。
那么 create
函数什么时候执行的,React又是怎么解决PassiveEffect
的呢,这是破案的要害。记下来咱们看一 下React 怎么解决PassiveEffect
。
react-reconciler/src/ReactFiberCommitWork.js
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
/* 异步调度 - PassiveEffect */
scheduleCallback(NormalPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
在commitBeforeMutationEffects
函数中,会异步调度 flushPassiveEffects
办法,flushPassiveEffects
办法中,对于React hooks 会执行 commitPassiveHookEffects
,而后会执行 commitHookEffectListMount
。
function commitHookEffectListMount(){
if (lastEffect !== null) {
effect.destroy = create(); /* 执行useEffect中饿 */
}
}
在 commitHookEffectListMount
中,create
函数会被调用。咱们给dom
元素加的地位就会失效。
那么问题来了,异步调度做了些什么呢? React的异步调度,为了避免一些工作执行耽搁了浏览器绘制,而造成卡帧景象,react 对于一些优先级不高的工作,采纳异步调度来解决,也就是让浏览器才闲暇的工夫来执行这些异步工作,异步工作执行在不同平台,不同浏览器上实现形式不同,这里先权且认为成果和setTimeout
一样。
雨过天晴
通过上述咱们发现 useEffect
的第一个参数 create
,采纳的异步调用的形式,那么闪现就很好了解了,在点击按钮组件第一次渲染过程中,首先执行函数组件render
,而后commit
替换实在dom节点,而后浏览器绘制结束。此时浏览器曾经绘制了一次,而后浏览器有空余工夫执行异步工作,所以执行了create
,批改了元素的地位信息,因为上一次元素曾经绘制,此时又批改了一个地位,所以感到闪现的成果,此案已破。,
那么咱们怎么样解决闪现的景象呢,那就是 React.useLayoutEffect
,useLayoutEffect
的 create
是同步执行的,所以浏览器绘制一次,间接更新了最新的地位。
React.useLayoutEffect(()=>{
card.current.style.left = offset
},[])
总结
本节可咱们学到了什么?
本文以破案的角度,从原理角度解说了 React
一些意想不到的景象,透过这些景象,咱们学习了一些 React 外在的货色,我对如上案例总结,
- 案件一-对一些组件渲染和组件谬误机会申明的了解
- 案件二-理论事件池概念的补充。
- 案件三-是对一些组件库引入多个版本
React
的思考和解决方案。 - 案件四-要留神给
memo
/PureComponent
绑定事件,以及如何解决PureComponent
逻辑,shallowEqual
的原理。 - 案件五-理论是对
fiber
双缓存树的解说。 - 案件六-是对
useEffect create
执行机会的解说。
最初, 送人玫瑰,手留余香,感觉有播种的敌人能够给笔者点赞,关注一波 ,陆续更新前端超硬核文章。欢送关注笔者公众号 前端Sharing , 第一工夫分享硬核文章
发表回复