useRef详细总结

吐槽君 分类:javascript

本文全方面介绍useRef,分别从什么是useRef、为什么使用useRef以及useRef三兄弟(指useRef、forwardRef以及useImperativeHandle)来讲解。本文所有示例

一、什么是useRef

const refContainer = useRef(initialValue);
 
  • 返回一个可变的ref对象,该对象只有个current属性,初始值为传入的参数(initialValue)。
  • 返回的ref对象在组件的整个生命周期内保持不变
  • 当更新current值时并不会re-render,这是与useState不同的地方
  • 更新useRef是side effect(副作用),所以一般写在useEffect或event handler里
  • useRef类似于类组件的this

简单示例

需求: 点击 button 的时候 选中文本框
实现:

import React, { MutableRefObject, useRef } from 'react'
const TextInputWithFocusButton: React.FC = () => {
   const inputEl: MutableRefObject<any> = useRef(null)
   const handleFocus = () => {
   	// `current` 指向已挂载到 DOM 上的文本输入元素
   	inputEl.current.focus()
   }
   return (
   	<p>
   		<input ref={inputEl} type="text" />
   		<button onClick={handleFocus}>Focus the input</button>
   	</p>
   )
}
export default TextInputWithFocusButton
 

通过ref获取组件内的DOM
小结: 通过useRef定义个inputEl变量,在input 元素上定义ref={inputEl},这样通过inputEl.current就可以获取到input Dom 元素,选中则调用下focus函数即可

见示例库里的domUseRef.tsx

二、为什么使用useRef

需求: 跨渲染取到状态值

只用useState:

实现:

import React, { useState } from "react";
const LikeButton: React.FC = () => {
	const [like, setLike] = useState(0)
	function handleAlertClick() {
		setTimeout(() => {
			alert(`you clicked on ${like}`) 
			//形成闭包,所以弹出来的是当时触发函数时的like值
		}, 3000)
	}
	return (
		<>
			<button onClick={() => setLike(like + 1)}>{like}赞</button>
			<button onClick={handleAlertClick}>Alert</button>
		</>
	)
}
export default LikeButton
 

现象
在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 6, 而非 10.
由于闭包,函数里的变量值为调用函数时对应的快照like值
为什么不是界面上like的实时状态?
当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。

小结:
不同渲染之间无法共享state状态值

见示例库里的likeButton

采用全局变量

在组件前定义一个类似 global 的变量
实现:

import React from 'react'
let like = 0
const LikeButton: React.FC = () => {
	function handleAlertClick() {
		setTimeout(() => {
			alert(`you clicked on ${like}`)
		}, 3000)
	}
	return (
		<>
			<button
				onClick={() => {
					like = ++like
				}}
			>
				{like}赞
			</button>
			<button onClick={handleAlertClick}>Alert</button>
		</>
	)
}
export default LikeButton
 

现象
在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为10.
采用global变量
小结
由于like变量是定义在组件外,所以不同渲染间是可以共用该变量,所以3秒后获取的like值就是最新的like值
该示例同时也说明,非state变量不会引起重新render

代码见示例库里的globalFix1

采用useRef

实现:

import React, { useRef } from 'react'
const LikeButton: React.FC = () => {
	// 定义一个实例变量
	let like = useRef(0)
	function handleAlertClick() {
		setTimeout(() => {
			alert(`you clicked on ${like.current}`)
		}, 3000)
	}
	return (
		<>
			<button
				onClick={() => {
					like.current = like.current + 1
				}}
			>
				{like.current}赞
			</button>
			<button onClick={handleAlertClick}>Alert</button>
		</>
	)
}
export default LikeButton
 

现象
在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为10.跟上面使用全局变量现象一致
采用useRef
小结
采用useRef,作为组件实例的变量,保证获取到的数据肯定是最新的。

该示例同时也说明,ref更改不会re-render

见示例库里的useRefFix2

useRef与全局变量的区别

上面两个法子都可以解决问题,那两个有什么区别呢
实现

