html form callback,一直以来`useCallback`的使用姿势都不对

整理自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 (

{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 = React.memo(Button);

export default function Index() {

const [clickCount, increaseCount] = useState(0);

const handleClick = () => {

console.log('handleClick');

increaseCount(clickCount + 1);

}

return (

{clickCount}

Click

)

}

即使使用了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 (

{children}

)

}

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 (

{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 比较耗时记得使用`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 (

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

)

});

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 = useEffectCallback(() => {

console.log(`Text: ${text}`);

}, [text]);

return (

<>

updateText(e.target.value)} />

>

)

}

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 (

{ 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 = 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 (

{ dispatch({type: 'log' })}}>

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

)

});

b739ec46bb5c46d9c0aa4ce35ba1ea56.png

关于找一找教程网

本站文章仅代表作者观点,不代表本站立场,所有文章非营利性免费分享。

本站提供了软件编程、网站开发技术、服务器运维、人工智能等等IT技术文章,希望广大程序员努力学习,让我们用科技改变世界。

[一直以来`useCallback`的使用姿势都不对]http://www.zyiz.net/tech/detail-147073.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值