Open lmk123 opened 2 years ago
今天解决一个 bug 的时候发现了 ref 的两种赋值方式是有区别的。
先说说我这里所说的两种方式具体是指什么。
第一种:使用 useRef():
useRef()
function MyInput1(props) { const inputRef = useRef(null) useEffect(() => { inputRef.current?.focus() }, []) return <input ref={inputRef} /> }
第二种:使用 useState():
useState()
function MyInput2(props) { const [inputEle, setInputEle] = useState(null) useEffect(() => { inputEle?.focus() }, [inputEle]) return <input ref={setInputEle} /> }
这两种方式从效果上看是一样的:组件都会获取到焦点。我平时倾向于使用第二种,因为第二种的方式可以让我在 useEffect 里检测到 testEle 的变化,但是今天遇到的一个 bug 恰恰是由于这种方式引起的。
useEffect
testEle
现在我们在第二种方式的基础上使用 useImperativeHandle() 抛出一个 focus() 方法:
useImperativeHandle()
focus()
const MyInput2 = forwardRef(function MyInput2(props, ref) { const [inputEle, setInputEle] = useState(null) useImperativeHandle(ref, () => ({ focus() { inputEle?.focus() } }), [inputEle]) return <input ref={setInputEle} /> })
然后我们在父组件里调用它的方法:
function Parent() { const myInput2Ref = useRef(null) useEffect(() => { myInput2Ref.current?.focus() }, []) return <MyInput2 ref={myInput2Ref} /> }
按照预期,输入框应该获得焦点,但是实际情况是——没有。
但是如果我们用第一种方式:
function MyInput1(props) { const inputRef = useRef(null) useImperativeHandle(ref, () => ({ focus() { inputRef.current?.focus() } }), []) return <input ref={inputRef} /> }
就可以按照预期获取到焦点!
其实我们只需要把 <MyInput1 /> 和 <MyInput2 /> 里调用 focus() 前的问号 ? 去掉就知道原因了。
<MyInput1 />
<MyInput2 />
?
为什么我调用 focus() 前会加问号? 因为 TypeScript 推导出 inputRef.current 和 inputEle 可能是 null。
inputRef.current
inputEle
null
在 <MyInput1 /> 中,即使把问号去掉,代码也是能正常运行的,也就是说,使用 useRef() 赋值时,在运行 useEffect() 时 inputRef.current 已经不是 null 了。
useEffect()
在 <MyInput2 /> 中,把问号去掉之后,代码就报错了,因为 effect 第一次运行时 inputEle 是 null。
在这之前,首先得了解一下 React Hooks 的运行时机。强烈推荐阅读《useEffect 完整指南》(我在遇到这个问题之后又去读了两遍 :joy:),这里先简单阐述一下。
在第一种方式中,运行的过程是这样的:
<input />
在第二种方式中,运行的过程是这样的:
setState()
把 useEffect() 换作 useImperativeHandle() 来思考也是一样的:当父组件第一次调用 myInput2Ref.current.focus() 的时候,<MyInput2 /> 里的 inputEle 仍然是 null,只有当 <MyInput2 /> 第二次渲染完成后,调用时的 inputEle 才是 DOM 元素——然而我们在父组件里并不能判断出 <MyInput2 /> 有没有按照我们的预期渲染完成,我们也不应该依靠子组件的渲染状态来决定父组件里代码的调用时机。
myInput2Ref.current.focus()
我们都知道 useState() 的状态变更会触发下一次渲染,但由于 useEffect() 的存在,总是会忽略掉这件事,从而导致代码没有按照预期执行。
虽然已经用了一段时间 Hooks 了,但仍然不太习惯,潜意识里总是按照 Class 组件那一套来“模拟”它的运行过程,没有再进一步思考状态变更的后果。
今天解决一个 bug 的时候发现了 ref 的两种赋值方式是有区别的。
先说说我这里所说的两种方式具体是指什么。
第一种:使用
useRef()
:第二种:使用
useState()
:这两种方式从效果上看是一样的:组件都会获取到焦点。我平时倾向于使用第二种,因为第二种的方式可以让我在
useEffect
里检测到testEle
的变化,但是今天遇到的一个 bug 恰恰是由于这种方式引起的。现在我们在第二种方式的基础上使用
useImperativeHandle()
抛出一个focus()
方法:然后我们在父组件里调用它的方法:
按照预期,输入框应该获得焦点,但是实际情况是——没有。
但是如果我们用第一种方式:
就可以按照预期获取到焦点!
问题出在哪里?
其实我们只需要把
<MyInput1 />
和<MyInput2 />
里调用focus()
前的问号?
去掉就知道原因了。在
<MyInput1 />
中,即使把问号去掉,代码也是能正常运行的,也就是说,使用useRef()
赋值时,在运行useEffect()
时inputRef.current
已经不是null
了。在
<MyInput2 />
中,把问号去掉之后,代码就报错了,因为 effect 第一次运行时inputEle
是null
。为什么这两种方式会有这种差异?
在这之前,首先得了解一下 React Hooks 的运行时机。强烈推荐阅读《useEffect 完整指南》(我在遇到这个问题之后又去读了两遍 :joy:),这里先简单阐述一下。
在第一种方式中,运行的过程是这样的:
<input />
的 UI 并交给浏览器渲染在网页上inputRef.current
设置成了<input />
useEffect()
,此时inputRef.current
已经是 DOM 元素了在第二种方式中,运行的过程是这样的:
<input />
的 UI 并交给浏览器渲染在网页上setState()
更新了inputEle
这个状态,但是此次渲染已经完成,这个状态的变更将触发下一次渲染useEffect()
,此时inputEle
仍然是初始值null
inputEle
状态由null
变成了 DOM 节点,所以 React 重新生成了 UIuseEffect()
,此时inputEle
才是 DOM 元素而不是null
把
useEffect()
换作useImperativeHandle()
来思考也是一样的:当父组件第一次调用myInput2Ref.current.focus()
的时候,<MyInput2 />
里的inputEle
仍然是null
,只有当<MyInput2 />
第二次渲染完成后,调用时的inputEle
才是 DOM 元素——然而我们在父组件里并不能判断出<MyInput2 />
有没有按照我们的预期渲染完成,我们也不应该依靠子组件的渲染状态来决定父组件里代码的调用时机。总结
我们都知道
useState()
的状态变更会触发下一次渲染,但由于useEffect()
的存在,总是会忽略掉这件事,从而导致代码没有按照预期执行。虽然已经用了一段时间 Hooks 了,但仍然不太习惯,潜意识里总是按照 Class 组件那一套来“模拟”它的运行过程,没有再进一步思考状态变更的后果。