import React, { useRef } from 'react'
// 定义一个全局变量
let like = 0
const LikeButton: React.FC = () => {
	let likeRef = useRef(0)
	function handleAlertClick() {
		setTimeout(() => {
			alert(`you clicked on ${like}`)
			alert(`you clicked on ${likeRef.current}`)
		}, 3000)
	}
	return (
		<p>
			<button
				onClick={() => {
					like = ++like
					likeRef.current = likeRef.current + 1
				}}
			>
				点赞
			</button>
			<button onClick={handleAlertClick}>Alert</button>
		</p>
	)
}
export default LikeButton
 

两种法子区别
现象
三个按钮依次点下,点击任意alert,最先弹出的是3(表示global变量取的最后渲染组件的值),后弹出1(表示ref属于组件自己,互相不影响)

小结

  • useRef 是定义在实例基础上的,如果代码中有多个相同的组件,每个组件的 ref 只跟组件本身有关,跟其他组件的 ref 没有关系。
  • 组件前定义的 global 变量,是属于全局的。如果代码中有多个相同的组件,那这个 global 变量在全局是同一个,他们会互相影响。

代码见示例库里的differenceFix1and2.tsx

三、useRef与createRef的区别

在一个组件的正常的生命周期中可以大致分为3个阶段:

  1. 从创建组件到挂载到DOM阶段。初始化props以及state, 根据state与props来构建DOM
  2. 组件依赖的props以及state状态发生变更,触发更新
  3. 销毁阶段

第一个阶段,useRef与createRef没有差别
第二个阶段,createRef每次都会返回个新的引用;而useRef不会随着组件的更新而重新创建
第三个阶段,两者都会销毁
实现:

import React, { useState, useRef, createRef } from 'react'
const RefDifference: React.FC = () => {
	let [renderIndex, setRenderIndex] = useState(1)
	let refFromUseRef = useRef<number>()
	let refFromCreateRef = createRef()
	console.info(refFromUseRef.current, 'refFromUseRef.current')
	console.info(refFromCreateRef.current, 'refFromCreateRef.current')
	if (!refFromUseRef.current) {
		refFromUseRef.current = renderIndex
	}

	if (!refFromCreateRef.current) {
		refFromCreateRef.current = renderIndex
	}
	return (
		<>
			<p>Current render index: {renderIndex}</p>
			<p>
				<b>refFromUseRef</b> value: {refFromUseRef.current}
			</p>
			<p>
				<b>refFromCreateRef</b> value:
				{refFromCreateRef.current}
			</p>

			<button onClick={() => setRenderIndex((prev) => prev + 1)}>
				Cause re-render
			</button>
		</>
	)
}
export default RefDifference
 

现象:
点击按钮时,从控制台可以看到refFromUseRef.current一直为1(因为refFromUseRef.current已经存在该引用),而refFromCreateRef.current却是undefined(因为createRef 每次渲染都会返回一个新的引用,所以if判断时为true,会被重新赋值,页面就会显示出新的值)
useRef与createRef
小结:
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

代码见示例库里的useRefAndCreateRef

以上讲的都是在当前组件使用ref的示例,后续接着说ref如何用在与子组件的交互上

四、获取子组件的属性或方法

需求: 调用子组件里的某个函数

自定义属性传入ref

实现:
父组件创建一个ref作为一个属性传入子组件。子组件根据内部方法的变化动态更改ref(useEffect)

import React, {
	MutableRefObject,
	useState,
	useEffect,
	useRef,
	useCallback
} from 'react'
interface IProps {
	//prettier-ignore
	label: string,
	cRef: MutableRefObject<any>
}
const ChildInput: React.FC<IProps> = (props) => {
	const { label, cRef } = props
	const [value, setValue] = useState('')
	const handleChange = (e: any) => {
		const value = e.target.value
		setValue(value)
	}
	const getValue = useCallback(() => {
		return value
	}, [value])
	useEffect(() => {
		if (cRef && cRef.current) {
			cRef.current.getValue = getValue
		}
	}, [getValue])
	return (
		<div>
			<span>{label}:</span>
			<input type="text" value={value} onChange={handleChange} />
		</div>
	)
}

