Refs 转发

接上文,开始研究refs转发的原因是因为,antd4更新的form表单更改了3的数据获取和方法调用的方式,需要在使用前使用类似于 this.refForm = React.createRef() 来定义,获取的时候通过 this.refForm.current 的方式来使用form自带的函数和数据,于是对refs转发产生了兴趣。

在一开始接触ref的时候是在vue里面,再vue里面,通过ref来操作子组件的函数,通过emit来调用父组件函数:

<template>
    <div ref='child' value="test">
</template>

methods:{
    testRef:function () {
        console.log(this.$refs.child) //获取上文的div实例
    }
}

而在react中,基本实现类似,不过就是将构造refs参考实例进行了前置,需要先构造才能用,比如想要控制一个组件内部的某些真实dom,需要注意的是,这时候的ref并不是通过props传递下去到div的dom中,而是获取了RefButton组件的实例,具体实现如下:

/*
* @desc 先定义一个组件式的button,在内部我们定义了一个简单的click事件,待会再外层调用它
*/
import React, { Component } from "react";
import styles from "./styles.module.less";

class RefButton extends Component {
  constructor(props) {
    super(props);
    this.state = { test: "测试用数据"};
  }
  onClick = () => {
    const { test } = this.state;
    this.setState({ test: test + 1 });
  };
  render() {
    const { test } = this.state;
    return (
      <div className={styles.contain}>
        {test}
        ........
      </div>
    );
  }
}
export default RefButton;

/*
* @desc 父组件
*/
class Refs extends Component {
  constructor(props) {
    super(props);
    this.state = { test: "新改的数据" };
    this.ref = React.createRef(); //定义一个ref
  }

  onClick = () => {
    const { test } = this.state;
    const message = this.ref.current; //调用之前定义的ref
    message.setState({
      test: test + 1,
    });
    this.setState({
      test: test + 1,
    });
  };
  render() {
    return (
        <div className={styles.contain}>
          <RefButton ref={this.ref} />  //让定义的ref传递下去
          <button className={styles.test_button} onClick={() => this.onClick()}>
            测试ref按钮
          </button>
          <Parent />
        </div>
    );
  }
}

export default Refs;

PS:不能在函数组件中使用ref,因为函数组件没有实例,需要使用实例应当使用下面的hook方法

此外,还有比较新的hook的用法,我只写了一个简单的例子,想要看细节可以去看看《关于useRef的用法》,里面详细介绍了该hook的基本使用,demo如下:

/*
* @desc 定义了两个函数组件,parent和child
*/
import React, { useImperativeHandle, useRef, useState } from "react";

// 引入useState 、useRef 、 useImperativeHandle组件

// 子组件
function Child(props, ref) {
  const [number, setNumber] = useState(0);
  const funcref = useRef();
  // 通过使用useImperativeHandle来给父组件传递子组件方法,为受控组件的更新和维护进行数据传递
  useImperativeHandle(ref, () => ({
    // 定义了一个递增函数,父组件单击按钮触发子组件的addNumber函数,给input中的数加一
    addNumber(value = 1) {
      typeof value === "number" && setNumber(value + number);
    },
    getFocus() {
      funcref.current.focus();
    },
  }));
  return (
    <>
      <input type="text" ref={funcref} value={number}></input>
    </>
  );
}

// 用forwardRef包裹子组件,使得父组件的ref穿透传递下去
// 我这里只包了一层,理论上来说无论多少层,只要在内部包裹了
// 都是可以获取到ref的
const ForwardChild = React.forwardRef(Child); 

// 父组件
function Parent() {
  // let [number, setNumber] = useState(0);
  // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
  // 只有当它被赋给某个元素的 ref 属性时,才会有值
  // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
  // 那么父组件就可以操作子组件中的某个元素
  // 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
  // 所以就需要用到 forwardRef 进行转发
  const inputRef = useRef(); //{current:''}
  function getFocus() {
    inputRef.current.getFocus();
  }
  function addMenber() {
    inputRef.current.addNumber();
  }
  return (
    <>
      <div className={styles.test_parent}>
        测试函数组件
        <ForwardChild ref={inputRef} />
        <button onClick={addMenber}>+</button>
        <button onClick={getFocus}>获取焦点</button>
      </div>
    </>
  );
}

