4-2 React 高级用法

41 阅读4分钟

原文链接(格式更好):《4-2 React 高级用法》

Ref

作用:用于访问组件实例或者 DOM 元素的

白话:React 版的document.querySelector(xx),更符合 React 的数据流规则

HostComponent:代表真实 DOM 元素的 React 组件

类组件 - createRef

import React, { Component } from 'react'

export default ClassCom extends Component {
  constructor(props) {
    super(props)

    this.inputRef = React.createRef()
  }

  handleClick = () => {
    this.inputRef.current.focus()
  }

  render() {
    return (<div>
      <input ref={this.inputRef} />
      <button onClick={this.handleClick}>Focus</button>
    </div>)
  }
}

函数组件 - useRef

import { useRef }  from 'react'

export default function FunCom() {
  const inputRef = useRef()

  const handleClick = () => {
    inputRef.current.focus()
  }
  
  return (<div>
    <input ref={inputRef} />
    <button onClick={handleClick}>Focus</button>
  </div>)
}

因为 Ref 本身是不支持跨层级传递的,当需要访问子组件的具体 DOM 时,则子组件需要用forwardRef包裹一下,否则只能获取子组件的 React 实例。

高级用法

基于forwardRef还可以实现父组件调用子组件的方法

useImperativeHandle(ref, () => obj)

当有useImperativeHandle时,则 ref 的 current 就会被替换为 obj

const ClassComChild2 = forwardRef(function (props, ref) {
  const inputRef = useRef();
  
  const handleClick = () => {
    inputRef.current.focus();
  };

  useImperativeHandle(ref, () => ({
    focusEvent: handleClick,
  }));

  return (
    <div>
      <h3>I am Class Com Child 2~ </h3>
      <input ref={inputRef} />
      <button onClick={handleClick}>Foucs</button>
    </div>
  );
});

export default class ClassComName extends Component {
  constructor(props) {
    super(props);

    this.childRef2 = React.createRef();
  }

  handleClick = () => {
    this.childRef2.current.focusEvent();
  };

  render() {
    return (
      <div>
        <ClassComChild2 ref={this.childRef2}></ClassComChild2>
      </div>
    );
  }
}

一般用于Moadl弹窗组件

Context

作用:一种在组件树中跨层级共享和传递状态(数据或函数)的方法

// 创建:
export const ThemeContext = React.createContext('')

// 提供:
<ThemeContext.Provider value={/* some value */}>
  {/* 组件树 */}
</ThemeContext.Provider>

// 消费:
// 1、Consumer
<ThemeContext.Consumer>
  {value => /* 使用 value */}
</ThemeContext.Consumer>

// 2、使用 useContext
const value = useContext(ThemeContext);

类组件 - Consumer

import ThemeContext from './ThemeContext'

export default class ClassCom extends Component {
  constructor(props) {
    super(props)

    this.state = { theme: 'light' }
  }
  render() {
    return <div>
        <ThemeContext.Provider value={this.state.theme}>
            <div>当前 theme:{ this.state.theme }</div>
            <Child1 />
  					<Child2 />
      	</ThemeContext.Provider>
    </div>
  }
}

export class Child1 extends Component {
  // 用法 1 - 定义:static contextType
  static contextType = ThemeContext

  render() {
    // 用法 1 - 获取:this.context
    return <div>当前 theme:{ this.context }</div>
  }
}

export class Child2 extends Component {
  render() {
    // 用法 2:ThemeContext.Consumer
    return <ThemeContext.Consumer>
      {
        (theme) => <div>当前 theme:{ theme }</div>
      }
    </ThemeContext.Consumer>
  }
}

函数组件 - useContext

import { useState, useContext } from "react";

import { ThemeContext } from "./ThemeContext";

const Child1 = () => {
  const theme = useContext(ThemeContext)

  return <div>当前 theme:{ theme }</div>
}

export default function FuncCom(){
  const [theme] = useState('light')
  
  return <div>
      <ThemeContext.Provider value={theme}>
          <div>当前 theme:{ this.state.theme }</div>
          <Child1 />
      </ThemeContext.Provider>
  </div>
}

HOC

HOC:Higher-Order Component,高阶组件,本质是个函数,参数为组件,返回为组件

高级函数:参数为函数,返回为函数的函数

作用:属性代理、反向继承

属性代理 - 常用

一般用函数组件实现

// ./components/hoc/propsProxy.jsx

import React from "react";

const withCard = (bgColor) => (ContextComponent) => {
  const WrpperComponent = (props) => {
    const wrapperStyle = {
      width: 200,
      height: 200,
      border: `1px solid ${props.borderColor}`,
      borderRadius: 5,
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      flexDirection: "column",
      backgroundColor: bgColor,
    };
    return (
      <div style={wrapperStyle}>
        <div>WrpperComponent</div>
        <ContextComponent {...props} />
      </div>
    );
  };

  return WrpperComponent;
};

const contextComponent = (props) => {
  return (
    <div>
      contextComponent
      <p>text:{props.text}</p>
    </div>
  );
};

export default withCard("blue")(contextComponent);

// App.jsx
import PropsProxy from "./components/hoc/propsProxy";

export default function App() {
  return <PropsProxy text="PropsProxy Text" borderColor="red"  />
}

反向继承 - 少见

一般用类组件实现