const ParentCom: React.FC = (props: any) => {
	const childRef: MutableRefObject<any> = useRef({})
	const handleFocus = () => {
		const node = childRef.current
		alert(node.getValue())
	}
	return (
		<div>
			<ChildInput label={'名称'} cRef={childRef} />
			<button onClick={handleFocus}>focus</button>
		</div>
	)
}

export default ParentCom

 

现象:
父组件按钮点击时,通过调用getValue,获取到子组件input里的value值
ref作为属性传入子组件
小结:
不够优雅,尤其是自定义一个属性传入ref

见示例库里的childComponentRef.tsx

通过useImperativeHandle,配合forwardRef

forwardRef: 将ref父类的ref作为参数传入函数式组件中
示例:

React.forwardRef((props, ref) => {})  //创建一个React组件,
//这个组件将会接受到父级传递的ref属性,
//可以将父组件创建的ref挂到子组件的某个dom元素上,
//在父组件通过该ref就能获取到该dom元素
 
const FancyButton = React.forwardRef((props, ref) => (  
  <button ref={ref} className="FancyButton">    
    {props.children}
  </button>
));
// 可以直接获取到button的DOM节点
const ref = React.useRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
 

useImperativeHandle在函数式组件中,用于定义暴露给父组件的ref方法,用来限制子组件对外暴露的信息,只有useImperativeHandle第二个参数定义的属性跟方法可以在父组件能够获取到
主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多

useImperativeHandle(ref, createHandle, [deps]) // 第一个参数暴露哪个ref;第二个参数暴露什么信息
 
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
// 渲染 <FancyInput ref={inputRef} /> 的父组件
//可以调用 inputRef.current.focus()
 

实现:

import React, {
	MutableRefObject,
	useState,
	useImperativeHandle,
	useRef,
	forwardRef,
	useCallback
} from 'react'
interface IProps {
	label: string
}
let ChildInput = forwardRef((props: IProps, ref: any) => {
	const { label } = props
	const [value, setValue] = useState('')
	// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
	// 参数1: 父组件传递的ref属性
	// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
	useImperativeHandle(ref, () => ({
		getValue
	}))
	const handleChange = (e: any) => {
		const value = e.target.value
		setValue(value)
	}
	const getValue = useCallback(() => {
		return value
	}, [value])
	return (
		<div>
			<span>{label}:</span>
			<input type="text" value={value} onChange={handleChange} />
		</div>
	)
})
const ParentCom: React.FC = (props: any) => {
	const childRef: MutableRefObject<any> = useRef({})
	const handleFocus = () => {
		const node = childRef.current
		alert(node.getValue())
	}
	return (
		<div>
			<ChildInput label={'名称'} ref={childRef} />
			<button onClick={handleFocus}>focus</button>
		</div>
	)
}

export default ParentCom

 

现象:

只往外暴露getValue
小结:
React 16.3版本之前,是不能够在函数组件里定义ref属性,因为函数组件没有实例。在16.3版本之后,引入了React.forwardRef,通过该函数就可以将父组件的ref传入子组件,子组件可以将该ref绑定在任何DOM元素上,但这样会将整个子组件暴露给父组件。通过useImperativeHandle就可以限制要将子组件里的哪些属性跟方法暴露给父组件

见示例库里的childComponentRef2

五、总结

  1. useRef可以用来定义变量,这些变量更改之后不会引起页面重新渲染,比如分页获取数据时,存储页码。
  2. useRef也可以用来区分初始渲染还是更新(通过current有没值,具体见示例库里的didOrUpdate.tsx)
  3. 在DOM节点上定义ref属性,通过.current就可以获取到该DOM元素
  4. 通过forwardRef就可以给函数子组件传入ref属性。
  5. 使用useImperativeHandle用于定义暴露给父组件的ref方法

回复

我来回复
  • 暂无回复内容