React Hooks:正确运用Memoization(记忆化)解决性能问题

React Hooks让咱们的工做方方面面都变得更好。可是但有时候一些性能问题也是很棘手的。咱们能够运用Hooks编写高性能应用,可是前提是你对于下面的这些问题有清醒的认知。react

你该不应使用Memoization?

Memoization:在计算机科学中,记忆化(英语:memoization而非memorization)是一种提升程序运行速度的优化技术。经过储存大计算量函数的返回值,当这个结果再次被须要时将其从缓存提取,而不用再次计算来节省计算时间。 记忆化是一种典型的时间存储平衡方案。 --维基百科typescript

React在大多数使用场景中已经足够高性能。若是你的应用足够快而且没有任何渲染问题,那么就不必往下看了。不要尝试去解决假想的渲染问题,因此在提升性能以前,确认下你是否是熟悉React Profiler数组

若是你已经明确知道加载慢的问题在哪,Memoization是最好的尝试方法。缓存

React.memo 是提升性能工具,同时也是一个HOC(高阶组件)。它和React.PureComponent很类似,只不过它应用于函数组件而不是class组件。若是函数组件根据给定的相同的props渲染相同的结果,React会记住这些,跳过渲染组件,而且复用最后一次渲染结果。函数

默认状况下它会对复杂的props对象进行浅比较。若是你想要控制比较过程,也能够提供一个自定义的比较函数做为钩子函数的第二个参数。工具

无Memoization:

咱们来举一个不使用Memoization的例子,而后看为何这样会形成问题。性能

function List({ items }) {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
}export default function App() {
  log('renderApp');      function List({ items }) {
        log('renderList');
        return items.map((item, key) => <div key={key}>item: {item.text}</div>);
    }
    export default function App() {
        log('renderApp');
        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));
        return (
            <div>
                <h1>{count}</h1>
                <button onClick={() => setCount(count + 1)}>inc</button>
                <List items={items} />
            </div>
        );
    }
	const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}
复制代码

NO1:Live Demo

每次点击increnderApprenderList都会打印,甚至List中没有任何改变。若是这棵数据树足够大,那么它会很容易引起性能问题。咱们须要减小渲染次数。flex

简单Memoization

const List = React.memo(({ items }) => {
        log('renderList');
        return items.map((item, key) => <div key={key}>item: {item.text}</div>);
    });
    export default function App() {
        log('renderApp');
        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));
        return (
            <div>
                <h1>{count}</h1>
                <button onClick={() => setCount(count + 1)}>inc</button>
                <List items={items} />
            </div>
        );
    }
复制代码

NO2:Live Demo

在这个例子中Memoization作了不少工做,而且减小了不少渲染次数。在加载过程当中renderApprenderList都打印了,可是点击inc的时候只有renderApp打印了。优化

Memoization & callback

咱们来作一个小修改,在全部List条目中添加上inc按钮。记住,向组件中传入回调函数来记忆化组件会形成微妙的bugspa

function App() {
        log('renderApp');

        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));

        return (
            <div>
                <div style={{ display: 'flex' }}>
                    <h1>{count}</h1>
                    <button onClick={() => setCount(count + 1)}>inc</button>
                </div>
                <List items={items} inc={() => setCount(count + 1)} />
            </div>
        );
    }
复制代码

NO3:Live Demo

在这个例子中,咱们的Memoization失败了。由于咱们使用了内联匿名函数,每次渲染的时候都会生成新的渲染结果,这样React.mome就失效了。因此在咱们记住这个组件以前,咱们要想办法记住这个函数。

useCallback

十分幸运的是,React有两个内建钩子能够提供上面的功能:useMemouseCallbackuseMemo对于复杂计算颇有用,useCallback对于传递回调函数给须要提升性能的子组件十分有用。

function App() {
        log('renderApp');

        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));

        const inc = useCallback(() => setCount(count + 1));

        return (
            <div>
                <div style={{ display: 'flex' }}>
                    <h1>{count}</h1>
                    <button onClick={inc}>inc</button>
                </div>
                <List items={items} inc={inc} />
            </div>
        );
    }
复制代码

NO4:Live Demo

在这个例子中,咱们的Memoization又失败了。renderList在每次inc被点击的时候都会调用。useCallback的默认行为是函数实例每次挂载的时候都计算新值。由于内联匿名函数每次渲染的时候都会建立新实例,默认设置下useCallback无效。

useCallback with input

const inc = useCallback(() => setCount(count + 1), [count]);

NO5:Live Demo

useCallback接收的第二个参数是一个输入变量数组,当且仅当这些输入变量变化的时候,useCallback会返回新值。在这个例子中,当count变化的时候useCallback会返回新值。每次count在渲染的时候改变,useCallback都会跟着改变。这样也没有实现很好的记忆功能。

useCallback with input of empty array

const inc = useCallback(() => setCount(count + 1), []);

NO6:Live Demo

useCallback也能够接收一个空数组做为第二个参数,这样内联匿名函数就只会调用一次。此次代码实现了记忆功能,点击按钮会打印renderApp,主要按钮的功能也正常,可是内部inc按钮中止正常工做了。

计数器从0增长到1后就中止工做了。匿名函数被建立一次,调用了屡次。由于匿名函数被建立的时候count是0,他的表现就像下面这样: const inc = useCallback(() => setCount(1), []); 这个问题的根本缘由在于,咱们试图同时读写state。幸运的是,react提供了解决上面问题的方法:

useState with functional updates

const inc = useCallback(() => setCount(c => c + 1), []);

NO7:Live Demo

useState返回的setters能够将函数做为参数,这个函数中能够读取上一state的值。在这个例子中,Memoization正确工做,没有bug。

useReducer

const [count, dispatch] = useReducer(c => c + 1, 0);

NO8:Live Demo

useReducer memorizationuseState中的例子同样正确运行了。由于dispatch要保证渲染过程当中参考对象一致,就不须要useCallback了,dispatch使得代码更少的犯Memoization相关错误。

useReducer vs useState

useReducer 更适合管理复杂对象或者下一state依赖上一state的状况。使用useReducer的常见模式是和useContext一块儿使用,避免在大型组件树中显示传递回调。 我比较推荐用useState来管理组件内部数据,useReducer适合管理须要在父子组件中传递的特殊双向数据。

总之,React.memouseReducer是最好的朋友;React.memouseState是时而产生冲突的兄弟,会形成一些问题;谨慎使用useCallback

生活的一部分是工做,工做的一部分是解决问题取悦生活,因此好好生活,好好工做,好好热爱(●ˇ∀ˇ●)

原文地址

相关文章
相关标签/搜索