React refs实例:正确使用子组件的功能
最近在抽离一个组件时,需要暴露一些方法给父组件使用,google 百度了一番无果,最后想到了 refs。可不太熟悉,于是又去查了遍官方文档,发现以前工作的时候写的一个全局弹窗的组件很傻,遂记录一下。 当时的业务需求:一个全局的支付弹窗,在不同的页面点击支付后打开弹窗。 写过的“笨”方法 最理想的使用方式就是 PayModal.show() 后直接展示。但是基于 react 的特性,一般做法都是传一个 visible props 上去,可组件挂载在全局 Layout 下面,没法儿在当前页面下控制。 于是乎: function PayModal(props) { const [visible, setVisible] = useState(false); /** * 直接在组件上赋值。反正只挂载一次。。。 */ PayModal.show = () => { setVisible(true); }; return ( <Modal visible={visible}> // ... </Modal> ); } 这样的写法显然有缺陷: 组件未挂载之前调用 .show() 将会报错仅限挂载一次 所以当时召集小伙伴们进行 code review 的时候就被“教怎么做了”,xxxxx。还有人提出通过 props 来暴露,大意是这样: function PayModal(props) { useEffect(() => { props.fns = { show: () => { setVisible(true); }, }; }, []); } // 使用时 class Page1 extends React.Component { constructor() { this.fns = {}; } render() { return ( <div> <PayModal fns={this.fns} /> </div> ); } } 这样的做法也可,但违背了 react 中自上而下数据的传递。你不知道哪个地方子组件修改你的数据,容易踩西瓜皮。 当时新同学推荐的做法是 ref + useImperativeHandle 👍 关于 Refs 官方文档上介绍地很清楚,也翻到过几遍,但是有时还是不太会用它。没有业务场景,就有时很难理解某些概念,为什么要这样?当理解学会后,就会想当然地使用,感觉还是业务驱动技术啊。 所以 ref 是个什么东西?以前看文档关注的重心,就知道到它能获取 dom,基本上替代了旧版 findDOMNode() 的用法: class Foo extends React.Component { constructor() { this.inputRef = React.createRef(); } componentDidMount() { this.inputRef.current; // dom 节点 this.inputRef.current.focus(); } render() { return ( <input ref={this.inputRef} /> ); } } 事实上它有 3 种情况(文档): ref 写在 HTML 元素上,.current 指向了该元素 dom 节点ref 写在 class 组件上,.current 指向了该组件实例无法在函数式组件上使用 ref,因为没有实例 此外 ref 其实也很简单,仅仅是一个纯对象,createRef() 也只是个工厂函数: // packages/react/src/ReactCreateRef.js // an immutable object with a single mutable value export function createRef() { const refObject = { current: null, }; if (__DEV__) { Object.seal(refObject); } return refObject; } 所以你甚至能这样写(不推荐): class Foo extends React.Component { constructor() { this.inputRef = { current: null }; } } 可能会有这样的疑问:为什么要用 this.inputRef.current 访问,直接 this.inputRef 访问不是更方便么? 这与 react 的性能优化有关,使用同一个对象(引用地址)能减少 react 的重复渲染。之后会写一篇优化相关的文章,具体介绍。 场景 回到场景,利用 ref 公开子组件上的方法,暴露给父组件使用。基于组件的写法分为 class 组件 与函数式。 class 组件暴露方法 基于 ref 的特性,无需额外操作即可访问子组件上的方法。例: class Parent { constructor() { this.ref = React.createRef(); } componentDidMount() { this.ref.current.sayHello(); } render() { return <Child ref={this.ref} />; } } // 子组件,实例化后上的 sayHello 可供调用 class Child { sayHello = () => { // ... } render() {} } 函数式组件暴露方法 得益于 hooks 的推行,函数式组件可复用能力大大增强。以前给函数式组件写 ref 时还得定义一个额外的 props 属性 wrapComponentRef 等等,现在官方提供了 API 直接操作。 此外便是通过 useImperativeHandle 重写 ref 以暴露方法。例: function Child(props, ref) { useImperativeHandle(ref, () => { // 重写。参考 antd 表单的做法 return { sayHello: () => { // ... }, }; }); return <div />; } // 转发 ref export default React.forwardRef(Child); 回顾:全局弹窗的解决方案 全局弹窗由于只挂载在整个应用的 Layout 下面,因此也没有办法通过 ref 来打开弹窗。。。 结合整个技术方案,数据流用的 redux,可以直接把 visible 放到全局的 store 里面,通过 action 触发: class Page1 extends React.Component { constructor(props){ super(props); this.state = {}; } handleClick = () => { this.props.dispatch({ type: OPEN_PAYMODAL, }); }; render() {} } export default connect()(Page1); 甚至还能用事件发布/订阅模式~~~有时的思维还是被什么最佳规范给局限住了。 结尾。被强行塞了一波知识 😫,zz...
JavaScript全屏阅读