Hook 概述
Hook是React 16.8的新增性,它可以让我们在不编写class的情况下使用state及其他React特性。Hook的诞生是为共享状态逻辑提供更好的原生途径。
我们在项目中也全面推广使用,一个很直观的感受,写起来很爽很简洁。通常情况下,我们会习惯把页面切割成很多的组件(function component),这让我们很容易的组织和管理页面的组成。但在function component中,重新渲染(re-render)很轻易的就会被触发,少量的组件还不会造成太大的问题,但是,遇到复杂的业务,大量的组件不断的被重新渲染,就会给浏览器很大的负担,会造成用户的体验不佳。
所以,我们有必要关注下使用React Hooks函数式组件的优化手段。
优化策略
React Hook优化的三个策略:
- 避免组件重新渲染
- 减少不必要的计算量
- 合理的State/Props数据
原则:
- 缓存组件
- 缓存计算
- 合理数据控制
优化方式:memo、useMemo、useCallback
对于开发React的同学来说,应该比较熟悉。这三种方法都是React官方提供的提供的用来减少不必要计算和渲染的函数工具。
memo
我们经常会让子组件依赖父组件的状态(state)和事件(event),在父组件中定义状态和事件方法,利用props将两者传递到子组件中。
如果父组件的状态改变,但是props的结果没有变,子组件仍然会被重新渲染,但是子组件的结果没有变化,多余的渲染造成性能上的浪费。
所以,React提供了 React.memo 来帮助我们解决这个问题:
const MyComponent = React.memo(Component = (props) => {
/* render using props */
});
React.memo 是以HOC(higher order component)的方式使用,我们只需要在需要减少渲染的组件外边再包裹一层 React.memo ,就可以让React帮我们记住原本的props。
Example
import React from "react";
const MyComponent = ({ myprops }) => {
const refCount = React.useRef(0);
refCount.current++;
return (
<p>
{myprops}, Ref Count: {refCount.current}
</p>
);
};
const MemorizeMyComponent = React.memo(MyComponent);
const MemoDemo = () => {
const [state, setState] = React.useState("");
const handleSetState = (e) => {
setState(e.target.value);
};
return (
<div className="App">
<input type="text" value={state} onChange={handleSetState} />
<MyComponent myprops="MyComponent" />
<MemorizeMyComponent myprops="MemorizeMyComponent" />
</div>
);
};
export default MemoDemo;
在示例程序中,我们改变父组件的state值,使用momo的组件在props没有变化的情况下,是没有重新渲染的,refCount值也没有变化。反之,没有使用memo的组件,会触发子组件的重新渲染,并强迫refCount不断增加。
然而,React.memo 是用 shallowly compare 的方法确认props的值是否一样,shallowly compare 在props 是Number或者String时比较的是数值,当props是Object时,比较的是记忆体位置(reference)。
因此,当父组件重新渲染时,在父组件宣告的Object都会重新分配记忆体位置,所以想要利用React.memo 防止重新渲染就会失效。
要解决这个问题的方法有两种:
React.memo 提供了第二个参数,让我们自定比较props的方法,让Object不再是比较记忆体位置:
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
使用React.useCallback,让React可以自动记住Object的记忆体位置,解决shallowly compare的比较问题。
小结
HOC组件,需要一层函数包裹
- Memo能够缓存上一次渲染结果,依据是控制Props是否产生变化
- 如果Props不是Shallowly Compare,那么需要使用的第二个函数参数控制是否需要重新渲染
- 更好的方式解决Shallowly Compare的问题?
useCallback
当父组件传递的props是Object时,父组件的状态被改变触发重新渲染,Object的记忆体位置也会被重新分配。React.memo 会用shallowly compare 比较props 中Object的记忆体位置,这个比较会让子组件重新被渲染。
因此,React提供了React.useCallback这个方法让React在父组件重新渲染时,如果 dependencies array 中的值没有在被修改的情况下,它会帮助我们记住Object,防止Object被重新分配记忆体位置。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
所以,当React.useCallback能够记住Object的记忆体位置,就可以避免父组件重新渲染后,Object被重新分配记忆体位置,造成 React.memo 的 shallowly compare 发现传递的Object记忆体位置不同。
Example
同样的,当我们在父组件改变input绑定的状态触发重新渲染,使用 useCallback 记住记忆体未知的function 就没有让子组件重新渲染,也咩有让refCount 增加;反之,没有被记住记忆体位置的function会被重新渲染。
import React from "react";
const MyComponent = React.memo((props) => {
const refCount = React.useRef(0);
refCount.current++;
const callback = props.useCallback ? "useCallback" : "without useCallback";
return (
<p>
MyComponent {callback}. Ref Count: {refCount.current}.
</p>
);
});
const equals = (a, b) => {
return a === b ? "Equal" : "Different";
};
const UseCallbackMemo = () => {
const [state, setState] = React.useState("");
const [someArg, setSomeArg] = React.useState("argument");
const handleSomethingUseCallback = React.useCallback(() => {}, [someArg]);
const handleSomething = (e) => {
setState(e.target.value);
};
const handleSetState = (e) => {
setState(e.target.value);
};
const refHandleSomethingUseCallback = React.useRef(
handleSomethingUseCallback
);
const refHandleSomething = React.useRef(handleSomething);
return (
<div className="App">
<input type="text" value={state} onChange={handleSetState} />
<p>
handleSomethingUseCallback:{" "}
{equals(
refHandleSomethingUseCallback.current,
handleSomethingUseCallback
)}
</p>
<p>
handleSomething: {equals(refHandleSomething.current, handleSomething)}
</p>
<MyComponent
handleSomething={handleSomethingUseCallback}
useCallback={true}
/>
<MyComponent handleSomething={handleSomething} useCallback={false} />
</div>
);
};
export default UseCallbackMemo;
练习
接下来,我们使用上面已有的内容,来优化这个很普通的Hook函数组件:
import React from "react";
const Button = ({ handleClick }) => {
const refCount = React.useRef(0);
return (
<button onClick={handleClick}>
button render count: {refCount.current++}
</button>
);
};
const UseCallbackTest = () => {
const [isOn, setIsOn] = React.useState(false);
const handleClick = () => setIsOn(!isOn);
return (
<div className="App">
<h2>{isOn ? "on" : "off"}</h2>
<Button handleClick={handleClick}></Button>
</div>
);
};
export default UseCallbackTest;
- 首先,发现当前组件的问题?
- 提出解决方案?并进一步进行优化。
Note:function component当中没有this,所有component只能通过闭包去取外部的变量,而state在React render的机制里面是 imumutable, 所以funciton还是会使用一开始useState回传的isOn,而非rerender后的isOn,所以就会造成打开后就关闭不了了。 如果对state熟悉,应该知道setState除了接收setState之外,还接收updater function,这个function input是prevState会return下个state。 同样的,updater function的机制在hook中,同样保留,因此我们可以使用update function取得上一个state而不是用closure的方式去取得外部状态。
分析
useCallback源码实现跟useMemo基本上完全一模一样,不同的是useMemo会调用函数获取缓存的值,而useCallck保存的函数所以不需要调用。其余代码一模一样,这里就不做赘述了直接贴源码:
mountCallback
updateCallback
areHookInputsEqual
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
//useCallback缓存的是函数 直接保存的就是函数引用
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
小结
- useCallback返回Memoized Callback
- 两种状态:mount/update
- fiberNode链表记录缓存值
- update阶段shallowly compare对比是否更新
- 如果依赖数组的值没有产生变化,那么不会产生新的Function Reference
- 通过Updater Function获取最新的State
- 配合memo一起使用
useMemo
有一种情况,和父组件没有关系,当组件重新渲染时,当前组件内部的function和运行都会重新计算,也会造成很大的负担,因此,这种问题也是不熟悉hook的同学经常犯的问题,这个也是性能优化的一个点。
所以,当传入的依赖或引用未变化时,那么返回的state引用还是一样的,这样child就不会重新渲染。
React.useMemo 是让React记住函数返回的值,如果 dependenices array 中的变量没有被修改,React.memo 会沿用上一次返回的值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Example
当我们的父组件改变input绑定的值后触发重新渲染,使用useMemo记住的值的子组件没有重新渲染,并让refCount增加;反之没有被记住的值的子组件一直在触发重新渲染并计算。
import React from "react";
const MyComponentWithoutUseMemo = () => {
const refCount = React.useRef(0);
const myfunction = () => {
refCount.current++;
return 1;
};
const value = myfunction();
return <p>MyComponent without useMemo. Ref count: {refCount.current}</p>;
};
const MyComponent = () => {
const refCount = React.useRef(0);
const myfunction = () => {
refCount.current++;
return 1;
};
const value = React.useMemo(() => {
return myfunction();
}, []);
return <p>MyComponent useMemo. Ref count: {refCount.current}</p>;
};
const MyComponentMemo = ({ value }) => {
return <p>MyComponent useMemo. {value}</p>;
};
const UseMemoDemo = () => {
const [state, setState] = React.useState("");
const [val, setVal] = React.useState(Date.now());
const handleSetState = (e) => {
setState(e.target.value);
};
const newValue = "Value:" + val + Math.ceil(Math.random() * 1000);
return (
<div className="App">
<input type="text" value={state} onChange={handleSetState} />
<MyComponentWithoutUseMemo />
<MyComponent />
<MyComponentMemo value={newValue} />
<button
onClick={() => {
setVal(Date.now());
}}
>
change val
</button>
</div>
);
};
export default UseMemoDemo;
Note:React官网提醒You may rely on
**useMemo**
as a performance optimization, not as a semantic guarantee。因此,不要什么都丢到useMemo里面去,在需要优化时才引用。否则只是让React处理更多的事情,造成更大的负担。
分析
从源码可以看出,react把所有的hooks分成了两个阶段的hooks:
- mount阶段对应第一次渲染初始化时候调用的hooks方法,分别对应了
mountMemo
,mountCallback
以及其他hooks。 - update阶段对应setXXX函数触发更新重新渲染的更新阶段,分别对应了
updateMemo
,updateCallback
以及其他hooks ```javascript // react-reconciler/src/ReactFiberHooks.js // Mount 阶段Hooks的定义 const HooksDispatcherOnMount: Dispatcher = { useCallback: mountCallback, useMemo: mountMemo, // 其他Hooks };
// Update阶段Hooks的定义 const HooksDispatcherOnUpdate: Dispatcher = { useCallback: updateCallback, useMemo: updateMemo, // 其他Hooks };
hooks在mount阶段和update阶段所调用的逻辑是不一样的。
<a name="J5mEU"></a>
##### mountMemo
1. 跟其他hook一样,在mount阶段直接创建一个hook挂载到链表上。
1. mount初始化的时候直接调用传入的函数获取需要缓存的值然后直接返回,这样子我们在const state = useMemo(() => {val: 1}, [val])就可以拿到相应的值
1. 跟useState不一样的是:useState是直接保存值到hook的memoizedState属性上,useMemo保存的是一个长度为2的数组,值分别是上面调用后的值以及传入的依赖
```javascript
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// 创建hook对象拼接在hook链表上
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 调用我们传入的函数 获取需要缓存的值
const nextValue = nextCreate();
//与useState直接保存值的不同 useMemo保存在memoizedState的是一个数组
// 第一个值是需要缓存的值 第二个是传入的依赖
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
updateMemo
- 跟useState一样通过
updateWorkInProgressHook
获取更新时当前的useMemo的hook对象。 - 如果上一次的useMemo值(memoizedState)不为空并且这一次传入的依赖(nextDeps)不为空,那么两次依赖做浅比较。
- 依赖没有发生变化,那么直接返回数组第一个值即上一次渲染的值。如果依赖发生变化重新调用函数生成新的值.
```javascript
function updateMemo
( nextCreate: () => T, deps: Array | void | null, ): T { // 第一篇已经说过 获取相对应的hook对象 const hook = updateWorkInProgressHook(); // 获取更新时的依赖 const nextDeps = deps === undefined ? null : deps; // 获取上一次的数组值 const prevState = hook.memoizedState; if (prevState !== null) { // Assume these are defined. If they’re not, areHookInputsEqual will warn. // 如果这次传入的依赖不为空做浅比较 如果依赖没有发生变化那么直接返回上一次的值 if (nextDeps !== null) {
} } // 依赖发生变化 重新调用生成新的值 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
// 浅比较dep的函数
function areHookInputsEqual(
nextDeps: Array
// 浅比较 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; } ```
小结
- useMemo返回Memoized Value
- 如果依赖数组的值没有产生变化,那么不会产生新的Value
- 作为组件自身优化的一种方式,减少计算量
- 类似Vue的Computed,但不建议作为语义化使用
源码和useCallback一模一样,唯一的区别就是一个获取缓存的函数,一个获取缓存值
数据控制
优化state:不是所有状态都应该放在组件的 state 中,例如缓存数据、常量、非UI状态数据
- 优化props:单一原则、影响shallowly compare、提升缓存命中率
- 不要滥用 Context
- 依赖context的组件具有穿透性
- 状态作用域, 全局
- 只放置必要的,关键的,被大多数组件所共享的状态
总结
- memo和useCallback通过组合技巧达到较少渲染的效果
- memo能够侦测props有没有产生变化,减少不必要渲染
- useCallback能够让props的object在父组件重新渲染时,不被重新分配地址
- useMemo能够让组件重新渲染时,避免不必要的重复运算
- 使用传入的状态,尽量不要使用闭包中的 State
- 管理复杂的状态可以考虑使用useReducer等类似的方式,对状态操作定义类型,执行不同的操作
- 优化State/Props数据、小心使用Contenxt
结束
Stay Hungry, Stay Foolish
解释,不同思考角度:
求知若饥,虚心若愚
做个吃货,做个二货