学完这篇文章,你会收获:react
了解Context
的实现原理web
源码层面掌握React
组件的render
时机,从而写出高性能的React
组件算法
源码层面了解shouldComponentUpdate
、React.memo
、PureComponent
等性能优化手段的实现性能优化
我会尽可能将文章写的通俗易懂。可是,要彻底理解文章内容,须要你掌握这些前置知识:markdown
Fiber
架构的大致工做流程架构
优先级
与更新
在React
源码中的意义ide
若是你还不具有前置知识,能够先阅读React技术揭秘。函数
Context
的实现与组件的render
息息相关。在讲解其实现前,咱们先来了解render
的时机。oop
换句话说,组件
在何时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);
复制代码
点击Parent
组件的div
子组件,触发更新,可是child render!
并不会打印。
这是由于Son
组件会进入bailout
逻辑。
要进入bailout
逻辑,需同时知足4个条件:
oldProps === newProps
即本次更新的props
全等于上次更新的props
。
注意这里是全等比较。
咱们知道组件render
会返回JSX
,JSX
是React.createElement
的语法糖。
因此render
的返回结果其实是React.createElement
的执行结果,即一个包含props
属性的对象。
即便本次更新与上次更新props
中每一项参数都没有变化,可是本次更新是React.createElement
的执行结果,是一个全新的props
引用,因此oldProps !== newProps
。
context value
没有变化咱们知道在当前React
版本中,同时存在新老两种context
,这里指老版本context
。
workInProgress.type === current.type
更新先后fiber.type
不变,好比div
没变为p
。
!includesSomeLane(renderLanes, updateLanes) ?
当前fiber
上是否存在更新
,若是存在那么更新
的优先级
是否和本次整棵Fiber
树调度的优先级
一致?
若是一致表明该组件上存在更新,须要走render
逻辑。
bailout
的优化还不止如此。若是一棵fiber
子树全部节点都没有更新,即便全部子孙fiber
都走bailout
逻辑,仍是有遍历的成本。
因此,在bailout
中,会检查该fiber
的全部子孙fiber
是否知足条件4(该检查时间复杂度O(1)
)。
若是全部子孙fiber
本次都没有更新须要执行,则bailout
会直接返回null
。整棵子树都被跳过。
不会bailout
也不会render
,就像不存在同样。对应的DOM不会产生任何变化。
如今咱们大致了解了render
的时机。有了这个概念,就能理解Context
API是如何实现的,以及为何被重构。
咱们先看被废弃的老Context
API的实现。
Fiber
树的生成过程是经过遍历实现的可中断递归,因此分为递和归2个阶段。
Context
对应数据会保存在栈中。
在递阶段,Context
不断入栈。因此Concumer
能够经过Context栈
向上找到对应的context value
。
在归阶段,Context
不断出栈。
那么老Context
API为何被废弃呢?由于他无法和shouldComponentUpdate
或Memo
等性能优化手段配合。
要探究更深层的缘由,咱们须要了解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相等
回到老Context
API。
当这些性能优化手段:
使组件命中bailout
逻辑
同时若是组件的子树都知足bailout
的条件4
那么该fiber
子树不会再继续遍历生成。
换言之,不会再经历Context
的入栈、出栈。
这种状况下,即便context value
变化,子孙组件也无法检测到。
知道老Context
API的缺陷,咱们再来看新Context
API是如何实现的。
当经过:
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。
这就解决了老Context
API的问题:
因为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
逻辑。
而Context
API本质是让Consumer
组件不知足条件4。
咱们也知道了,React
虽然每次都会遍历整棵树,但会有bailout
的优化逻辑,不是全部组件都会render
。
极端状况下,甚至某些子树会被跳过遍历(bailout
返回null
)。