官方文档也说了,针对HOC(高阶组件),refs转发很有用,就比如我一开始说的antd 的form表单,但是他并不是把ref作为props传递下去了,他只是获取了高阶组件返回的组件的实例,如果想要在组件内部使用父组件ref的,则可以通过 React.forwardRef() 来将ref传递下去,该方法可以透传。

PS:需要注意如果想要让ref当作props传递下去,不能再使用ref作为变量名

PS:更多 refsDom 的操作在 Refs & DOM

Fragments

我的理解就是透明层,看起来有东西,其实都是骗系统的,让他以为这里有最外层包裹了,不会给你找麻烦,毕竟很多时候想要封装组件,必须有个最外层,而有的组件其实是并列的关系,多包一层,即使是空的 div 也是会出问题的,比如说常用的例子:

<table>
    <tr>
        <div>
            <td></td>
            <td></td>
        </div>
    </tr>
</table>

看起来中间那层div是没什么东西的,但是它就会让你的table无法渲染出来。这时候使用 <Fragment>{Child}</Fragment><React.Fragment>{Child}</React.Fragment> 对子组件进行包裹就可以避免这样的问题,此外React还提供该语法的简写 <>{Child}</> 这样的空标签也可以达到相同的目的。

高阶组件

高阶组件就是一种比较特别的函数,该函数以组件为参数,返回结果也是一个组件。主要是针对一些复用性较高,但是又需要有一定的自定义操作,比如整个组件的业务逻辑一致,区别在于传入的对象不一致或者是其中一个函数不一致,如果是普通的写法,要么在组件内加很多的判断,当这种判断不是一种两种,而是极多,带有不确定的情况的时候,就需要有一个类似于中间件的东西来处理现有组件,以实现相应的逻辑,于是就有了高阶组件,个人认为是 组件的中转函数 。如下是一个简单的HOC demo,具体的流程见注释和代码:

/*
* @desc 构造了一个简单的hoc函数,传入组件,然后接受一个函数,改变组件的内容
*/
function hocFunction(Comp, func) {
  class hocFunction extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data: func(),
      };
    }

    componentDidUpdate(prevProps) {
      console.log("Current props: ", this.props);
      console.log("Previous props: ", prevProps);
    }

    render() {
      // 过滤掉非此 HOC 额外的 props,且不要进行透传
      const { extraProp, ...passThroughProps } = this.props;

      // 将 props 注入到被包装的组件中。
      // 通常为 state 的值或者实例方法。
      const injectedProp = this.state.data;

      return <Comp injectedProp={injectedProp} {...passThroughProps} />;
    }
  };
  
   // 这是react进行的约定,因为容器组件也会显示在调试中,所以尽量都要给他命名
  hocFunction.displayName = `hocFunction(${getDisplayName(hocFunction)})`;
  
  return hocFunction;
}

function HocTest(props) {
  return (
    <>
      <p>
        {props.injectedProp}:test all the hoc will be change.
      </p>
    </>
  );
}

class Hoc extends Component {
  constructor(props) {
    super(props);
    this.state = {
      test: "",
    };
    this.LogTestHoc = hocFunction(HocTest, () => {
      return 20;
    });
  }
  changeProps = () => {
    const { test } = this.state;
    this.setState(
      {
        test: test !== "" ? "" : "这是一个测试用的数据",
      },
      () => {
        this.LogTestHoc = hocFunction(HocTest, () => {
          return test;
        });
      }
    );
  };
  render() {
    const { test } = this.state;
    const LOGHOC = this.LogTestHoc;
 
    return (
      <div className={styles.contain}>
        <ErrorBoundary>
          <LOGHOC test={test}></LOGHOC>
          <button id="buttonC" onClick={this.changeProps}>
            测试更新props
          </button>
        </ErrorBoundary>
      </div>
    );
  }
}

export default Hoc;

该demo在constructor中定义hoc,并且在单击更新按钮时触发hoc的改变,从而使得hoc包裹的HocTest组件发生变化。

PS: 不能在render种定义hoc,这会造成每次调用 render 函数都会创建一个新的hoc,这将导致子树每次渲染都会进行卸载,和重新挂载的操作!

