开篇
「单向数据流」是react推崇的范式,数据流「自顶向下」,一个很明确的概念就是当我们的父组件发生重渲染的时候,会连带着子组件的重渲染。
比如如下的组件,当父组件 setNumber
之后,虽然说我们的子组件自身的状态没有任何的变化,但是由于父组件的 re-render ,子组件中会随着render次数打印这句console。
child render
import { memo, useState } from "react";
import "./styles.css";
const Child = ({ number }) => {
const [word, setWord] = useState("bibi");
console.log("child render");
return (
<div>
this is child with word: {word}
<br />
<button onClick={() => setWord("dandan")}>change word</button>
</div>
);
};
export default function App() {
const [number, setNumber] = useState(0);
return (
<div className="App">
<h1>this is father with number: {number}</h1>
<br />
<button onClick={() => setNumber((n) => n + 1)}>change number</button>
<Child />
</div>
);
}
针对这个情况,解决办法也很简单,对于 FunctionComponent 来说,我们有 React.memo
来帮助我们做类似 ClassComponent 中 shouldComponentUpdate
的事情
import { memo, useState } from "react";
import "./styles.css";
++const Child = memo(({ number }) => {
const [word, setWord] = useState("bibi");
console.log("child render");
return (
//...
);
++});
export default function App() {
const [number, setNumber] = useState(0);
return (
//...
);
}
你可以在这里看到对应的demo
带上context玩玩
可以看一下这里的demo
明确一个概念,我们常说的「当context的value发生了变化,导致了我们的组件树发生了重渲染」
这里的「value发生了变化」指的是value实实在在的发生了更改:
- 如果value是一个对象,对象的引用发生了变化
- 如果value引用不变,对象内部的某个值发生了变化
当我们在父组件中引入了一个context对象,并且在子组件中消费了context,不管我们是在context中提供了修改函数或者是useReduce之类的dispatch函数,最终都会修改了这个value变量。
调度dispatchA -> 引起stateA immutable更新 -> 更新value -> 引用地址变化发生context重渲染
听起来很make sense对不对~
绝大多数人对「context」都是上述概念。似乎「context中value」的变化是因为我的 dispatch
产生的。
听起来也没错,但是很不幸不光如此。如果我们尝试在context中使用一个静态类型数据,比如一个简单的基本类型
<AppContext.Provider value={0}>
<Child />
</AppContext.Provider>
当我们的父组件发生re-render的时候,Child组件不由得也会发生重渲染。这个时候context的value没有发生改变。
真遗憾
所以明确一下结论,能让context发生重渲染的机制有两种:
- dispatch一个update产生的re-render
- context所在组件产生的re-render,也会导致context发生re-render,听起来有点拗口(狗头)
其中2所产生的重渲染不一定是自身state的重渲染,很可能也有外部因素比如props而导致的重渲染。
当我们聊题目的时候,我们在聊什么?
绝大多数场景,当我们看到一个命题比如「context带来的无意义重渲染」,我们往往在聊的都是通过dispatch产生的重渲染。
那么context发生的重渲染和正常父子组件的重渲染有没有啥区别?
回到开篇的例子,再看看上文context的例子,或许第二个例子中多了各种reduce和dispatch,但是从根本意义上而言,真没啥区别。
根本原因是,他们都是由于某一层重渲染导致了自身的重渲染。但唯一的区别可能是,context所在的层次太过于高,甚至往往是最高。
在这么层级如此之高的情况下发生重渲染,渲染成本是十分昂贵的。
回到题目,我们的处理方式是?
上文的概念甚至可以继续收拢成「怎么避免父组件发生重渲染导致的自身渲染」
基于这个想法出发,一些正常的方案我们就能够想得到了。
泛memo化
可以指的是React.memo,也可以指useMemo包裹起来的组件/值,以方便我们达到scu的功能。直接上Dan的例子
React.memo
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme;
return <ThemedButton theme={theme} />
}
// 对 ThemedButton 使用 Memo,只「响应」theme 的变化
const ThemedButton = memo(({ theme }) => {
return <ExpensiveTree className={theme} />;
});
useMemo
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // 相当于自己实现的 selector
return useMemo(() => {
return <ExpensiveTree className={theme} />;
}, [theme])
}
props.children
通过children来实现memo效果其实也是黑科技之一。
我们都知道react在运行时都是调用的React.createElement,只要我们保持传进去的props.children的引用不变,一样可以实现类似memo的效果。
const Container = (props) => {
//...
return (
<Context.Provider value={value}>
{props.children}
</Context.Provider>
)
}
const Demo = () => {
return (
<div>
<Container>
<Count />
<SetCount />
<Pure />
</Container>
</div>
)
}
粒度比较粗,局限性比较大:
- 实际工作中除非是特别简单的场景。否则次次都需要多包一个额外的父容器带来的成本较高。
- 由于「1」的原因,永远需要向其他同事解释这东西是用来干嘛的。或者次次写注释。
- 且不好阅读。
拆分context
拆!我就摁拆!
这当然是一个解决方案之一,对不同上文背景的context进行拆分,这样就很容易能做到「组件按需选用订阅自己的 Contexts data」。
实际上拆分context也是官方提出的「最佳」解决方案之一。既然一个context很可能处于较高的位置,那么是不是所有的context都必须处于这个位置?
合理的决定context value的位置,从而让我们原本整个组件树从上而下重渲染变成个别子树的重渲染。
理论上只要context拆的足够细致,value的划分足够合理,我们完全可以规避各种所谓不必要的重渲染。
- value是不是必须处于这一层context?他们之间不共用的是否可以再拆一下?
- context会有多变和不多变之分么?一些很少变动的context我们是不是可以放到外层?频繁变动的放到内层?
将Context拆分到最小的粒度,是最好的实现。但是也自然带来一个问题:当context非常多的时候,外层的Provider会嵌套很多层。因此,在日常开发中,开发人员还是会考虑将context合并。永远也做不到代码和性能的兼得。
看起来麻烦,写起来更麻烦。js这叼语言灵活的飞起,每个人的编程范式都不一样,图方便一把梭完事他不香吗?
在项目中喜欢在根组件塞一个大对象的人不在少数。(including me
所以这一套当然是好的,但是必然要求开发者需要有良好的编程习惯,以及严格的自我约束。
社区方案
能不能用更细粒度的实现按需订阅?
use-context-selector
了解了上面分析,其实我们也不难猜测出,或自己设计并实现一个 Context selectors:使用同一份引用地址维护 state 和 selectors,在修改 state 时,保证改引用地址不变,因此自动 bailout 掉整颗组件树的无脑渲染,我们通过比对分析出变化 state 所对应的组件,调用 forceUpdate 去触发必要组件的 re-rendering。
熟悉的vue感觉。。
推荐阅读
详细也可以见这一篇 如何优雅地处理使用 React Context 导致的不必要渲染问题?
程墨的经典回答 避免React Context导致的重复渲染
codesandbox举例