React.StrictMode下会渲染两次

不废话直接代码

App.js

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  const promise = Promise.resolve("hello");
  console.log(promise);
  promise.then((c) => console.log(c));
  return (
    <div className="App">
      <h1 onClick={() => setCount(count + 1)}>count: {count}</h1>
    </div>
  );
}

index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);

看到每次渲染会打印两次hello,如果去掉index.js里的StrictMode则只会打印一次hello。

官方作者的说法是在StrictMode下的development环境里,会render两次,但是打印promise却只有一次!

更奇怪的是,如果仍然在StrictMode下,将第8行注释掉,第9行注释取消,则只会打印hello一次。这个我暂时无法理解😣

代码:https://codesandbox.io/s/strict-mode-render-twice-gxdick?file=/src/App.js

useMemo和useCallback

本文探究useMemo和useCallback两个Hook究竟应该如何使用。

比较useMemo和useCallback

首先,useMemo和useCallback在函数的参数方面类似

useMemo(callback, deps)
useCallback(callback, deps)

两者的形式的确相似,第一个参数都是回调函数,第二个参数是依赖关系(useEffect的依赖关系),先解释依赖关系

  • 当deps是null/undefined时候无论是挂载还是更新阶段都会返回新的值,所以在useMemo或useCallback中deps是null值是没有意义的!(但是对于useEffect是有意义的);
  • 当deps是空数组[],则只在挂载阶段返回一次值;
  • 当deps是变量列表时,如[a, b]则a或b中任何一个值发生改变则要返回新值。

这是useMemo和useCallback的相似之处,但实际上两个函数在参数的签名上不完全一样,第一个回调函数参数,useCallback是允许有参数的,但useMemo是没有参数的!

const memo = useMemo((a) => a+b, [b]) // 参数a没有机会传
const memo = useMemo(() => a+b, [a, b]) // 这样才可能是正常的

const handleClick = useCallback((e) => { // 这是很常见的
  e.preventDefault()
  ...
}

useMemo和useCallback 的返回值不同

  • useMemo返回的是回调函数运行后的返回值
  • useCallback返回的是回调函数

可以把下面的代码视为同等

const handleClick = useCallback((page) => dispatch(fetchFeeds(page)), [dispatch])
const handleClick = useMemo(() => (page) => dispatch(fetchFeeds(page)), [dispatch])

这里用useMemo返回了一个function,和上一行useCallback做的事情一样,只不过通常我们并不会这样写。

useMemo要点

useMemo的主要作用在于如果组件产生一次新的渲染,只要缓存值的依赖关系没有变,就仍然返回上次缓存的值,节省了运算时间,并且保持计算值不变(有可能避免额外的子组件渲染)。

官网说明也需要注意

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

其实就是说缓存值并不一定是缓存的,缓存有可能丢失!

另外要注意无论是useMemo还是useCallback它们并不会把历史缓存值返回,返回的都是上一次缓存住的值!

useCallback的使用

有人在凡是用到回调函数的地方不论什么情况都用useCallback处理一下,认为这样能提高性能,是不对的,不清楚useCallback的工作原理反而可能会造成效率下降运行变慢。我们必须厘清在什么情况下才应该使用useCallback。

首先,之所以要使用useCallback是为了避免不必要的属性变化而造成子组件重新渲染,从这个角度来说,直接绑定在dom上的回调事件没有必要用useCallback处理

function MyComponent() {
  const handleClick = useCallback(() => {...}, [...]) // 这样没什么用
  return (<button onClick={handleClick}></button>)
}

要正确使用useCallback,需要理解function是一种引用类型,在闭包内创建的function每次都会是一个新的对象,所以如果这样将这个函数作为参数传给子组件的时候,虽然函数做的事情没有变,但是作为函数的对象已经发生了改变,则会造成子组件重新渲染,所以为了避免这种情况,应该使用useCallback。

假设,我们渲染一个很长的数组列表,所有列表元素上都有一个父元素传来的onClick回调,这时如果父组件刷新,某个不应该影响子组件渲染的属性发生改变,如果用了useCallback将减少很多不必要的子组件渲染。

示例:https://codesandbox.io/s/usecallback-demo-gozqsm?file=/src/App.js

此示例中有一个大列表,如果传给它的onItemClick回调用useCallback处理和不用它处理的效果不同(需要注意的是,MyList是一个memo后的组件,否则父组件只要重新render,即使子组件的属性完全没变也仍然会渲染)。现在当父组件重新渲染时handleClickCached函数每次都是一样的值,因此list2不会重新渲染,而list1重新渲染其下的每个子组件也会随之重新渲染造成资源浪费

因此要注意区分开什么情况下才应该使用useCallback,useCallback的返回值并非没有开销,如果是没有大量的子组件渲染,则不必一定useCallback处理

最后,凡是开发过程中其实都可以先不用useMemo或useCallback处理,只在事后优化的时候再酌情添加,而且这些值的使用往往也要和React.memo配合

React.memo使用

React.memo 用于包裹函数组件,返回一个高阶组件。

React.memo和React.PureComponent是一对概念,React.PureComponent是class组件的优化版本。

如要了解它们和普通没有缓存的组件的区别是什么,首先应该了解React的渲染原理,通常只要父级的组件重新render,不论子组件是否有改变都强制重新渲染,这样会产生一些不必要的渲染。

所以作为普通的class组件(即继承React.Component),shouldComponentUpdate方法总是返回true,而如果继承React.PureComponent的class组件,shouldComponentUpdate会判断组件的props或state是否发生变化(浅对比 shallow compare),如果未发生变化则返回false,从而略过渲染。

React.memo返回的高阶组件渲染行为和React.PureComponent是同样的

引用自官方文档

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

示例:https://codesandbox.io/s/react-memo-demo-1z0zgn

以上示例App组件中有两个state:title和name,name作为子组件的属性。示例

  • 当title发生改变时,父组件render会造成子组件render,title并不会影响子组件的props,所以看到两种不同的表现,组件1和组件3父组件render时它们也会同时render,但组件2和组件4不受影响。
  • 当name发生改变时,只要和props和上次不同,子组件都会render。
  • 子组件内部的state发生变化时,它总会重新render。

究竟什么时候应该使用React.memo,要注意并非所有情况下都用React.memo返回高阶组件就是最有效率的做法,如果一个组件的属性多数情况下会每次都改变,那么React.memo就发挥不了什么作用,反而增加了缓存和对比 所花费的系统资源,这也是React作者没有将所有组件都这样处理的原因,并且渲染并不一定带来dom元素的真实改变,所以要根据实际情况因地制宜的判断。