React 高级特性

一、函数组件

没有state的情况下,只接受props的情况下,可以使用函数组件代替class组件。

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法
function List(props) {
  const { list } = this.props;

  return (
    <ul>
      {list.map((item, index) => {
        return (
          <li key={item.id}>
            <span>{item.title}</span>
          </li>
        );
      })}
    </ul>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

二、非受控组件

  • ref
  • defaultValue defaultChecked
  • 手动操作 DOM 元素

(1)代码演示

class UncontrolComp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "非受控组件",
    };
    this.nameRef = React.createRef();
    this.fileRef = React.createRef();
  }

  render() {
    return (
      <div>
        {/* 获取 input 的值 */}
        <input defaultValue={this.state.name} ref={this.nameRef}></input>
        <span>{this.state.name}</span>
        <button onClick={this.alertName}></button>

        {/* 获取文件 */}
        <input type="file" ref={this.fileRef}></input>
        <button onClick={this.alertFile}></button>
      </div>
    );
  }

  alertName = () => {
    const elem = this.nameRef.current;
    alert(elem.value);
  };

  alertFile = () => {
    const elem = this.fileRef.current;
    alert(elem.files[0].name);
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

(2)使用场景

  • 必须手动操作 DOM 元素,setState 实现不了
  • 文件上传<input type="file"></input>
  • 某些富文本编辑器,需要传入 DOM 元素

(3)受控组件 vs 非受控组件

  • 优先使用受控组件,符合 React 设计原则
  • 必须操作 DOM 时,再使用非受控组件

三、Portals

  • 组件默认会按照既定层次嵌套渲染
  • Portals能将组件渲染到父组件以外

class Portal extends React.Component {
    render() {
        return ReactDOM.createPortal(
            {/* this.props.children 相当于 vue 的slot */}
            <div className="modal">{this.props.children}</div>,
            {/* dom元素,将第一个参数的 dom 插入 到这个 dom 节点下 */}
            document.body
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11

类比 Vue3teleport

使用场景:

  • overfiow: hidden
  • 父组件 z-index 值太小
  • fixed 需要放到 body 第一层级

四、context

  • 公共信息(语言、主题),传递给每个组件
  • 用 props 太繁琐
  • 用 redux 小题大做

使用

  1. 定义:const ThemeContext = React.createContext('light')
  2. 父组件生产数据,使用<ThemeContext.Provider value={ }></ThemeContext.Provider>包裹
  3. 子组件消费数据:
  • 函数组件使用<ThemeContext.Consumer>{ value => <p>{value}</p> }</ThemeContext>消费
  • class 组件使用ChildComp.contextType = ThemeContext消费

代码演示

import React from "react";

// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext("light");

// 底层组件 - 函数是组件
function ThemeLink(props) {
  // const theme = this.context // 会报错。函数式组件没有实例,即没有 this

  // 函数式组件可以使用 Consumer
  return (
    <ThemeContext.Consumer>
      {(value) => <p>links theme is {value}</p>}
    </ThemeContext.Consumer>
  );
}

// 底层组件 - class 组件
class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
  render() {
    const theme = this.context; // React 会往上找到最近的 theme Provider,然后使用它的值。
    return (
      <div>
        <p>buttons theme is {theme}</p>
      </div>
    );
  }
}
ThemedButton.contextType = ThemeContext; // 指定 contextType 读取当前的 theme context。

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
      <ThemeLink />
    </div>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: "light",
    };
  }
  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        <Toolbar />
        <hr />
        <button onClick={this.changeTheme}>change theme</button>
      </ThemeContext.Provider>
    );
  }
  changeTheme = () => {
    this.setState({
      theme: this.state.theme === "light" ? "dark" : "light",
    });
  };
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

五、异步组件

  • import()
  • React.lazy
  • React.Suspense

代码演示

import React from "react";

const ContextDemo = React.lazy(() => import("./ContextDemo"));

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <p>引入一个动态组件</p>
        <hr />
        <React.Suspense fallback={<div>Loading...</div>}>
          <ContextDemo />
        </React.Suspense>
      </div>
    );

    // 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 网速)
    // 2. 看 network 的 js 加载
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

六、性能优化

1. SCU(shouldComponentUpdate 的简称)

背景:

  1. 默认情况下,父组件更新,子组件无条件也跟随更新(SCU默认返回true
  2. 鉴于性能损耗问题,组件数据没变化的情况下,没必要更新,避免不必要的开销

(2)基本用法

shouldComponentUpdate(nextProps, nextState) {
    if (nextState.count !== this.state.count) {
        return true // 可以渲染
    }
    return false // 不可重复渲染
}
1
2
3
4
5
6

注意:SCU必须配合不可变值使用,否则nextState.xxxthis.state.xxx永远相等,shouldComponentUpdate返回的永远为false

(2)总结

  • SCU默认返回true,即 React 默认重新渲染所有子组件
  • 必须配合“不可变值”使用
  • 可先不用SCU,有性能问题时在考虑使用

2. PureComponent 和 React.memo

  • PureComponent,SCU 中实现了浅比较
  • memo,函数组件中的 PureComponent
  • 浅比较已使用大部分情况(尽量不要做深度比较)
// class 组件:PureComponent,和 Component 用法一致
class PureComp extends React.PureComponent {}

// 函数组件
function MayComponent(props) {}

function areEqual(prevProps, nextProps) {
  /**
    如果把 nextProps 传入 render 方法返回的结果 和
    prevProps 传入 render 方法返回的结果 一致,则返回 true,否则返回 false
    */
}

export default React.memo(PureComp, areEqual);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3. 不可变值 immutable.js

  • 彻底拥抱“不可变值”
  • 基于共享数据(不是深拷贝),速度快
  • 有一定的学习和迁移成本,按需使用
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set("b", 50);
map1.get("b"); // 2
map2.get("b"); // 50
1
2
3
4

七、公共逻辑抽离

(1)高阶组件 HOC

参数为组件,返回值为新组件的函数(类似高阶函数、装饰器模式)

import React from "react";

// 高阶组件
const withMouse = (Component) => {
  class withMouseComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { x: 0, y: 0 };
    }

    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY,
      });
    };

    render() {
      return (
        <div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
          {/* 1. 透传所有 props 2. 增加 mouse 属性 */}
          <Component {...this.props} mouse={this.state} />
        </div>
      );
    }
  }
  return withMouseComponent;
};

const App = (props) => {
  const a = props.a;
  const { x, y } = props.mouse; // 接收 mouse 属性
  return (
    <div style={{ height: "500px" }}>
      <h1>
        The mouse position is ({x}, {y})
      </h1>
      <p>{a}</p>
    </div>
  );
};

export default withMouse(App); // 返回高阶函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

(2)Render Props

核心:将一个函数以props方式传递给子组件,子组件渲染时调用这个函数

import React from "react";
import PropTypes from "prop-types";

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
        {/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */}
        {this.props.render(this.state)}
      </div>
    );
  }
}
Mouse.propTypes = {
  render: PropTypes.func.isRequired, // 必须接收一个 render 属性,而且是函数
};

const App = (props) => (
  <div style={{ height: "500px" }}>
    <p>{props.a}</p>
    <Mouse
      render={
        /* render 是一个函数组件 */
        ({ x, y }) => (
          <h1>
            The mouse position is ({x}, {y})
          </h1>
        )
      }
    />
  </div>
);

/**
 * 即,定义了 Mouse 组件,只有获取 x y 的能力。
 * 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。
 */

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

(3)HOC vs Render Props

  • HOC:模式简单,易于理解,但会增加组件层级
  • Render Props:代码简洁,但学习成本较高
  • 按需使用