从Context源码实现谈React性能优化
学完这篇文章,你会收获:
了解Context的实现原理
源码层面掌握React组件的render时机,从而写出高性能的React组件
源码层面了解shouldComponentUpdate、React.memo、PureComponent等性能优化手段的实现
我会尽量将文章写的通俗易懂。但是,要完全理解文章内容,需要你掌握这些前置知识:
Fiber架构的大体工作流程
优先级与更新在React源码中的意义
组件render的时机
Context的实现与组件的render息息相关。在讲解其实现前,我们先来了解render的时机。
换句话说,组件在什么时候render?
这个问题的答案,已经在React组件到底什么时候render啊聊过。在这里再概括下:
在React中,每当触发更新(比如调用this.setState、useState),会为组件创建对应的fiber节点。
fiber节点互相链接形成一棵Fiber树。
有2种方式创建fiber节点:
bailout,即复用前一次更新该组件对应的fiber节点作为本次更新的fiber节点。
render,经过diff算法后生成一个新fiber节点。组件的render(比如ClassComponent的render方法调用、FunctionComponent的执行)就发生在这一步。
经常有同学问:React每次更新都会重新生成一棵Fiber树,性能不会差么?
React性能确实不算很棒。但如你所见,Fiber树生成过程中并不是所有组件都会render,有些满足优化条件的组件会走bailout逻辑。
比如,对于如下Demo:
function Son() {
console.log('child render!');
return <div>Son</div>;
}
function Parent(props) {
const [count, setCount] = React.useState(0);
return (
<div onClick={() => {setCount(count + 1)}}>
count:{count}
{props.children}
</div>
);
}
function App() {
return (
<Parent>
<Son/>
</Parent>
);
}
const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
在线Demo地址[2]
点击Parent组件的div子组件,触发更新,但是child render!并不会打印。
这是因为Son组件会进入bailout逻辑。
bailout的条件
要进入bailout逻辑,需同时满足4个条件:
1.oldProps === newProps
即本次更新的props全等于上次更新的props。
注意这里是全等比较。
我们知道组件render会返回JSX,JSX是React.createElement的语法糖。
所以render的返回结果实际上是React.createElement的执行结果,即一个包含props属性的对象。
即使本次更新与上次更新props中每一项参数都没有变化,但是本次更新是React.createElement的执行结果,是一个全新的props引用,所以oldProps !== newProps。
2.context value没有变化
我们知道在当前React版本中,同时存在新老两种context,这里指老版本context。
3.workInProgress.type === current.type
更新前后fiber.type不变,比如div没变为p。
4.!includesSomeLane(renderLanes, updateLanes) ?
当前fiber上是否存在更新,如果存在那么更新的优先级是否和本次整棵Fiber树调度的优先级一致?
如果一致代表该组件上存在更新,需要走render逻辑。
bailout的优化还不止如此。如果一棵fiber子树所有节点都没有更新,即使所有子孙fiber都走bailout逻辑,还是有遍历的成本。
所以,在bailout中,会检查该fiber的所有子孙fiber是否满足条件4(该检查时间复杂度O(1))。
如果所有子孙fiber本次都没有更新需要执行,则bailout会直接返回null。整棵子树都被跳过。
不会bailout也不会render,就像不存在一样。对应的DOM不会产生任何变化。
老Context API的实现现
在我们大体了解了render的时机。有了这个概念,就能理解ContextAPI是如何实现的,以及为什么被重构。
我们先看被废弃的老ContextAPI的实现。
Fiber树的生成过程是通过遍历实现的可中断递归,所以分为递和归2个阶段。
Context对应数据会保存在栈中。
在递阶段,Context不断入栈。所以Concumer可以通过Context栈向上找到对应的context value。
在归阶段,Context不断出栈。
那么老ContextAPI为什么被废弃呢?因为他没法和shouldComponentUpdate或Memo等性能优化手段配合。
shouldComponentUpdate的实现
要探究更深层的原因,我们需要了解shouldComponentUpdate的原理,后文简称其为SCU。
使用SCU是为了减少不必要的render,换句话说:让本该render的组件走bailout逻辑。
刚才我们介绍了bailout需要满足的条件。那么SCU是作用于这4个条件的哪个呢?
显然是第一条:oldProps === newProps
当使用shouldComponentUpdate,这个组件bailout的条件会产生变化:
-- oldProps === newProps
++ SCU === false
同理,使用PureComponenet和React.memo时,bailout的条件也会产生变化:
-- oldProps === newProps
++ 浅比较oldProps与newsProps相等
回到老ContextAPI。
当这些性能优化手段:
使组件命中bailout逻辑
同时如果组件的子树都满足bailout的条件4
那么该fiber子树不会再继续遍历生成。
换言之,不会再经历Context的入栈、出栈。
这种情况下,即使context value变化,子孙组件也没法检测到。
新Context API的实现
知道老ContextAPI的缺陷,我们再来看新ContextAPI是如何实现的。
当通过:
ctx = React.createContext();
创建context实例后,需要使用Provider提供value,使用Consumer或useContext订阅value。
如:
ctx = React.createContext();
const NumProvider = ({children}) => {
const [num, add] = useState(0);
return (
<Ctx.Provider value={num}>
<button onClick={() => add(num + 1)}>add</button>
{children}
</Ctx.Provider>
)
}
使用:
const Child = () => {
const {num} = useContext(Ctx);
return <p>{num}</p>
}
当遍历组件生成对应fiber时,遍历到Ctx.Provider组件,Ctx.Provider内部会判断context value是否变化。
如果context value变化,Ctx.Provider内部会执行一次向下深度优先遍历子树的操作,寻找与该Provider配套的Consumer。
在上文的例子中会最终找到useContext(Ctx)的Child组件对应的fiber,并为该fiber触发一次更新。
注意这里的实现非常巧妙:
一般更新是由组件调用触发更新的方法产生。比如上文的NumProvider组件,点击button调用add会触发一次更新。
触发更新的本质是为了让组件创建对应fiber时不满足bailout条件4:
!includesSomeLane(renderLanes, updateLanes) ?
从而进入render逻辑。
在这里,Ctx.Provider中context value变化,Ctx.Provider向下找到消费context value的组件Child,为其fiber触发一次更新。
则Child对应fiber就不满足条件4。
这就解决了老ContextAPI的问题:
由于Child对应fiber不满足条件4,所以从Ctx.Provider到Child,这棵子树没法满足:
!! 子树中所有子孙节点都满足条件4
所以即使遍历中途有组件进入bailout逻辑,也不会返回null,即不会无视这棵子树的遍历。
最终遍历进行到Child,由于其不满足条件4,会进入render逻辑,调用组件对应函数。
const Child = () => {
const {num} = useContext(Ctx);
return <p>{num}</p>
}
在函数调用中会调用useContext从Context栈中找到对应更新后的context value并返回。
总结
React性能一大关键在于:减少不必要的render。
从上文我们看到,本质就是让组件满足4个条件,从而进入bailout逻辑。
而ContextAPI本质是让Consumer组件不满足条件4。
我们也知道了,React虽然每次都会遍历整棵树,但会有bailout的优化逻辑,不是所有组件都会render。
极端情况下,甚至某些子树会被跳过遍历(bailout返回null)。
参考资料
[1]React技术揭秘:
[2]在线Demo地址: https://codesandbox.io/s/quirky-chaplygin-5bx67?file=http://www.ym97.com/src/App.js
原文地址:https://www.4hou.com/posts/0OxV
相关热词:
本站内容来源于网络,如有侵权请与我们联系,我们会及时删除,我们深感抱歉!
注:本站所有信息仅供用于网络技术学习参考,学习中请遵循相关法律法规!
本文地址: https://www.juheyunku.com/jiaob/bcjs/8320.shtml
相关文章
热门TAG
命令 外链 企业网站 白帽 php 织梦教程 dedecms修改内容 javascript 织梦 功能 标签 调用 详解 技巧 权重 服务器 网站流量 Dedecms 织梦cms HTML tags标签 python jquery教程 jquery windows 蜘蛛 搜索引擎 网站收录 JSP 实例解析最新文章
-
大牛眼中的好代码是什么
时间:2021-01-05
-
大数据专业毕业生未来可
时间:2021-01-05
-
看看人家那后端API接口写
时间:2021-01-05
-
详解三大编译器:gcc、l
时间:2021-01-05
-
超级干货:什么是低代码
时间:2021-01-05
-
如何在整个DevOps中建立分
时间:2021-01-05
-
如何从零思考设计你的
时间:2021-01-05
-
Vue源码中值得学习的方法
时间:2021-01-05
热门文章
-
20个有争议的编程观点
时间:2020-12-25
-
从0学ARM-汇编伪指令、LDS详解
时间:2020-12-26
-
你需要了解的几种微前端解决方案
时间:2020-12-27
-
2020年编程语言排行榜单年终大盘点
时间:2020-12-23
-
高级语言中的语句在汇编中是如何实现的
时间:2020-12-27
-
8 个让我更有效率的 Git 别名
时间:2020-12-27
-
一个高薪的码农,应具备的8种能力
时间:2020-12-25
-
Vue源码中值得学习的方法
时间:2021-01-05
-
大数据clouderaManager5.14.0离线部署
时间:2020-12-26
-
如何互联网没了女性的编程会怎样?
时间:2020-12-27
