一篇文章,带你学会useRef、useCallback、useMemo

一篇文章,带你学会useRef、useCallback、useMemo

通过此文章,可以了解到:

  1. useRef保持引用不变是怎么回事;
  2. useRef能拿到上一次的值是怎么回事;
  3. useCallback和useMemo的区别;
  4. 如何使用useCallback,避免无状态组件(函数式组件)的不必要渲染;

一、useRef是怎么回事

1.useRef的含义

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

关键字:

- mutable ref (可变的ref对象, 并不一定指的就是DOM对象哦)

- persist (持久化)

2.举例分析

实现一个需求:

点击按钮,input输入框获取焦点

示例1
获取DOM,使用ref对象拿到DOM,createRef和以前的refs属性异曲同工,所以只拿出createRef和useRef做对比。
  1. createRef API 版本实现
createRef创建的DOM对象,如果挂载在class component中,就表示的组件的instance(实例),如果挂载在DOM元素上,就表示实际的DOM原生对象,具体就是它的current属性。
const UseCreateRef = () => {
    let inputElement = createRef<HTMLInputElement>();
    const focusHandle = () => {
        // if(inputElement.current){
        //     inputElement.current.focus();
        // }
        // 或者----告知ts: inputElement.current的值非空
        inputElement.current!.focus();
    }
    return (
        <div className="content">
            <input ref={inputElement} placeholder="createRef API" />
            <Button onClick={focusHandle}>点击获取焦点</Button>
        </div>
    )
}

2. useRef API版本实现

const UseUseRef = () => {
    const inputElement = useRef<HTMLInputElement | null>(null);
    const focusHandle = () => {
        if(inputElement.current) {
            inputElement.current.focus();
        }
    }
    return (
        <div className="content">
            <input ref={inputElement} placeholder="useRef API" />
            <Button onClick={focusHandle}>点击获取焦点</Button>
        </div>
    )
}

两者是有区别的

1.useRefreact hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西.

2.createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用(persist)。

其中关键点在于:

useRef获取引用是实时的,createRef获取引用是不变的,再举个例子来加深理解。

点击按钮,让index➕1
const UseRefDemoa = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const refFromUseRef = useRef<number | null>(null);
    const refFromCreateRef:any = createRef<HTMLDivElement>();
    if (!refFromUseRef.current) {
        refFromUseRef.current = renderIndex;
    }
    if (!refFromCreateRef.current) {
        refFromCreateRef.current = renderIndex;
    }
    return (
        <div ref={refFromCreateRef} className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <span className="item_title">使用useRef来获取renderIndex {refFromUseRef.current}</span>
            <span className="item_title">使用createRef来获取renderIndex {refFromCreateRef.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
        </div>
    );
}


如何理解:

对于函数式组件来说,每次useState都会造成整个函数的重新渲染,逻辑从上到下重新执行一遍,那么为什么用了useRef就能够阻止re-render过程呢?是因为useRef可以保持引用不会变化,就算重新渲染,refFromUseRef的值也会一直都在,不会因为重新渲染就会重置,所以也就不会在重新赋值了。

useRef实际上就是用了闭包的方式来保持旧引用。

3.再来看一个例子,说明useRef的DOM引用不变这个功能

const UseRefDemob = () => {
    const [renderIndex, setRenderIndex] = useState(1);

    const handleClick = ():void => {
        setTimeout(() => {
            alert(renderIndex)
        }, 3000)
    }

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
            <Button onClick={handleClick}>
                点击让弹出renderIndex
            </Button>
        </div>
    );
}

具体操作看页面,我们看到: 弹出的renderIndex并不是实时的renderIndex,这是因为每次点击事件触发,UseRefDemob都会重新渲染执行,handleClick函数拿到的每次都是当时的renderIndex值,这也就理解了为什么会在你点击增加renderIndex的值后,handleClick依然使用的是当时的renderIndex值,我们拆解成普通js代码:

const handleClick = (renderIndex) => {
        setTimeout(() => {
            alert(renderIndex)
        }, 3000)
}

let renderIndex = 1;
handleClick(renderIndex);

renderIndex = 2;
handleClick(renderIndex);

renderIndex = 3;
handleClick(renderIndex);

renderIndex = 4;
renderIndex = 5;
renderIndex = 6;

结果:

依次会弹出1,2,3

因为我们在代码中更改了renderIndex=6,那么怎样才能让延迟弹出的结果都是我们实时更改的结果呢?

利用引用。

let obj = {};
obj.renderIndex = 0;

const handleClick = () => {
    setTimeout(() => {
        alert(obj.renderIndex)
    }, 3000)
}
obj.renderIndex = 1;
handleClick(obj.renderIndex);

obj.renderIndex = 2;
handleClick(obj.renderIndex);

obj.renderIndex = 3;
handleClick(obj.renderIndex);

obj.renderIndex = 4;
obj.renderIndex = 5;
obj.renderIndex = 6;
这样每次弹出的结果都是6.实现了我们的要求

那么也就容易理解了,useRef也可以实现我们的需求,因为它也是引用,并且不会在函数重新执行后改变引用。

const UseRefDemoc = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const ref = useRef<number | null>(null);
    useEffect(() => {
        ref.current = renderIndex
    })
    const handleClick = ():void => {
        setTimeout(() => {
            alert(ref.current)
        }, 3000)
    }

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}, {ref.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
            <Button onClick={handleClick}>
                点击让弹出renderIndex
            </Button>
        </div>
    );
}

