SpringCloudalibaba+Vue开发仿社交小程序-所谓伊人,在水之湄

React 为什么重新渲染
SpringCloudalibaba+Vue开发仿社交小程序
download:https://www.51xuebc.com/thread-547-1-1.html
更新(重新渲染)是 React 的重要特性 —— 当用户与应用交互的时分,React 需求重新渲染、更新 UI,以响应用户的输入。但是,React 为什么会重新渲染呢?假如不晓得 React 为什么会重新渲染,我们如何才干防止额外的重新渲染呢?
TL; DR
状态改动是 React 树内部发作更新的唯二缘由之一。
这句话是 React 更新的公理,不存在任何例外。本文也将会盘绕解释这句话展开。为了防止有人抬杠,这句话引入了一些限制定语和关键词:
名词解释
「更新」和「重新渲染」
在 React 中,「更新」和「重新渲染」是关系严密,但是含义完整不同的两个词。下面这句话才干正确表达这两个词的正确含义:
React 的「更新」包含三个阶段:渲染(Render),运用 createElement 或 jsx-runtime 产生全新的 React Element 对象、组装出一颗 React 树;Reconcilation,React Reconciler 比拟 重生成的 React 树 和 当前的 React 树,判别如何用最高效的办法完成「更新」;Commit,操作 Host(如 DOM、Native 等),使新的 UI 呈如今用户面前。