import React, { Component } from "react";

class ContentCom extends Component {
  render() {
    return (
      <div>
        ContentCom
        <div id="21dsa"> 一个普通的组件。。。。</div>
      </div>
    );
  }
}

// 新需求:当该组件渲染完后时,进行埋点统计
// 新需求改动点:-- start
const Logs = (logMap) => {
  return (Com) => {
    const didMount = Com.prototype.componentDidMount;

    return class Inner extends Com {
      componentDidMount() {
        if (didMount) {
          didMount.call(this);
        }

        Object.entries(logMap).forEach(([key, value]) => {
          if (document.getElementById(key)) {
            console.log(`元素 id:${key},开始埋点统计`);

            if (value && typeof value === "object") {
              Object.entries(value).forEach(([method, fn]) => {
                if (typeof fn === "function") {
                  fn.call(this);
                }
              });
            }
          }
        });
      }
      render() {
        return super.render();
      }
    };
  };
};

const LogContentCom = Logs({
  "21dsa": {
    pv() {
      console.log("[统计 pv ] >");
    },
    uv() {
      console.log("[统计 uv ] >");
    },
  },
})(ContentCom);
// 新需求改动点:-- end

export default class ReverseExtends extends Component {
  render() {
    return (
      <div>
        ReverseExtends
        {/* 改动点:<ContentCom /> => <LogContentCom /> */}
        <LogContentCom />
      </div>
    );
  }
}

渲染控制

一般为两种方法来控制渲染

1、父组件隔离子组件的渲染

2、子组件控制不要额外渲染

当组件内的响应式数据改变时,整个 render 都会重新执行一遍,生成新的虚拟 DOM,然后新旧对比,完成真实 DOM 的更新

父组件隔离子组件的渲染

类组件

import React, { Component } from "react";

const Count = ({ count }) => {
  console.log("[ Count 执行, count ] >", count);
  return <div>curr count: {count}</div>;
};

export default class RenderController extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      number: 0,
    };

    this.Count = <Count count={this.state.count} />;
  }

  // Count 组件的渲染控制逻辑
  CountComponentController = () => {
    // 每当 this.state 的值有变化时,该函数都会执行哦
    console.log("[ CountComponentController 执行 ] >");

    if (this.state.count !== this.Count.props.count) {
      // 只有当 count 的值有变化,则重新赋值,触发渲染执行
      this.Count = <Count count={this.state.count} />;
    }

    // 否则,直接返回 this.Count,不重新渲染
    return this.Count;
  };

  render() {
    return (
      <div>
        <div>RenderController</div>
        <div>
          <span>count: {this.state.count}</span>
          <button
            onClick={() => this.setState({ count: this.state.count + 1 })}
          >
            +
          </button>
          {/* 这样写,那 number 的变化也会导致该组件重新执行,只是真实 DOM 不重新渲染 */}
          {/* <Count count={this.state.count} /> */}

          {/* 这样写,那 number 的变化将不会导致该组件重新执行 */}
          {this.CountComponentController()}
        </div>
        <div>
          <span>number: {this.state.number}</span>
          <button
            onClick={() => this.setState({ number: this.state.number + 1 })}
          >
            +
          </button>
        </div>
      </div>
    );
  }
}

函数组件

也就是 useMemo 的使用

import React, { useMemo, useState } from "react";

const Count = ({ count }) => {
  console.log("[ Count 执行, count ] >", count);
  return <div>curr count: {count}</div>;
};

export default function RenderController() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  const memoCount = useMemo(() => <Count count={count} />, [count]);
  return (
    <div>
      <div>RenderController</div>
      <div>
        <span>count: {count}</span>
        <button onClick={() => setCount(count + 1)}>+</button>
        {/* 这样写,那 number的变化也会导致该组件重新执行,只是真实 DOM 不重新渲染 */}
        {/* <Count count={count} /> */}

        {memoCount}
      </div>
      <div>
        <span>number: {number}</span>
        <button onClick={() => setNumber(number + 1)}>+</button>
      </div>
    </div>
  );
}

useMemo(fn, deps),返回值为 fn 函数的返回值

fn:函数,返回值进行缓存

deps:依赖项,改变时重新执行函数

子组件控制不要额外渲染

类组件:

  1. shouldComponentUpdate(生命周期)
  2. PureComponent:props、state 进行浅比较,有变化后则更新
export default class RenderController extends PureComponent {
  cunstructor(props) {
    super(props)
  }

  render() {
    return (/* ... */)
  }
}

补充

useCallback

使用场景:

  1. 回调函数作为props传递给子组件:如果您有一个父组件传递给子组件的回调函数,且这个回调函数依赖于父组件的状态或属性,那么使用useCallback可以确保子组件在父组件重新渲染时不会因为回调函数的引用发生变化而重新渲染。
  2. 依赖于props的内联函数:如果您在组件内部定义了一个依赖于props的内联函数,而这个函数在组件重新渲染时没有发生变化,那么使用useCallback可以避免不必要的函数重新创建。
  3. 作为effect的依赖项:如果您将回调函数作为effect的依赖项,使用useCallback可以确保effect只在回调函数引用发生变化时才重新运行。

useCallback(fn, deps),返回值为 fn 函数

fn:要执行函数

deps:依赖项,改变时返回新的函数