1388 字
7 分钟
useRef能做啥?

useRef#

useRef主要的功能就是。

  1. 帮助我们获取到DOM元素或者组件实例
  2. 保存在组件生命周期内不会变化的值

如下:

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的过程是这样的

  1. 组件初始化,执行到useRef,初始化了一个参数null
  2. 这时候触发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]);
}
useRef能做啥?
https://nollieleo.github.io/posts/useref能做啥/
作者
翁先森
发布于
2021-08-03
许可协议
CC BY-NC-SA 4.0