大局部开发者会把「更新」和「重新渲染」混为一谈,由于在上述三个阶段中,只要「渲染」这一阶段是开发者能够控制的(「Reconcilation」和「Commit」分别由 react-reconciler 和 React Host 控制)。本文接下来的局部中,「重新渲染」一概指代 React 组件在「更新」时的「渲染」阶段,而「更新」则一概指代(重新)渲染、Reconcilation 和 Commit 整个过程。
「React 树」和「React 树内部」
React Tree 自身能够在恣意时分更新。实践上,假如你曾经经过 React 文档学习 React,你在「Hello World」一章就曾经见过这个 Pattern 了:
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
// 假如你是在 React 18 发布以前学习的 React,你可能会用 ReactDOM.render():
// ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
每秒钟调用一次 ReactDOM 提供的 render 使一整颗 React 树停止了完好的更新。但是绝大局部时分,你不会更新一整颗 React 树,而是 React 树内的一局部组件(在 React 应用中,你只会调用一次 createRoot().render 或者 hydrateRoot())。
「唯二缘由」
假如你在运用 React class 组件,那么你能够运用继承自 React.Component 的 forceUpdate 办法更新一个组件:
class MyComponent extends React.Component {
handleInput() {
this.forceUpdate();
}
}
因而,我们也能够把这句话改写成:假如一颗 React 树中一切的 class 组件都没有运用 forceUpdate 办法,那么状态改动是这颗 React Tree 内部发作更新的独一缘由。
在正文开端之前,先放出一句十分具有迷惑性的话:
误区 0:React 组件更新有三个缘由:状态改动,prop 改动,Context 改动。
假如你去问一些运用 React 的开发者「为什么 React 会更新/重新渲染」,大约会得到这个答案。这句话不无道理,但是并不能反响真实的 React 更新机制。
本文只会引见 React 为什么会发作更新,不会引见如何防止「不用要」的更新(或许我会以这个为话题另外写一篇文章?)。
状态更新和单向数据流
让我们以计数器为例:
const BigNumber = ({ number }) => (
<div style={{ fontWeight: 700, fontSize: 36 }}>{number}</div>
);
const Counter = () => {
const [count, setCount] = useState(0);
const handleButtonClick = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<BigNumber number={count} />
<button onClick={handleButtonClick}>Increment</button>
</div>
);
};
const App = () => (
<>
<Counter />
<footer>
<a href="https://skk.moe/">Sukka</a>
</footer>
</>
);
在这个例子中,我们声明了三个组件,根组件 渲染了 ;而 渲染了 。在 组件中,我们声明了一个组件内的状态 count,当点击按钮时会改动状态 count、使其递增。
当我们点击按钮的时分,setCount 被调用、count 状态发作改动,React 更新了 组件。而当 React 更新一个组件时,也会更新这个组件下的一切子组件(至于为什么,很快就会讲的)。因而 组件更新时,子组件 也会更新。
如今让我们先厘清一个最简单的误区:
误区 1:当一个状态发作改动时,整颗 React 树都会更新。
有少数运用 React 的开发者会置信这一点(还好不是大多数!)。实践上,当状态发作改动的时分,React 只会更新「具有这个状态」的组件,和这个组件的一切子组件。
为什么父组件(在这个例子中, 是 的父组件)没有发作更新呢?由于 React 的主要任务就是坚持 React 内的状态和 React 渲染的 UI 的同步。React 更新,就是找出如何改动 UI,使其和新的状态同步。而在 React 中,数据是自上而下单向传送的(单向数据流,The Data Flows Down)。在这个例子中, 组件的状态 count 向下流向了 组件的 prop number,但是不可能向上流向了 组件。因而,count 状态改动, 组件并不需求更新。
当 count 状态改动时, 组件及其子组件 都发作了更新。而 组件更新时,运用了 prop number 的新的值停止渲染。那么 组件更新的缘由是由于 prop number 的改动吗?
不,和 props 完整没有关系
误区 2:React 组件更新的其中一个缘由是它的 prop 发作了改动。
如今让我们修正一下上面那个例子:
import BigNumber from './big-number';
const SomeDecoration = () => <div>Hooray!</div>
const Counter = () => {
const [count, setCount] = useState(0);
const handleButtonClick = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<BigNumber number={count} />
<button onClick={handleButtonClick}>Increment</button>
<SomeDecoration />
</div>
);
};
const App = () => (
<>
<Counter />
<footer>
<a href="https://skk.moe/">Sukka</a>
</footer>
</>
);
组件不承受任何 prop、不运用其父组件 的 count 状态,但是当 count 状态发作改动时, 组件依然发作了更新。当一个组件更新时,React 会更新 一切的子组件,不论这个子组件能否承受一个 prop:React 并不能百分之百肯定 组件能否直接/间接地依赖了 count 状态。
理想中,每一个 React 组件都应该是一个 纯函数 —— 一个「纯」的 React 组件,当输入相同的 props 时,总是会渲染相同的 UI。但是理想是骨感的,我们十分容易写出一个「不纯」的 React 组件:
const CurrentTime = () =>
Last rendered at {new Date().toString()}
包含了状态(运用了 useState)的组件也不是纯组件:即便 prop 不改动,组件也会由于状态不同而渲染出不同的 UI。
有的时分,你很难判别一个组件能否是纯组件。你可能会将一个 Ref 作为 prop 传送给一个组件(forwardRef,useImperativeHandle,诸如此类的 case)。Ref 自身是 Reference Stable 的、React 并不能晓得 Ref 中的值能否改动。
React 的目的是展现最新、维持分歧的 UI。为了防止向用户展现过时的 UI,当父组件更新时,React 会更新一切子组件,即便子组件不承受任何 prop。props 和组件更新没有任何关系。
纯组件和 memo
你大约很熟习(或者至少听说过)React.memo、shouldComponentUpdate 或者 React.PureComponent,这些工具允许我们「疏忽更新」:
const SomeDecoration = memo(() => <div>Hooray!</div>);
当我们将 组件的声明包裹在 memo 中时,我们实践上做的是通知 React「嘿!我觉得这是个纯组件,只需它的 prop 不改动,我们就别更新它」。
如今,让我们把 和 都包裹在 memo 中,看看会发作什么:
const BigNumber = memo(({ number }) => (
<div style={{ fontWeight: 700, fontSize: 36 }}>{number}</div>
));
const SomeDecoration = memo(() => <div>Hooray!</div>);
const Counter = () => {
const [count, setCount] = useState(0);
const handleButtonClick = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<BigNumber number={count} />
<button onClick={handleButtonClick}>Increment</button>
<SomeDecoration />
</div>
);
};
const App = () => (
<>
<Counter />
<footer>
<a href="https://skk.moe/">Sukka</a>
</footer>
</>
);
如今,当 count 状态更新后,React 会更新 组件及其一切子组件, 和 。由于 承受一个 prop number,而 number 的值发作了改动,因而 会更新。但是 的 prop 没有发作改动(由于不承受任何 prop),所以 React 跳过了 的更新。
于是你想,为什么 React 不默许一切组件都是纯组件呢?为什么 React 不 memo 一切组件呢?事实上,React 组件更新的开支没有想象中的那么大。以 组件为例,它只需求渲染一个
。
假如一个组件承受很多复杂的 prop,有可能渲染这个组件并比照 Virtual DOM 的性能开支以至小于等于浅比拟一切 prop 的开支。绝大局部时分,React 是足够快的。因而,只要当一个 纯组件 有大量纯的子组件、或者这个 纯组件 内部有很多复杂计算时,我们才需求将其包裹在 memo 中。
当一个包裹在 memo 中的组件运用了 useState、useReducer 或者 useContext,当这个组件内的状态发作改动时,这个组件依然会更新。
另外一个 React 默许不 memo 一切组件的缘由是:让 React 在 Runtime 中判别子组件的全部依赖、以跳过子组件的不用要更新,是十分艰难、十分不理想的。计算子组件依赖的最好机遇是编译期间。关于这个 idea 的更多细节,能够看看黄玄在 React Conf 2021 上的演讲 React without memo。
让我们谈谈 Context
误区 3:React 组件更新的其中一个缘由是 Context.Provider 的 value 发作了更新。
假如说,当一个组件由于状态改动而更新时,其一切子组件都要随之更新。那么当我们经过 Context 传送的状态发作改动时,订阅了这个 Context 的一切子组件都要更新也是毫不不测的了。
关于纯组件来说,Context 能够视为一个「躲藏的」、或者「内部的」prop:
const User = memo(() => {
const user = useContext(UserContext);
if (!user) {
return 'Hello, new comer!';
}
return `Hello, ${user.name}!`;
})
在上面的例子中, 组件是一个不承受任何 prop、不运用 useState、也没有任何反作用的纯组件。但是, 组件依赖 UserContext。当 UserContext 保管的状态发作改动时, 组件也会更新。
众所周知,当 Context 的 value 发作改动的时分,一切 <Context.Provider /> 的子组件都会更新。那么为什么即便不依赖 Context 的子组件也会更新呢?Context 自身并不是一个状态管理工具,只是一种状态传送工具。Context 的 value 发作改动的基本缘由还是状态的改动:
const CountContext = createContext(0);
const BigNumber = memo(() => {
const number = useContext(CounterContext);
return (
<div style={{ fontWeight: 700, fontSize: 36 }}>{number}</div>
)
});
const Counter = () => {
const [count, setCount] = useState(0);
const handleButtonClick = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<CountContext.Provider value={count}>
<BigNumber number={count} />
</CountContext.Provider>
<SomeDecoration />
<button onClick={handleButtonClick}>Increment</button>
</div>
);
};
正如上面的例子,CountContext 发作改动的缘由,是 组件的 count 状态发作了改动;发作更新的,也不只仅是 CountContext 的消费组件(及其子组件),还包括 一切的子组件。
代码部署后可能存在的BUG没法实时晓得,事后为理解决这些BUG,花了大量的时间停止log 调试,


