Ref的理解与使用,不单单停留在用获取真实 DOM 元素和获取类组件实例层面上,应该分成两个部分去分析,第一个部分是 Ref 对象的创建,第二个部分是 React 本身对Ref的处理。两者不要混为一谈,所谓 Ref 对象的创建,就是通过 React.createRef 或者 React.useRef 来创建一个 Ref 原始对象。而 React 对 Ref 处理,主要指的是对于标签中 ref 属性,React 是如何处理以及 React 转发 Ref 。
Ref对象创建
什么是 ref 对象,所谓 ref 对象就是用字符串,函数,create/useREf三种方法获取创建创建出来的对象,一个标准的 ref 对象应该是如下的样子:
{
current:null , // current指向ref对象获取到的实际内容,可以是dom元素,组件实例,或者其它。
}
hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来
React对Ref属性的处理-标记ref
首先明确一个问题是 DOM 元素和组件实例 必须用 ref 对象获取吗?答案是否定的,React 类组件提供了多种方法获取 DOM 元素和组件实例,说白了就是 React 对标签里面 ref 属性的处理逻辑多样化。
当你打印Ref时,第一次打印为 null ,第二次才有值 ,为什么会这样呢?
ref 执行时机和处理逻辑
对于整个 Ref 的处理, ref 就是用来获取真实的 DOM 以及组件实例的,所以需要 commit 阶段处理。对于 Ref 处理函数,React 底层用两个方法处理:commitDetachRef 和 commitAttachRef ,上述两次 console.log 一次为 null,一次为div 就是分别调用了上述的方法。这两次正正好好,一次在 DOM 更新之前,一次在 DOM 更新之后。
三个阶段:
-
一次更新中,在 commit 的 mutation 阶段, 执行commitDetachRef,commitDetachRef 会清空之前ref值,使其重置为 null。
-
第二阶段:DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。
-
第三阶段:layout 阶段,在更新真实元素节点之后,此时需要更新 ref 。主要判断 ref 获取的是组件还是 DOM 元素标签,如果 DOM 元素,就会获取更新之后最新的 DOM 元素。上面流程中讲了三种获取 ref 的方式。 如果是字符串 ref="node" (当 ref 属性是一个字符串的时候,React 会自动绑定一个函数,用来处理 ref 逻辑)或是 函数式
ref={(node)=> this.node = node }会执行 ref 函数,重置新的 ref
Ref 的处理特性
只有在 ref 更新的时候,才会调用如上方法更新 ref ,究其原因还要从如上两个方法的执行时期说起在 fiber 初始化,更新这两种情况给 effectTag 标记 Ref,只有标记了 Ref tag 才会有后续的 commitAttachRef 和 commitDetachRef 流程。( current 为当前调和的 fiber 节点 )
高阶用法
- forwardRef 合并、转发跨层级 Ref
- 函数组件缓存数据,
- 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
- 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。
- 实现非控制渲染的组件通信(forwardRef + useImperativeHandle),如果有种场景不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法,这种情况通常发生在一些数据层托管的组件上,比如
<Form/>表单
function Son (props,ref) {
const inputRef = useRef(null)
const [ inputValue , setInputValue ] = useState('')
useImperativeHandle(ref,()=>{
const handleRefs = {
onFocus(){ /* 声明方法用于聚焦input框 */
inputRef.current.focus()
},
onChangeValue(value){ /* 声明方法用于改变input的值 */
setInputValue(value)
}
}
return handleRefs
},[])
return <div>
<input placeholder="请输入内容" ref={inputRef} value={inputValue} />
</div>
}
const ForwarSon = forwardRef(Son)
// 父组件
class Index extends React.Component{
cur = null
handerClick(){
const { onFocus , onChangeValue } =this.cur
onFocus() // 让子组件的输入框获取焦点
onChangeValue('let us learn React!') // 让子组件input
}
render(){
return <div style={{ marginTop:'50px' }} >
<ForwarSon ref={cur => (this.cur = cur)} />
<button onClick={this.handerClick.bind(this)} >操控子组件</button>
</div>
}
}