与第三方库的协同

没怎么看

深入JSX

官网说的很清楚:

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖

主要内容就是将一些JSX中的一些禁忌和使用方法
1、不管你用没用,只要写了就一定要import引入到当前作用域
2、可以使用点语法来调用react自带的方法组件等,如 React.Fragment
3、如果你想使用一个组件,组件名必须大写,定义的时候没大写,使用的时候也要通过赋值给大写的变量
4、不能在使用组件的时候进行判断,变量等,必须在使用前就改变。如:

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
    // 错误!JSX 类型不能是一个表达式。
    return <components[props.storyType] story={props.story} />;
}
function Story(props) {
    const Comp = components[props.storyType]
    // 正确!
    return <Comp story={props.story} />;
}

5、可以在props中使用表达式:value={1+2+2+3}
6、字符串字面量可以直接作为props传递
7、props的默认值时true,比如:autoplayautoplay={true} 时等价的
8、可以通过展开运算符写入props ,比如:<Demo {...props} />
9、在JSX中字符串、子组件、函数都能成为子元素
10、可以有多个子元素:

<Comp>
    <First />
    <Second />
</Comp>

11、可以通过 this.props.children 来获取子元素,这在定义外层容器中十分有用;

//定义一个Comp的函数组件,在子组件外层包裹一个div
function Comp(props){
    const {children , ...other} = props
    return <div {...other}>{Children}</div>
}

性能优化

没有怎么看

Portals

第一眼看到这个的时候,有一点惊喜的,作为一个萌新前端,之前在自己写组件的时候,就碰到过想要把组件的一部分渲染在组件的外层(曾经写了一个类似于select的组件,展开后被内容遮挡,设置z-index又会让未展开的时候显示异常)。

该方法的核心就是 React.createPortal(this.props.children, this.el) 方法,该方法是接受一个子元素,将其挂载到一个元素上,下面是一个简单的demo,创建一个 div ,将 div 挂载到 body 内,然后把子元素置于 div 中。感兴趣的可以尝试自己写一个简单的modal,下面就是一个madal 的一部分:

class Demo extends Component {
  constructor(props) {
    super(props);
    this.state = { visible: false };
    
    // 先创建一个div
    this.el = document.createElement("div");
  }
  
  componentDidMount() {
    // 在组件生成时判定状态,如果是visible = true就调用生成子组件的函数
    if (this.props.visible !== this.state.visible) {
      this.initDom();
      this.setState({ visible: this.props.visible });
    }
  }
  
  componentDidUpdate(preProps, nextProps) {
    // 在组件更新时再进行判定
    if (this.props.visible !== nextProps.visible) {
      this.initDom();
      nextProps.visible = this.props.visible;
    }
  }
  componentWillUnmount() {
    //在组件卸载时摧毁之前创建的div
    document.body.removeChild(this.el);
  }
  
  initDom = () => {
    const { destory, visible } = this.props;
    let self = this;
    // 将之前创建的div置入body
    document.body.appendChild(this.el);
    // 定义了一个蒙版的底层
    this.el.setAttribute("class", "select_modal_box");
    this.el.style.cssText = `position: absolute;width: 100%;height: 100%;top:0;background:rgba(0,0,0,0.3)`;
    // 定义了一个函数监听蒙版的单击事件,接收外界的destory和visible
    this.el.addEventListener(
      "click",
      function (event) {
        if (event.target.className === "select_modal_box") {
          console.log("456456465", event.target);
          if (destory) {
            self.props.onCancel();
            let doc = document.getElementById("select_modal_box");
            console.log(event.currentTarget);
            event.currentTarget &&
              doc &&
             document.body.removeChild(event.currentTarget);
          } else {
            self.props.onCancel();
            this.style.visibility = "hidden";
          }
          self.setState({
            visible: false,
          });
        }
      },
      false
    );
    // 如果visible= false 隐藏modal
    !visible ? (this.el.style.visibility = "hidden") : console.log("test");
  };
  render() {
    // 返回一个挂载到div上的子组件,子组件由外部传入。
    return ReactDOM.createPortal(this.props.children, this.el);
  }
}

Profiler

未完待续。。。


Cyearn
64 声望2 粉丝