4.useRef可以拿到前一个值

const UseRefDemod = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const ref = useRef<number | null>(null);
    useEffect(() => {
        ref.current = renderIndex
    })

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <span className="item_title">上一个index是: {ref.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
        </div>
    );
}

为什么能拿到上一个值呢?

  • 首先理解一下React-Hooks的生命周期:

函数组件被调用 -> 执行代码 ->根据return的JSX渲染DOM -> 执行useEffect -> 函数组件被重新调用 -> 执行代码 -> 根据return的JSX重新渲染DOM -> 执行useEffect。(循环往复)

  • 接下来拆解一下:

第一次UseRefDemod被执行,参数renderIndex为1,ref.current先是undefined (因为这时useEffect还没有被调用),然后根据return的JSX,渲染DOM,页面上被渲染出ref.current的值 -> undefined,接着 useEffect被调用,此时ref 的current值被赋值,也就是1。

第二次UseRefDemod执行,参数renderIndex为2,ref.current先是undefined,然后return JSX渲染DOM,页面上本渲染出的ref.current为1(此时还没有被赋值),接着useEffect调用,此时ref.current才会被赋值为2,但是已经渲染出来了,不会再变的,页面上就还是上一次的1,也就是上一次的值。

如果对useEffect的用法有疑惑的话,推荐文章:

既然能拿到上一次的值,此时我们可以封装我们的usePrevious了:

import { useRef, useEffect } from 'react'
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, value);
  return ref.current;
}

总结一下为什么用useRef能拿到上一次的值:

  1. useRef保持引用不变;
  2. 函数式组件的声明周期决定,jsx的渲染比useEffect早;
  3. 手动修改ref.current并不会触发组件的重新渲染;

拿到前一个值这件事,想到了什么?

想到了class react中的生命周期shouldComponentUpdate(nextProps,nextState)中比较前后两次属性是否相同来做优化,减少渲染次数,和componentWillReceiveProps(nextProps)比较子组件前后两次属性值的变化来执行某些方法。

5.还有一个用处,用在定时器

export default function App(props){
  const [count, setCount] = useState(0);

  const timerID = useRef();

  useEffect(() => {
    timerID.current = setInterval(()=>{
        ...
    }, 1000); 
  });

  useEffect(()=>{
     return () => clearInterval(timerID.current);
  });

  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
    </>
  );
}

容易理解。

二、useCallbackuseMemo怎么使用

1.useMemo和useCallback的定义

useMemo的签名:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。

官网上的解释:

关键词:高开销

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

关键词:纯函数

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

const WithMemo = function() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive:number = useMemo(() => {
        // 加入此处是一段大量运算的逻辑,实现了只有依赖项count变化时才会重新触发。达到了性能优化的目的
        console.log('执行了');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count])

    return <div>
        <h4>{count}-{val}-{expensive}</h4>
        <div>
            <Button onClick={() => setCount(count + 1)}>+c1</Button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

介绍useCallback之前,提个问题:如何避免函数式组件的不必要更新?

useCallback的签名:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一个 memoized 回调函数。 可以看出,两者的关系如下:

useCallback(fn, deps) === useMemo(() => fn, deps))

使用useMemo可以实现useCallback。

官网的解释:

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

解释一下:

useCallback第一个参数是一个函数,返回一个 memoized 回调函数。只有当第二个参数也就是依赖项发生变化的情况下,memoized 回调函数才会更新,否则将会指向同一块内存区域。 这种情况通常用在子组件中,比如把memoized 回调函数传给子组件后,子组件就可以通过shouldComponentUpdate或者React.memo来避免不必要的更新。

重点:useCallbackReact.memo必须结合使用,否则白用!

重点:useCallbackReact.memo必须结合使用,否则白用!

重点:useCallbackReact.memo必须结合使用,否则白用!

重点:useCallbackReact.memo必须结合使用,否则白用!

我们看一下官网的案例:

const UseRefDemof = () => {
    const [text, updateText] = useState('初始值');
    const textRef = useRef<string | null>();

    useEffect(() => {
      textRef.current = text; 
    });

    const handleSubmit = useCallback(() => {
      const currentText = textRef.current; 
      alert(currentText);
    }, [textRef]); 

    return (
      <>
        <span>父组件</span>
        <Input value={text} onChange={e => updateText(e.target.value)} />
        <ExpensiveTree onSubmit={handleSubmit} />
      </>
    );
}
interface Eprops {
    onSubmit: any
}
const ExpensiveTree:SFC<Eprops>  = React.memo(({onSubmit}) => {
    console.log('子组件渲染');
    return (
        <div>
            <span>子组件</span>
            <Button onClick={onSubmit}>点击弹出</Button>
        </div>
    )
})

配合使用了useCallback和React.memo,父组件的渲染就不会导致子组件无缘无故渲染,因为子组件的属性onSubmit方法的引用一直是同一个,不会随着父组件的re-render而发生变化,这就解决了一个很普遍的问题:

如何阻止一个无状态组件无必要的渲染。

当然,上面代码中handleSubmit函数如果写成下面这种就不行了,为什么就不行了呢,看到这里你肯定知道了,也就不赘述了。

const handleSubmit = useCallback(() => {
  alert(text);
}, [text]);

编辑于 2020-03-27 10:44