关于react.js:浅析React中的Ref

40次阅读

共计 6324 个字符,预计需要花费 16 分钟才能阅读完成。

引言

置信大部分同学对 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 创立的 refcurrent属性始终都为 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 底层解决的,咱们在组件外面是无奈承受到的,例如keyref

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 的形式传递 keyref给组件是有效的。

那如何传递 refkey给子组件,既然 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?

假如这样一种场景,你的组件中须要援用内部库提供的组件,例如 antdfusion 这种组件库提供的组件,而且你须要传递 ref 给这个组件,因为援用的内部组件是不可能晓得你会用什么样的别名属性来传递 ref,所以这个时候只能应用forwardRef 来进行传递 ref,因为这个是react 规定用来传递 ref 值的属性名。

跨层级传递 Ref

场景:须要把 refGrandFather组件传递到 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 的音讯啦~~"} />;
}

控制台打印后果

只管咱们传递上来的 refcurrent为字符串属性,然而咱们通过在子组件外面批改 current 属性,进而获取到了子组件外面 buttoninputdom 节点

下面的代码其实能够把 useEffect 更改成为 **useImperativeHandle**,这是react 提供的 hook,用来配合forwardRef 来进行应用,能够。

**useImperativeHandle**接管三个参数:

  1. 通过 forwardRef 传递过去的ref
  2. 处理函数,其返回值裸露给父组件的 ref 对象
  3. 依赖项

    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存储的值在发生变化后是不会引起整个组件的从新渲染的。

正文完
 0