javascript

您所在的位置:网站首页 memo怎么用 javascript

javascript

#javascript| 来源: 网络整理| 查看: 265

一、误区 :useCallback是解决函数组件过多内部函数导致的性能问题

使用函数组件时经常定义一些内部函数,总觉得这会影响函数组件性能。也以为useCallback就是解决这个问题的,其实不然(Are Hooks slow because of creating functions in render?):

JS内部函数创建是非常快的,这点性能问题不是个问题;得益于相对于 class 更轻量的函数组件,以及避免了 HOC, renderProps 等等额外层级,函数组件性能差不到那里去;其实使用useCallback会造成额外的性能; 因为增加了额外的deps变化判断。

useCallback其实也并不是解决内部函数重新创建的问题。 仔细看看,其实不管是否使用useCallback,都无法避免重新创建内部函数:

export default function Index() { const [clickCount, increaseCount] = useState(0); // 没有使用`useCallback`,每次渲染都会重新创建内部函数 const handleClick = () => { console.log('handleClick'); increaseCount(clickCount + 1); } // 使用`useCallback`,但也每次渲染都会重新创建内部函数作为`useCallback`的实参 const handleClick = useCallback(() => { console.log('handleClick'); increaseCount(clickCount + 1); }, []) return (

{clickCount}

Click ) }二、useCallback解决的问题

useCallback其实是利用memoize减少不必要的子组件重新渲染

import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( {children} ) } const MemoizedButton = Reacmo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); const handleClick = () => { console.log('handleClick'); increaseCount(clickCount + 1); } return (

{clickCount}

Click ) }

即使使用了Reacmo修饰了Button组件,但是每次点击【Click】btn都会导致Button组件重新渲染,因为:

Index组件state发生变化,导致组件重新渲染;每次渲染导致重新创建内部函数handleClick ,进而导致子组件Button也重新渲染。

使用useCallback优化:

import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( {children} ) } const MemoizedButton = Reacmo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); // 这里使用了`useCallback` const handleClick = useCallback(() => { console.log('handleClick'); increaseCount(clickCount + 1); }, []) return (

{clickCount}

Click ) }三、useCallback的问题3.1 useCallback的实参函数读取的变量是变化的(一般来自state, props)export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // BUG:每次输出都是初始值 }, []); return ( updateText(e.target.value)} />

useCallback(fn, deps)

) }

修改input值,handleSubmit 处理函数的依旧输出初始值。如果useCallback的实参函数读取的变量是变化的,记得写在依赖数组里。

export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // 每次输出都是初始值 }, [text]); // 把`text`写在依赖数组里 return ( updateText(e.target.value)} />

useCallback(fn, deps)

) }

虽然问题解决了,但是方案不是最好的,因为input输入框变化太频繁,useCallback存在的意义没啥必要了。

3.2 How to read an often-changing value from useCallback?

还是上面例子,如果子组件比较耗时,问题就暴露了:

// 注意:ExpensiveTree 比较耗时记得使用`Reacmo`优化下,要不然父组件优化也没用 const ExpensiveTree = Reacmo(function (props) { console.log('Render ExpensiveTree') const { onClick } = props; const dateBegin = Date.now(); // 很重的组件,不优化会死的那种,真的会死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return (

很重的组件,不优化会死的那种

) }); export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( updateText(e.target.value)} /> ) }

问题:更新input值,发现比较卡顿。

3.2.1 useRef解决方案

优化的思路:

为了避免子组件ExpensiveTree在无效的重新渲染,必须保证父组件re-render时handleSubmit属性值不变;

在handleSubmit属性值不变的情况下,也要保证其能够访问到最新的state。

export default function Index() { const [text, updateText] = useState('Initial value'); const textRef = useRef(text); const handleSubmit = useCallback(() => { console.log(`Text: ${textRef.current}`); }, [textRef]); // 这里可以写成`[]` useEffect(() => { console.log('update text') textRef.current = text; }, [text]) return ( updateText(e.target.value)} /> ) }

原理:

handleSubmit由原来直接依赖text变成了依赖textRef,因为每次re-render时textRef不变,所以handleSubmit不变;每次text更新时都更新textRef.current。这样虽然handleSubmit不变,但是通过textRef也是能够访问最新的值。useRef+useEffect这种解决方式可以形成一种固定的“模式”:export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useEventCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( updateText(e.target.value)} /> ) } function useEventCallback(fn) { const ref = useRef(fn); useEffect(() => { // 每次re-render都会执行这里(逻辑简不影响性能),保证fn永远是最新的 ref.current = fn; }) return useCallback(() => { ref.current && ref.current(); // 通过ref.current访问最新的回调函数 }, []) }通过useRef保持变化的值,通过useEffect更新变化的值;通过useCallback返回固定的callback。3.2.2 useReducer解决方案const ExpensiveTreeDispatch = Reacmo(function (props) { console.log('Render ExpensiveTree') const { dispatch } = props; const dateBegin = Date.now(); // 很重的组件,不优化会死的那种,真的会死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return ( { dispatch({type: 'log' })}}>

很重的组件,不优化会死的那种

) }); function reducer(state, action) { switch(action.type) { case 'update': return action.preload; case 'log': console.log(`Text: ${state}`); return state; } } export default function Index() { const [text, dispatch] = useReducer(reducer, 'Initial value'); return ( dispatch({ type: 'update', preload: e.target.value })} /> ) }

原理:

dispatch自带memoize, re-render时不会发生变化;在reducer函数里可以获取最新的state。We recommend to pass dispatch down in context rather than individual callbacks in props.

React官方推荐使用context方式代替通过props传递callback方式。上例改用context传递callback函数:

function reducer(state, action) { switch(action.type) { case 'update': return action.preload; case 'log': console.log(`Text: ${state}`); return state; } } const TextUpdateDispatch = React.createContext(null); export default function Index() { const [text, dispatch] = useReducer(reducer, 'Initial value'); return ( dispatch({ type: 'update', preload: e.target.value })} /> ) } const ExpensiveTreeDispatchContext = Reacmo(function (props) { console.log('Render ExpensiveTree') // 从`context`获取`dispatch` const dispatch = useContext(TextUpdateDispatch); const dateBegin = Date.now(); // 很重的组件,不优化会死的那种,真的会死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return ( { dispatch({type: 'log' })}}>

很重的组件,不优化会死的那种

) });

整理自GitHub笔记:一直以来useCallback的使用姿势都不对。Buy me a coffee ☕



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3