共计 17803 个字符,预计需要花费 45 分钟才能阅读完成。
前言
明天咱们来一期不同寻常的 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,第一工夫分享硬核文章