useRef
useRef主要的功能就是。
- 帮助我们获取到DOM元素或者组件实例
- 保存在组件生命周期内不会变化的值
如下:
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );}这个创建ref的过程是这样的
- 组件初始化,执行到useRef,初始化了一个参数null
- 这时候触发render了,这个过程实现了一个ref的挂载,从
null到相对应的组件实例或者DOM挂载,相当于对ref.current的赋值了,这个过程会有一个ref的数据变化
两个特点
- 每次组件重新渲染useRef的返回值都是同一个(引用不变)
ref.current发生变化的时候,不会触发组件的重新渲染,区别于其他的hooks
因此引出两个场景
1.不要单独拿ref作为依赖项
useEffect(()=>{ ....},[ref]);
useEffect(()=>{ ...}, []);上面两者相当于是一样的了,因为ref始终是同一个引用。
2.手动调用自己的ref挂载函数
这里我们需要通过callback ref的形式去挂载
function MeasureExample() { const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []);
return ( <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> );}注意:这里的ref挂载针对的是DOM元素,如果是要拿组件的ref,则往下看
forwardRef
我们用forwardRef包裹函数式组件,见下例
const Parent = () => { const childRef = useRef(null);
useEffect(() => { ref.current.focus(); }, []);
return ( <> <Child ref={ref} /> </> );};const Child = forwardRef((props, ref) => { return <input type="text" name="child" ref={ref} />;});使用forwardRef包裹之后,函数式组件会获得被分配给自己的ref(作为第二个参数)。如果你没有使用forwardRef而直接去ref的话,React会报错。
useImperativeHandle
上面forwardRef的例子中,Parent中的ref拿到了Child组件的完整实例,它不但可以使用focus方法,还可以使用其它所有的DOM方法,比如blur,style。这种方式是不推荐的,我们需要严格的控制ref的权力,控制它所能调用到的方法。
所以我们要使用useImperativeHandle来限制暴露给父组件的方法。
const Parent = () => { const childRef = useRef(null);
useEffect(() => { // 这里只能调用到focus方法 ref.current.focus(); }, []);
return ( <> <Child ref={ref} /> </> );};const Child = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input type="text" name="child" ref={inputRef} />;});这样子,我们就可以手动控制需要暴露给父组件的方法。
应用
获取上一次的值
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current;}这个hooks返回出来的值,在渲染的过程中,总是会显示上一次的值。我们来解析一下这个函数的运行步骤。 假设上例中ref的初始值传入的value是0,每次数据更新传入的都是递增的数据,比如1,2,3。
- 初始化,ref.current = 0,渲染出来
- 数据变化,
value传入1, 因为useEffect会在渲染完毕之后才执行,所以这次的渲染过程中,这个为1的value值不会赋值给ref.current。渲染出来的还是上一个值0,渲染完毕了,ref.current变为1。但是ref.current变化不会触发组件的重新渲染,所以需要等到下次的渲染才能显示到页面上。 - 如此往复,渲染的就总是上一次的值。
使用useRef来保存不需要变化的值
因为useRef的返回值在组件的每次redner之后都是同一个,所以它可以用来保存一些在组件整个生命周期都不需要变化的值。最常见的就是定时器的清除场景。
刚开始在React里写定时器,你可能会这样写
const App = () => { let timer;
useEffect(() => { timer = setInterval(() => { console.log('触发了'); }, 1000); },[]);
const clearTimer = () => { clearInterval(timer); }
return ( <> <Button onClick={clearTimer}>停止</Button> </>)}但是上面这个写法有个巨大的问题,如果这个App组件里有state变化或者他的父组件重新render等原因导致这个App组件重新render的时候,我们会发现,点击按钮停止,定时器依然会不断的在控制台打印,定时器清除事件无效了。
为什么呢?因为组件重新渲染之后,这里的timer以及clearTimer 方法都会重新创建,timer已经不是定时器的变量了。
所以对于定时器,我们都会使用useRef来定义变量。
const App = () => { const timer = useRef();
useEffect(() => { timer.current = setInterval(() => { console.log('触发了'); }, 1000); },[]);
const clearTimer = () => { clearInterval(timer.current); }
return ( <> <Button onClick={clearTimer}>停止</Button> </>)}实现深度比较useEffect
普通的useEffect只是一个浅比较的方法,如果我们依赖的state是一个对象,组件重新渲染,这个state对象的值没变,但是内存引用地址变化了,一样会触发useEffect的重新渲染。
const createObj = () => ({ name: 'zouwowo'});useEffect(() => { // 这个方法会无限循环}, [createObj()]);我们来使用useRef实现一个深度依赖对比的useDeepEffect
import equal from 'fast-deep-equal';export useDeepEffect = (callback, deps) => { const emitEffect = useRef(0); const prevDeps = useRef(deps); if (!equal(prevDeps.current, deps)) { // 当深比较不相等的时候,修改emitEffect.current的值,触发下面的useEffect更新 emitEffect.current++; } prevDeps.current = deps; return useEffect(callback, [emitEffect.current]);}