整理自 gitHub 笔记
一、误区:
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 ( <div> <p>{clickCount}</p> <Button handleClick={handleClick}>Click</Button> </div> ) }
二、useCallback
解决的问题
useCallback
其实是利用 memoize
减少不必要的子组件重新渲染
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
优化:
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
的问题
3.1 useCallback
的实参函数读取的变量是变化的(一般来自 state, props)
export default function Index() {const = 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 = useState('Initial value');
const handleSubmit = useCallback(() => {console.log(`Text: ${text}`); // 每次输出都是初始值
}, ); // 把 `text` 写在依赖数组里
return (
<>
<input value={text} onChange={(e) => updateText(e.target.value)} />
<p onClick={handleSubmit}>useCallback(fn, deps)</p>
</>
)
}
虽然问题解决了,但是方案不是最好的,因为 input 输入框变化太频繁,useCallback
存在的意义没啥必要了。
3.2 How to read an often-changing value from 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 = useState('Initial value');
const handleSubmit = useCallback(() => {console.log(`Text: ${text}`);
}, );
return (
<>
<input value={text} onChange={(e) => updateText(e.target.value)} />
<ExpensiveTree onClick={handleSubmit} />
</>
)
}
问题:更新 input 值,发现比较卡顿。
3.2.1 useRef
解决方案
优化的思路:
- 为了避免子组件
ExpensiveTree
在无效的重新渲染,必须保证父组件 re-render 时handleSubmit
属性值不变; - 在
handleSubmit
属性值不变的情况下,也要保证其能够访问到最新的 state。
export default function Index() {const = useState('Initial value');
const textRef = useRef(text);
const handleSubmit = useCallback(() => {console.log(`Text: ${textRef.current}`);
}, [textRef]);
useEffect(() => {console.log('update text')
textRef.current = 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 = useState('Initial value');
const handleSubmit = useEffectCallback(() => {console.log(`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。
3.2.2 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 = 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
。
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 = 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>
)
});