React 组件渲染过程如果有一个环节出现问题,就会导致整个组件渲染失败,那么整个组件的 UI 层都会显示不出来,这样造成的危害是巨大的,如果越靠近 APP 应用的根组件,渲染过程中出现问题造成的影响就越大,有可能直接造成白屏的情况。
比如如下例子
function ErrorTest(){
return
}
function Test(){
return <div>let us learn React!</div>
}
class Index extends React.Component{
componentDidCatch(...arg){
console.log(arg)
}
render(){
return <div>
<ErrorTest />
<div> hello, my name is alien! </div>
<Test />
</div>
}
}为了防止如上的渲染异常情况 React 增加了 componentDidCatch 和 static getDerivedStateFromError() 两个额外的生命周期,去挽救由于渲染阶段出现问题造成 UI 界面无法显示的情况。
componentDidCatch 可以捕获异常,它接受两个参数:
那么 componentDidCatch 中可以再次触发 setState,来降级UI渲染,componentDidCatch() 会在commit阶段被调用,因此允许执行副作用。
class Index extends React.Component{
state={
hasError:false
}
componentDidCatch(...arg){
uploadErrorLog(arg) /* 上传错误日志 */
this.setState({ /* 降级UI */
hasError:true
})
}
render(){
const { hasError } =this.state
return <div>
{ hasError ? <div>组件出现错误</div> : <ErrorTest /> }
<div> hello, my name is alien! </div>
<Test />
</div>
}
}componentDidCatch 作用:
React更期望用 getDerivedStateFromError 代替 componentDidCatch 用于处理渲染异常的情况。getDerivedStateFromError 是静态方法,内部不能调用 setState。getDerivedStateFromError 返回的值可以合并到 state,作为渲染使用。用 getDerivedStateFromError 解决如上的情况。
class Index extends React.Component{
state={
hasError:false
}
static getDerivedStateFromError(){
return { hasError:true }
}
render(){
/* 如上 */
}
}如上完美解决了 ErrorTest 错误的问题。注意事项: 如果存在 getDerivedStateFromError 生命周期钩子,那么将不需要 componentDidCatch 生命周期再降级 ui。
上述内容讲了异步渲染和渲染错误边界,都是对一些特殊情况下渲染的处理。上章节讲到,大部分优化环节 React 都自己在内部处理了。但是有一种情况也值得开发者注意,那就是列表中 key 的使用。合理的使用 key 有助于能精准的找到用于新节点复用的老节点。 React 是如何 diff children 的呢。
我这里为了方便大家了解流程,就不放过多源码了,我用如下几个案例来描述 React diffChild 核心流程。之前做过一期 vue3.0 diff算法的文章,实际在处理手法上还是有一些相似之处的。首先 React 在一次更新中当发现通过 render 得到的 children 如果是一个数组的话。就会调用 reconcileChildrenArray 来调和子代 fiber ,整个对比的流程就是在这个函数中进行的。
第一步:遍历新 children ,复用 oldFiber
react-reconciler/src/ReactChildFiber.js
function reconcileChildrenArray(){
/* 第一步 */
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],expirationTime,);
if (newFiber === null) { break }
// ..一些其他逻辑
}
if (shouldTrackSideEffects) { // shouldTrackSideEffects 为更新流程。
if (oldFiber && newFiber.alternate === null) { /* 找到了与新节点对应的fiber,但是不能复用,那么直接删除老节点 */
deleteChild(returnFiber, oldFiber);
}
}
}
}alternate === null ,那么会删除老 fiber 。第二步:统一删除oldfiber
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}newIdx === newChildren.length 此时证明所有 newChild 已经全部被遍历完,那么剩下没有遍历 oldFiber 也就没有用了,那么调用 deleteRemainingChildren 统一删除剩余 oldFiber 。情况一:节点删除
第三步:统一创建newFiber
if(oldFiber === null){
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber,newChildren[newIdx],expirationTime,)
// ...
}
}情况二:节点增加
第四步:针对发生移动和更复杂的情况
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren,returnFiber)
/* 从mapRemainingChildren删掉已经复用oldFiber */
}情况三:节点位置改变
第五步:删除剩余没有复用的oldFiber
if (shouldTrackSideEffects) {
/* 移除没有复用到的oldFiber */
existingChildren.forEach(child => deleteChild(returnFiber, child));
}最后一步,对于没有复用的 oldFiber ,统一删除处理。
情况四:复杂情况(删除 + 新增 + 移动)
首先 A 节点,在第一步被复用,接下来直接到第四步,遍历 newChild ,E被创建,D B 从 existingChildren 中被复用,existingChildren 还剩一个 C 在第五步会删除 C ,完成整个流程。
接下来 React.lazy + Suspense 来完全模拟实现一个异步组件。
实现效果:
主要思路:
编写:
/**
*
* @param {*} Component 需要异步数据的component
* @param {*} api 请求数据接口,返回Promise,可以再then中获取与后端交互的数据
* @returns
*/
function AysncComponent(Component,api){
const AysncComponentPromise = () => new Promise(async (resolve)=>{
const data = await api()
resolve({
default: (props) => <Component rdata={data} { ...props} />
})
})
return React.lazy(AysncComponentPromise)
}思路:
使用:
/* 数据模拟 */
const getData=()=>{
return new Promise((resolve)=>{
//模拟异步
setTimeout(() => {
resolve({
name:'alien',
say:'let us learn React!'
})
}, 1000)
})
}
/* 测试异步组件 */
function Test({ rdata , age}){
const { name , say } = rdata
console.log('组件渲染')
return <div>
<div> hello , my name is { name } </div>
<div>age : { age } </div>
<div> i want to say { say } </div>
</div>
}
/* 父组件 */
export default class Index extends React.Component{
LazyTest = AysncComponent(Test,getData) /* 需要每一次在组件内部声明,保证每次父组件挂载,都会重新请求数据 ,防止内存泄漏。 */
render(){
const { LazyTest } = this
return <div>
<Suspense fallback={<div>loading...</div>} >
<LazyTest age={18} />
</Suspense>
</div>
}
}效果:
总结:
这个demo仅供大家参考,加深一下对异步组件和 HOC 的理解,但是这种在真实的开发场景也会遇到一些问题。
这节主要讲了 React 的异步组件(React.lazy + Suspense)动态加载原理,渲染的错误边界及其处理,diff 性能调优,以及用一个实践 demo ,lazy + Suspense 模拟实现了异步组件。