React 版本 16.14.0
例子
import { useState } from 'react';
export default function App() {
let [color, setColor] = useState('red');
return (
<div>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
function ExpensiveTree() {
console.log('re-render!');
return <p>I am a very slow component tree.</p>;
}
当选择颜色时,ExpensiveTree
组件也会渲染。这对于 ExpensiveTree
来说,是一次额外的渲染。可以进行优化。
React.memo / useMemo
// React.memo
const ExpensiveTree = React.memo(() => {
console.log('re-render !')
return <p>I am a very slow component tree.</p>;
})
// useMemo
const ExpensiveTree = () => {
return useMemo(() => {
console.log('re-render !')
return <p>I am a very slow component tree.</p>;
}, [])
}
state 下移
也就是将需要渲染更新的组件跟不需要渲染的组件区分开来:
import { useState } from 'react';
export default function App() {
return (
<div>
<Form />
<ExpensiveTree />
</div>
);
}
function Form() {
let [color, setColor] = useState('red');
return (
<>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
)
}
function ExpensiveTree() {
console.log('re-render!');
return <p>I am a very slow component tree.</p>;
}
内容提升
将不需要更新渲染的组件,作为 children 传进组件 A 内,而组件 A 中包含需要更新渲染的其他组件。
import { useState } from 'react';
export default function App() {
return (
<Parent>
<ExpensiveTree />
</Parent>
)
}
function Parent({children}) {
let [color, setColor] = useState('red');
return (
<div>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
{children}
</div>
);
}
function ExpensiveTree() {
console.log('re-render!');
return <p>I am a very slow component tree.</p>;
}
这个方法也适用于父组件需要更新渲染,但某些子组件不需要更新渲染的情况,如:
export default function App() {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
改造如下:
export default function App() {
let [color, setColor] = useState('red');
return (
<Parent>
<p>Hello, world!</p>
<ExpensiveTree />
</Parent>
);
}
function Parent({children}) {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
为什么内容提升的做法可以优化?
可以看下 optimize-react-re-renders 中的解释:
If we create the JSX element once and re-use that same one, then we'll get the same JSX every time!
function Logger(props) {
console.log(`${props.label} rendered`)
return null // what is returned here is irrelevant...
}
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>The count is {count}</button>
<Logger label="counter" />
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'))
每次点击按钮,控制台都会打印 Logger 组件内的 log 信息。说明 Logger 组件每次都会 re-render。
看下 jsx 对应的 React.createElement 的代码:
function App() {
return /*#__PURE__*/React.createElement(Counter, {
logger: /*#__PURE__*/React.createElement(Logger, {
label: "counter"
})
});
}
function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(c => c + 1);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
onClick: increment
}, "The count is ", count), /*#__PURE__*/React.createElement(Logger, {
label: "counter"
}));
}
优化后的代码如下:
export default function App() {
return (
<Counter logger={<Logger label="counter" />} />
)
}
function Counter(props) {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>The count is {count}</button>
{props.logger}
</div>
)
}
将对应的 jsx 翻译为 React.createElement,如下:
function App() {
return /*#__PURE__*/React.createElement(Counter, {
logger: /*#__PURE__*/React.createElement(Logger, {
label: "counter"
})
});
}
function Counter(props) {
const [count, setCount] = React.useState(0);
const increment = () => setCount(c => c + 1);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
onClick: increment
}, "The count is ", count), props.logger);
}
观察一下,Counter 组件前后的区别:
// 未优化前
function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(c => c + 1);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
onClick: increment
}, "The count is ", count), /*#__PURE__*/React.createElement(Logger, {
label: "counter"
}));
}
// 优化后
function Counter(props) {
const [count, setCount] = React.useState(0);
const increment = () => setCount(c => c + 1);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
onClick: increment
}, "The count is ", count), props.logger);
}
明显地看到,Logger 组件在未优化前是使用 React.createElement
创建的,而优化后是 props.logger
。为优化前,当父组件,也就是 Counter 更新时,render 方法执行后,Logger 组件也会使用 React.createElement
创建了一个新的 ReactElement 返回,也就是重渲染了。
深究一下
为什么 props.logger 可以重用 ReactElement,不需要重渲染?
在 beginWork
方法中,可以看到 didReceiveUpdate
为 false (也就是可重用 node)的条件为:
-
props 不变 (oldProps === newProps)
const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps;
-
context 不变(hasLegacyContextChanged())
-
type 不变
-
lane 不变(!includesSomeLane(renderLanes, updateLanes))
回顾上面的例子:
function App() {
return /*#__PURE__*/React.createElement(Counter, {
logger: /*#__PURE__*/React.createElement(Logger, {
label: "counter"
})
});
}
function Counter(props) {
const [count, setCount] = React.useState(0);
const increment = () => setCount(c => c + 1);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
onClick: increment
}, "The count is ", count), props.logger);
}
可以看到 Counter 的 props 在 App 组件内就已经是确定的了,待 Counter 组件更新时,props 并不会变化,因为 App 组件是重用的,所以 props 不变,也就是:
{
logger: /*#__PURE__*/React.createElement(Logger, {
label: "counter"
})
}
所以,props.logger 在 Counter 变化时,依然稳定地复用了以前的 ReactElement。
参考
[before-you-memo] overreacted.io/zh-hans/bef…
[optimize-react-re-renders] kentcdodds.com/blog/optimi…
[beginWork] packages/react-reconciler/src/ReactFiberBeginWork.old.js
[createElement] packages/react/src/ReactElement.js