引言
置信大部分同学对 ref 的认知还处于获取 DOM
节点和组件实例层面上,实际上除了这个性能,还有其它小技巧能够应用,这篇文章将具体地介绍 ref
的创立和应用,相干代码会以函数组件为主。
创立 Ref
字符串类型 Ref
class App extends React.Component {render() {console.log("this", this);
return (
<>
<div ref="dom">hello world</div>
<Children ref="children" />
</>
);
}
}
class Children extends React.Component {render() {return <div>china</div>;}
}
打印后果
无论是对于实在 DOM
,还是类组件,通过字符串创立的ref
会绑定在 this.refs
对象上。
函数类型 Ref
class App extends React.Component {
dom = null
children = null
render() {componentDidMount() {console.log(this.dom, this.childrenDom);
}
return (
<>
<div ref={(node) => this.dom = node}>hello world</div>
<Children ref={(node) => this.childrenDom = node} />
</>
);
}
}
class Children extends React.Component {render() {return <div>china</div>;}
}
打印后果
对象类型 Ref
createRef 创立 Ref
创建对象类型的 Ref
对于类组件和函数组件的创立形式是不一样的,在类组件中创立 Ref
对象须要通过 createRef
函数来进行创立,这个函数的实现其实非常简单
function createRef() {
return {current: null}
}
从下面的代码来看,就是创立并返回带有 current
属性的对象。
留神:不要在函数组件外面应用 createRef 函数
举个例子来看下为什么在函数组件外面不能应用 createRef 函数来创立Ref
function Counter() {const ref1 = React.useRef(0);
const ref2 = React.createRef();
const [count, setCount] = React.useState(0);
console.log(ref1, ref2);
return (
<>
<div class="counter-area">
<div class="count1">count1: {ref1.current}</div>
<div class="count2">count2: {ref2.current}</div>
</div>
<button
onClick={() => {ref1.current = (ref1.current || 0) + 1;
ref2.current = (ref2.current || 0) + 1;
setCount(count + 1);
}}
>
点我 ++
</button>
</>
);
}
一起看下打印后果
发现 count2
的右侧始终没有打印,依据控制台外面打印的数据不难发现由 createRef
创立的 ref
的current
属性始终都为 null
,所以count2
右侧没有数据。
函数组件更新 UI 视图实际上会将整个函数从新执行一遍,那么 createRef
函数在组件每次渲染的时候也会从新调用,生成初始状态的 ref
,对应的current
属性为null
。
useRef 创立 Ref
React
提供了内置的 useRef
来生成 ref
对象。
function Counter() {const ref = React.useRef(null);
React.useEffect(() => {console.log(ref.current);
}, []);
return <div ref={ref}>hello world</div>;
}
下面有说过在函数组件外面通过 createRef
创立 ref
会有问题,那通过 useRef
这个函数生成的 ref
会不会有上述的问题?
答案是不会,在类组件外面能够把 ref
存储到实例对象下来,然而函数组件并没有实例的说法,然而函数组件有对应的 Fiber
对象,只有组件没有销毁,那么 fiber
对象也不会销毁,将 useRef
产生的 ref
挂载到对应的 fiber
对象上,那么 ref
就不会在函数每次从新执行时被重置掉。
Ref 的高阶用法
Ref 转发
咱们在通过 Props 的形式向组件传递信息时,某些特定的属性是会被 React
底层解决的,咱们在组件外面是无奈承受到的,例如key
、ref
function Counter(props) {const { key, ref} = props;
console.log("props", props);
return (
<>
<div class="key">key: {key}</div>
<div class="ref">ref: {ref.current}</div>
</>
);
}
function App() {const ref = useRef(null);
return <Counter key={"hello"} ref={ref} />;
}
控制台中打印信息如下,能够理解到通过 props
的形式传递 key
和ref
给组件是有效的。
那如何传递 ref
和key
给子组件,既然 react
不容许,那么咱们换个身份,暗度陈仓。
function Counter(props) {const { pkey, pref} = props;
console.log("props", props);
return (
<>
<div class="key">key: {pkey}</div>
<div class="ref">ref: {pref.current}</div>
</>
);
}
function App() {const ref = useRef(null);
return <Counter pkey={"hello"} pref={ref} />;
}
打印信息如下
通过别名的形式能够传递 ref,那么为什么还须要 forwardRef 来传递 Ref?
假如这样一种场景,你的组件中须要援用内部库提供的组件,例如 antd
、fusion
这种组件库提供的组件,而且你须要传递 ref
给这个组件,因为援用的内部组件是不可能晓得你会用什么样的别名属性来传递 ref
,所以这个时候只能应用forwardRef
来进行传递 ref
,因为这个是react
规定用来传递 ref
值的属性名。
跨层级传递 Ref
场景:须要把 ref
从GrandFather
组件传递到 Son
组件,在 Son
组件外面展现 ref
存储的信息并获取 Son
组件外面的 dom
节点。
const Son = (props) => {const { msg, grandFatherRef} = props;
return (
<>
<div class="msg">msg: {msg}</div>
<div class="ref" ref={grandFatherRef}>
ref: {grandFatherRef.current}
</div>
</>
);
};
const Father = forwardRef((props, ref) => {return <Son grandFatherRef={ref} {...props} />;
});
function GrandFather() {const ref = useRef("我是来自 GrandFather 的 ref 啦~~");
useEffect(() => console.log(ref.current), []);
return <Father ref={ref} msg={"我是来自 GrandFather 的音讯啦~~"} />;
}
页面展现状况
控制台打印后果
下面的代码就是通过别名配合 forwardRef
来转发 ref
,这种转发ref
的形式,是十分常见的用法。通过别名的形式传递 ref
和通过 forwardRef
传递 ref
的形式其实没有太大的差异,最实质的区别就是通过别名的形式须要传递方和接管方人为地都约定好属性名,而通过 forwardRef
的形式是 react
外面约定了传递的 ref
属性名。
合并转发 Ref
咱们从父组件传递上来的 ref 不仅仅能够用来展现获取某个 dom
节点,还能够在子组件外面更改 ref
的信息,获取子组件外面其它信息。
场景:咱们须要获取子组件外面 input 和 button 这两个 dom
节点
const Son = forwardRef((props, ref) => {console.log("props", props);
const sonRef = useRef({});
useEffect(() => {
ref.current = {...sonRef.current,};
return () => ref.current = {}
}, []);
return (
<>
<button ref={(button) => Object.assign(sonRef.current, { button})}>
点我
</button>
<input
type="text"
ref={(input) => Object.assign(sonRef.current, { input})}
/>
</>
);
});
function Father() {const ref = useRef("我是来自 Father 的 ref 啦~~");
useEffect(() => console.log(ref.current), []);
return <Son ref={ref} msg={"我是来自 Father 的音讯啦~~"} />;
}
控制台打印后果
只管咱们传递上来的 ref
的current
为字符串属性,然而咱们通过在子组件外面批改 current
属性,进而获取到了子组件外面 button
和input
的 dom
节点
下面的代码其实能够把 useEffect
更改成为 **useImperativeHandle**
,这是react
提供的 hook
,用来配合forwardRef
来进行应用,能够。
**useImperativeHandle**
接管三个参数:
- 通过
forwardRef
传递过去的ref
- 处理函数,其返回值裸露给父组件的
ref
对象 -
依赖项
const Son = forwardRef((props, ref) => {console.log("props", props); const sonRef = useRef({}); useImperativeHandle( ref, () => ({...sonRef.current,}), []); return ( <> <button ref={(button) => Object.assign(sonRef.current, { button})}> 点我 </button> <input type="text" ref={(input) => Object.assign(sonRef.current, { input})} /> </> ); }); function Father() {const ref = useRef("我是来自 Father 的 ref 啦~~"); useEffect(() => console.log(ref.current), []); return <Son ref={ref} msg={"我是来自 Father 的音讯啦~~"} />; }
高阶组件转发 Ref
在应用高阶组件包裹一个原始组件的时候,因为高阶组件会返回一个新的组件,如果不进行 ref 转发,从下层组件传递下来的 ref 会指向这个新的组件
function HOC(Comp) {function Wrapper(props) {const { forwardedRef, ...restProps} = props; return <Comp ref={forwardedRef} {...restProps} />; } return forwardRef((props, ref) => (<Wrapper forwardedRef={ref} {...props} /> )); } const Son = forwardRef((props, ref) => {return <div ref={ref}>hello world</div>; }); const NewSon = HOC(Son); function Father() {const ref = useRef("我是来自 Father 的 ref 啦~~"); useEffect(() => console.log(ref.current), []); return <NewSon ref={ref} msg={"我是来自 Father 的音讯啦~~"} />; }
留神:第 12 行处减少
forwardRef
转发是因为函数组件无奈间接接管ref
属性
控制台打印后果
Ref 实现组件间的通信
子组件能够通过 props
获取和扭转父组件外面的 state
,父组件也能够通过ref
来获取和扭转子组件外面的state
,实现了父子组件之间的双向通信,这其实在肯定水平上突破了 react 单向数据传递的准则。
const Son = forwardRef((props, ref) => {const { toFather} = props;
const [fatherMsg, setFatherMsg] = useState('');
const [sonMsg, setSonMsg] = useState('');
useImperativeHandle(
ref,
() => ({fatherSay: setFatherMsg}),
[]);
return (
<div className="son-box">
<div className="father-say"> 父组件对我说: {fatherMsg}</div>
<div className="son-say">
我对父组件说:
<input
type="text"
value={sonMsg}
onChange={(e) => setSonMsg(e.target.value)}
/>
<button onClick={() => toFather(sonMsg)}>to father</button>
</div>
</div>
);
});
function Father() {const [fatherMsg, setFatherMsg] = useState('');
const [sonMsg, setSonMsg] = useState('');
const sonRef = useRef(null);
return (
<div className="father-box">
<div className="son-say"> 子组件对我说: {sonMsg}</div>
<div className="father-say">
对子组件说:
<input
type="text"
value={fatherMsg}
onChange={(e) => setFatherMsg(e.target.value)}
/>
<button onClick={() => sonRef.current.fatherSay(fatherMsg)}>
to son
</button>
</div>
<Son ref={sonRef} toFather={setSonMsg} />
</div>
);
}
Ref 和 State 的抉择
咱们在平时的学习和工作中,在应用函数组件的时候,经常会应用 useState
来生成 state
,每次state
扭转都会引发整个函数组件从新渲染,然而有些 state
的扭转不须要更新视图,那么咱们能够思考应用 ref 来存储,ref
存储的值在发生变化后是不会引起整个组件的从新渲染的。