整理自gitHub笔记html
useCallback
是解决函数组件过多内部函数致使的性能问题使用函数组件时常常定义一些内部函数,总以为这会影响函数组件性能。也觉得useCallback
就是解决这个问题的,其实否则(Are Hooks slow because of creating functions in render?):react
得益于相对于 class 更轻量的函数组件,以及避免了 HOC, renderProps 等等额外层级,函数组件性能差不到那里去;
useCallback
会形成额外的性能;deps
变化判断。useCallback
其实也并非解决内部函数从新建立的问题。
仔细看看,其实无论是否使用useCallback
,都没法避免从新建立内部函数:git
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 ( <div> <p>{clickCount}</p> <Button handleClick={handleClick}>Click</Button> </div> ) }
useCallback
解决的问题useCallback
实际上是利用memoize
减小没必要要的子组件从新渲染github
import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( <button onClick={handleClick}>{children}</button> ) } const MemoizedButton = React.memo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); const handleClick = () => { console.log('handleClick'); increaseCount(clickCount + 1); } return ( <div> <p>{clickCount}</p> <MemoizedButton handleClick={handleClick}>Click</MemoizedButton> </div> ) }
即便使用了React.memo
修饰了Button
组件,可是每次点击【Click】btn都会致使Button
组件从新渲染,由于:数组
Index
组件state发生变化,致使组件从新渲染;handleClick
,Button
也从新渲染。使用useCallback
优化:ide
import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( <button onClick={handleClick}>{children}</button> ) } const MemoizedButton = React.memo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); // 这里使用了`useCallback` const handleClick = useCallback(() => { console.log('handleClick'); increaseCount(clickCount + 1); }, []) return ( <div> <p>{clickCount}</p> <MemoizedButton handleClick={handleClick}>Click</MemoizedButton> </div> ) }
useCallback
的问题useCallback
的实参函数读取的变量是变化的(通常来自state, props)export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // BUG:每次输出都是初始值 }, []); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <p onClick={handleSubmit}>useCallback(fn, deps)</p> </> ) }
修改input
值,handleSubmit
处理函数的依旧输出初始值。
若是useCallback
的实参函数读取的变量是变化的,记得写在依赖数组里。函数
export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // 每次输出都是初始值 }, [text]); // 把`text`写在依赖数组里 return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <p onClick={handleSubmit}>useCallback(fn, deps)</p> </> ) }
虽然问题解决了,可是方案不是最好的,由于input输入框变化太频繁,useCallback
存在的意义没啥必要了。性能
仍是上面例子,若是子组件比较耗时,问题就暴露了:优化
// 注意:ExpensiveTree 比较耗时记得使用`React.memo`优化下,要否则父组件优化也没用 const ExpensiveTree = React.memo(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 ( <div onClick={onClick}> <p>很重的组件,不优化会死的那种</p> </div> ) }); export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) }
问题:更新input值,发现比较卡顿。spa
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 ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) }
原理:
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 = useEffectCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) } function useEffectCallback(fn, dependencies) { const ref = useRef(null); useEffect(() => { ref.current = fn; }, [fn, ...dependencies]) return useCallback(() => { ref.current && ref.current(); // 经过ref.current访问最新的回调函数 }, [ref]) }
useRef
保持变化的值,useEffect
更新变化的值;useCallback
返回固定的callback。useReducer
解决方案const ExpensiveTreeDispatch = React.memo(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 ( <div onClick={() => { dispatch({type: 'log' })}}> <p>很重的组件,不优化会死的那种</p> </div> ) }); 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 ( <> <input value={text} onChange={(e) => dispatch({ type: 'update', preload: e.target.value })} /> <ExpensiveTreeDispatch dispatch={dispatch} /> </> ) }
原理:
dispatch
自带memoize
, re-render时不会发生变化;reducer
函数里能够获取最新的state
。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 ( <TextUpdateDispatch.Provider value={dispatch}> <input value={text} onChange={(e) => dispatch({ type: 'update', preload: e.target.value })} /> <ExpensiveTreeDispatchContext dispatch={dispatch} /> </TextUpdateDispatch.Provider> ) } const ExpensiveTreeDispatchContext = React.memo(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 ( <div onClick={() => { dispatch({type: 'log' })}}> <p>很重的组件,不优化会死的那种</p> </div> ) });