关于react.js:React进阶探案揭秘六种React‘灵异现象

37次阅读

共计 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的值受到 statevalue 属性管制,小明想要通过 handerChange 扭转 value 值,然而他冀望在 setTimeout 中实现更新。能够当他想要扭转 input 值时候,意想不到的事件产生了。

控制台报错如上所示。Cannot read property 'value' of null 也就是说明 e.targetnull。事件源 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);
}

dispatchEventForLegacyPluginEventSystemlegacy 模式下,所有事件都必然通过的次要函数,batchedEventUpdates是解决批量更新的逻辑,外面会执行咱们真正的事件处理函数,咱们在事件原理篇章讲过 nativeEvent 就是 真正原生的事件对象 eventtargetInst 就是 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 隐没大概率就是因为这个 destructordestructorrelease中被触发,而后将事件源放进事件池,期待下一次复用。

当初所有的锋芒都指向了 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),意思是 ReactReact Dom 版本不统一,造成这种状况,然而咱们我的项目中的 ReactReact 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 HooksReactCurrentDispatcher.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 组件,只有 propsnametype 扭转,才促使组件渲染。然而理论状况却是这样:

点击按钮成果:

上不着天; 下不着地

为什么会呈现这种状况呢?咱们再排查一下 Index 组件,发现 Index 组件上有一个 changeType,那么是不是这个的起因呢?咱们来剖析一下,首先状态更新是在父组件 Home上,Home组件更新每次会产生一个新的 changeName,所以IndexPureComponent每次会 浅比拟 ,发现props 中的 changeName 每次都不相等,所以就更新了,给咱们直观的感觉是生效了。

那么如何解决这个问题,React hooks 中提供了 useCallback,能够对 props 传入的回调函数进行缓存,咱们来改一下 Home 代码。

const changeName = React.useCallback((name) => {setType(name)
},[])

成果:

这样就基本解决了问题,用 useCallbackchangeName 函数进行缓存,在每一次 Home 组件执行,只有 useCallbackdeps没有变,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,会浅比拟新老propsstate是否相等,如果相等,则不更新组件。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 会从新创立propsprops 都是不相等的。
  • 第二步,如果新老 props 有一个不为对象,或者不存在,那么间接返回false
  • 第三步,判断新老 propskey 组成的数组数量等不想等,阐明 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上的 memoizedStatebaseState = 0 即是初始化 useState 的值。
  • fiber上的 alternatenull
  • 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 对象上的 memoizedStatebaseState更新成了 1。

而后咱们打印一下 alternatebaseState也更新成了 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 = 1setNumber(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 会造成这种状况,咱们只能顺藤摸瓜找到 useEffectcallback执行机会说起。

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.useLayoutEffectuseLayoutEffectcreate是同步执行的,所以浏览器绘制一次,间接更新了最新的地位。

  React.useLayoutEffect(()=>{card.current.style.left = offset},[])

总结

本节可咱们学到了什么?

本文以破案的角度,从原理角度解说了 React 一些意想不到的景象,透过这些景象,咱们学习了一些 React 外在的货色,我对如上案例总结,

  • 案件一 - 对一些组件渲染和组件谬误机会申明的了解
  • 案件二 - 理论事件池概念的补充。
  • 案件三 - 是对一些组件库引入多个版本 React 的思考和解决方案。
  • 案件四 - 要留神给 memo / PureComponent 绑定事件,以及如何解决 PureComponent 逻辑,shallowEqual的原理。
  • 案件五 - 理论是对 fiber 双缓存树的解说。
  • 案件六 - 是对 useEffect create 执行机会的解说。

最初, 送人玫瑰,手留余香,感觉有播种的敌人能够给笔者 点赞,关注 一波,陆续更新前端超硬核文章。欢送关注笔者公众号 前端 Sharing,第一工夫分享硬核文章

正文完
 0