Skip to content

React 更新机制

我们在前面已经学习 React 的渲染流程:

那么 React 的更新流程呢?

React 的更新流程

React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树。

React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI:

如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^2),其中 n 是树中元素的数量;

https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;

这个开销太过昂贵了,React 的更新性能会变得非常低效;

于是,React 对这个算法进行了优化,将其优化成了 O(n),如何优化的呢?

同层节点之间相互比较,不会垮节点比较;

不同类型的节点,产生不同的树结构;

开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定;

keys 的优化

我们在前面遍历列表时,总是会提示一个警告,让我们加入一个 key 属性:

方式一:在最后位置插入数据

这种情况,有无 key 意义并不大

方式二:在前面插入数据

这种做法,在没有 key 的情况下,所有的 li 都需要进行修改;

当子元素(这里的 li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

在下面这种场景下,key 为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改;

将 key 为 333 的元素插入到最前面的位置即可;

key 的注意事项:

key 应该是唯一的;

key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字);

使用 index 作为 key,对性能是没有优化的;

render 函数被调用

我们使用之前的一个嵌套案例:

在 App 中,我们增加了一个计数器的代码;

当点击+1 时,会重新调用 App 的 render 函数;

而当 App 的 render 函数被调用时,所有的子组件的 render 函数都会被重新调用;

那么,我们可以思考一下,在以后的开发中,我们只要是修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法,性能必然是很低的:

事实上,很多的组件没有必须要重新 render;

它们调用 render 应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的 render 方法;

如何来控制 render 方法是否被调用呢?

通过 shouldComponentUpdate 方法即可;

shouldComponentUpdate

React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有

返回值:

该方法有两个参数:

参数一:nextProps 修改之后,最新的 props 属性

参数一:nextProps 修改之后,最新的 props 属性

该方法返回值是一个 boolean 类型:

返回值为 true,那么就需要调用 render 方法;

返回值为 false,那么就不需要调用 render 方法;

默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;

比如我们在 App 中增加一个 message 属性:

jsx 中并没有依赖这个 message,那么它的改变不应该引起重新渲染;

但是因为 render 监听到 state 的改变,就会重新 render,所以最后 render 方法还是被重新调用了;

App.jsx

javascript
import React, { Component } from "react";
import Home from "./Home";
import Recommend from "./Recommend";
import Profile from "./Profile";

export class App extends Component {
  constructor() {
    super();

    this.state = {
      message: "Hello World",
      counter: 0,
    };
  }

  shouldComponentUpdate(nextProps, newState) {
    // App进行性能优化的点
    if (
      this.state.message !== newState.message ||
      this.state.counter !== newState.counter
    ) {
      return true;
    }
    return false;
  }

  changeText() {
    this.setState({ message: "你好啊,李银河!" });
    // this.setState({ message: "Hello World" })
  }

  increment() {
    this.setState({ counter: this.state.counter + 1 });
  }

  render() {
    console.log("App render");
    const { message, counter } = this.state;

    return (
      <div>
        <h2>
          App-{message}-{counter}
        </h2>
        <button onClick={(e) => this.changeText()}>修改文本</button>
        <button onClick={(e) => this.increment()}>counter+1</button>
        <Home message={message} />
        <Recommend counter={counter} />
      </div>
    );
  }
}

export default App;

Home.jsx

javascript
import React, { Component } from "react";

export class Home extends Component {
  constructor(props) {
    super(props);

    this.state = {
      friends: [],
    };
  }

  shouldComponentUpdate(newProps, nextState) {
    // 自己对比state是否发生改变: this.state和nextState
    if (this.props.message !== newProps.message) {
      return true;
    }
    return false;
  }

  render() {
    console.log("Home render");
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    );
  }
}

export default Home;

Recommend.jsx

javascript
import React, { Component } from "react";

export class Recommend extends Component {
  shouldComponentUpdate(nextProps) {
    if (this.props.counter !== nextProps.counter) {
      return true;
    }
    return false;
  }

  render() {
    console.log("Recommend render");
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    );
  }
}

export default Recommend;

App 的 shouldComponentUpdate 判断只有当 message 或 counter 发生改变时才会重写渲染 App 组件,在 App 组件中将 message 传给了 Home 组件,将 counter 传给了 Recommend 组件,点击修改文本,改变 message,就会更新 Home 组件,Recommend 组件不会重新渲染;点击 counter+1 按钮,counter 发生改变,Recommend 组件会重新渲染,而 Home 组件不会。

PureComponent

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?

props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;

事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢?

将 class 继承自 PureComponent。

image-20230611203234203

shallowEqual 方法

在 checkShouldComponentUpdate 方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个 shallowEqual 就是进行浅层比较:

如果前后的 props 或 state 一样,那么返回 true,取反变成 false,就不会重新渲染。

如果前后的 props 或 state 不是对象或者为 null,那么返回 false,取反变成 true,会重新渲染。

获取前后 props 或 state 的所有 key,进行遍历;如果长度不一样,说明发生了改变,返回 false,取反变成 true,会重新渲染。

遍历旧的 props 或 state,如果新的 props 或 state 没有旧的 props 或 state 的属性,或者旧的 props 或 state 和新的 props 或 state 不是同一个对象,那么返回 false,取反变成 true,会重新渲染。

image-20230611203257582

高阶组件 memo

目前我们是针对类组件可以使用 PureComponent,那么函数式组件呢?

事实上函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的

我们需要使用一个高阶组件 memo:

当 counter 发生改变时,Profile 组件不会重新渲染。

App.jsx

javascript
import React, { PureComponent } from "react";
import Home from "./Home";
import Recommend from "./Recommend";
import Profile from "./Profile";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      message: "Hello World",
      counter: 0,
    };
  }

  // shouldComponentUpdate(nextProps, newState) {
  //   // App进行性能优化的点
  //   if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
  //     return true
  //   }
  //   return false
  // }

  changeText() {
    this.setState({ message: "你好啊,李银河!" });
    // this.setState({ message: "Hello World" })
  }

  increment() {
    this.setState({ counter: this.state.counter + 1 });
  }

  render() {
    console.log("App render");
    const { message, counter } = this.state;

    return (
      <div>
        <h2>
          App-{message}-{counter}
        </h2>
        <button onClick={(e) => this.changeText()}>修改文本</button>
        <button onClick={(e) => this.increment()}>counter+1</button>
        <Home message={message} />
        <Recommend counter={counter} />
        <Profile message={message} />
      </div>
    );
  }
}

export default App;

Home.jsx

javascript
import React, { PureComponent } from "react";

export class Home extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      friends: [],
    };
  }

  // shouldComponentUpdate(newProps, nextState) {
  //   // 自己对比state是否发生改变: this.state和nextState
  //   if (this.props.message !== newProps.message) {
  //     return true
  //   }
  //   return false
  // }

  render() {
    console.log("Home render");
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    );
  }
}

export default Home;

Recommend.jsx

javascript
import React, { PureComponent } from "react";

export class Recommend extends PureComponent {
  // shouldComponentUpdate(nextProps) {
  //   if (this.props.counter !== nextProps.counter) {
  //     return true
  //   }
  //   return false
  // }

  render() {
    console.log("Recommend render");
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    );
  }
}

export default Recommend;

Profile.jsx

javascript
import { memo } from "react";

const Profile = memo(function (props) {
  console.log("profile render");
  return <h2>Profile: {props.message}</h2>;
});

export default Profile;

不可变数据的力量

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      books: [
        { name: "你不知道JS", price: 99, count: 1 },
        { name: "JS高级程序设计", price: 88, count: 1 },
        { name: "React高级设计", price: 78, count: 2 },
        { name: "Vue高级设计", price: 95, count: 3 },
      ],
      friend: {
        name: "kobe",
      },
      message: "Hello World",
    };
  }

  // shouldComponentUpdate(nextProps, nextState) {
  //   shallowEqual(nextProps, this.props)
  //   shallowEqual(nextState, this.state)
  // }

  addNewBook() {
    const newBook = { name: "Angular高级设计", price: 88, count: 1 };

    // 1.直接修改原有的state, 重新设置一遍
    // 在PureComponent是不能引起重新渲染(re-render)
    this.state.books.push(newBook);
    this.setState({ books: this.state.books });

    // 2.复制一份books, 在新的books中修改, 设置新的books
    const books = [...this.state.books];
    books.push(newBook);

    this.setState({ books: books });
  }

  addBookCount(index) {
    // this.state.books[index].count++ // 这种写法不会触发render
    const books = [...this.state.books];
    books[index].count++;
    this.setState({ books: books });
  }

  render() {
    const { books } = this.state;

    return (
      <div>
        <h2>数据列表</h2>
        <ul>
          {books.map((item, index) => {
            return (
              <li key={index}>
                <span>
                  name:{item.name}-price:{item.price}-count:{item.count}
                </span>
                <button onClick={(e) => this.addBookCount(index)}>+1</button>
              </li>
            );
          })}
        </ul>
        <button onClick={(e) => this.addNewBook()}>添加新书籍</button>
      </div>
    );
  }
}

export default App;

如何使用 ref

在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 元素,但是某些特殊的情况,确实需要获取到 DOM 进行某些操作:

管理焦点,文本选择或媒体播放;

触发强制动画;

集成第三方 DOM 库;

我们可以通过 refs 获取 DOM;

如何创建 refs 来获取对应的 DOM 呢?目前有三种方式:

方式一:传入字符串

使用时通过 this.refs.传入的字符串格式获取对应的元素;

方式二:传入一个对象(推荐)

对象是通过 React.createRef() 方式创建出来的;

使用时获取到创建的对象其中有一个 current 属性就是对应的元素;

方式三:传入一个函数

该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;

使用时,直接拿到之前保存的元素对象即可;

javascript
import React, { PureComponent, createRef } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {};

    this.titleRef = createRef();
    this.titleEl = null;
  }

  getNativeDOM() {
    // 1.方式一: 在React元素上绑定一个ref字符串
    // console.log(this.refs.why)

    // 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素
    // console.log(this.titleRef.current)

    // 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
    console.log(this.titleEl);
  }

  render() {
    return (
      <div>
        <h2 ref="why">Hello World</h2>
        <h2 ref={this.titleRef}>你好啊,李银河</h2>
        <h2 ref={(el) => (this.titleEl = el)}>你好啊, 师姐</h2>
        <button onClick={(e) => this.getNativeDOM()}>获取DOM</button>
      </div>
    );
  }
}

export default App;

ref 的类型

ref 的值根据节点的类型而有所不同:

当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;

当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;

你不能在函数组件上使用 ref 属性,因为他们没有实例;

函数式组件是没有实例的,所以无法通过 ref 获取他们的实例:

但是某些时候,我们可能想要获取函数式组件中的某个 DOM 元素;

这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用 ref;

ref 获取类组件的实例

javascript
import React, { PureComponent, createRef } from "react";

class HelloWorld extends PureComponent {
  test() {
    console.log("test------");
  }

  render() {
    return <h1>Hello World</h1>;
  }
}

export class App extends PureComponent {
  constructor() {
    super();

    this.hwRef = createRef();
  }

  getComponent() {
    console.log(this.hwRef.current);
    this.hwRef.current.test();
  }

  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={(e) => this.getComponent()}>获取组件实例</button>
      </div>
    );
  }
}

export default App;

ref 的转发

在前面我们学习 ref 时讲过,ref 不能应用于函数式组件:

因为函数式组件没有实例,所以不能获取到对应的组件对象

但是,在开发中我们可能想要获取函数式组件中某个元素的 DOM,这个时候我们应该如何操作呢?

方式一:直接传入 ref 属性(错误的做法)

方式二:通过 forwardRef 高阶函数;

javascript
import React, { PureComponent, createRef, forwardRef } from "react";

const HelloWorld = forwardRef(function (props, ref) {
  return (
    <div>
      <h1 ref={ref}>Hello World</h1>
      <p>哈哈哈</p>
    </div>
  );
});

export class App extends PureComponent {
  constructor() {
    super();

    this.hwRef = createRef();
  }

  getComponent() {
    console.log(this.hwRef.current); // <h1>Hello World</h1>
  }

  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={(e) => this.getComponent()}>获取组件实例</button>
      </div>
    );
  }
}

export default App;

认识受控组件

在 React 中,HTML 表单的处理方式和普通的 DOM 元素不太一样:表单元素通常会保存在一些内部的 state。

比如下面的 HTML 表单元素:

这个处理方式是 DOM 默认处理 HTML 表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;

在 React 中,并没有禁止这个行为,它依然是有效的;

但是通常情况下会使用 JavaScript 函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;

实现这种效果的标准方式是使用“受控组件”;

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "coderwhy",
    };
  }

  inputChange(event) {
    console.log("inputChange:", event.target.value);
    this.setState({ username: event.target.value });
  }

  render() {
    const { username } = this.state;

    return (
      <div>
        {/* 受控组件 */}
        <input
          type="checkbox"
          value={username}
          onChange={(e) => this.inputChange(e)}
        />

        {/* 非受控组件 */}
        <input type="text" />
        <h2>username: {username}</h2>
      </div>
    );
  }
}

export default App;

受控组件基本演练

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进

行更新。

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们将两者结合起来,使 React 的 state 成为“唯一数据源”;

渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;

被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。

由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

自己提交 form 表单

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为,就不会刷新浏览器
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组织
    console.log("获取所有的输入内容");
    console.log(this.state.username);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleUsernameChange(event) {
    this.setState({ username: event.target.value });
  }

  render() {
    const { username } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码,这里是htmlFor,for是个关键字,防止冲突,使用htmlFor,和className一样 */}
          <label htmlFor="username">
            用户:
            <input
              id="username"
              type="text"
              name="username"
              value={username}
              onChange={(e) => this.handleUsernameChange(e)}
            />
          </label>

          <button type="submit">注册</button>
        </form>
      </div>
    );
  }
}

export default App;

多个表单同一个函数

我们上面的表单中只有用户名,我们可以加入一个密码输入框

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
      password: "",
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleUsernameChange(event) {
    this.setState({ username: event.target.value });
  }

  handlePasswordChange(event) {
    this.setState({ password: event.target.value });
  }

  render() {
    const { username, password } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码 */}
          <label htmlFor="username">
            用户:
            <input
              id="username"
              type="text"
              name="username"
              value={username}
              onChange={(e) => this.handleUsernameChange(e)}
            />
          </label>
          <label htmlFor="password">
            密码:
            <input
              id="password"
              type="password"
              name="password"
              value={password}
              onChange={(e) => this.handlePasswordChange(e)}
            />
          </label>

          <button type="submit">注册</button>
        </form>
      </div>
    );
  }
}

export default App;

但是,我们会发现 handleUsernameChange 和 handlePasswordChange 里面的处理逻辑有点相似,我们可以进一步进行优化。

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
      password: "",
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  // handleUsernameChange(event) {
  //   this.setState({ username: event.target.value })
  // }

  // handlePasswordChange(event) {
  //   this.setState({ password: event.target.value })
  // }

  // 使用计算属性名,前提是要给input绑定name
  handleInputChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  render() {
    const { username, password } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码 */}
          <label htmlFor="username">
            用户:
            <input
              id="username"
              type="text"
              name="username"
              value={username}
              onChange={(e) => this.handleInputChange(e)}
            />
          </label>
          <label htmlFor="password">
            密码:
            <input
              id="password"
              type="password"
              name="password"
              value={password}
              onChange={(e) => this.handleInputChange(e)}
            />
          </label>

          <button type="submit">注册</button>
        </form>
      </div>
    );
  }
}

export default App;

受控组件的其他演练

textarea 标签

texteare 标签和 input 比较相似:

select 标签

select 标签的使用也非常简单,只是它不需要通过 selected 属性来控制哪一个被选中,它可以匹配 state 的 value 来选中。

处理多个输入

多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:

这里我们可以使用 ES6 的一个语法:计算属性名(Computed property names)

checkbox 的单选和多选

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
      password: "",
      isAgree: false,
      hobbies: [
        { value: "sing", text: "唱", isChecked: false },
        { value: "dance", text: "跳", isChecked: false },
        { value: "rap", text: "rap", isChecked: false },
      ],
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);
    const hobbies = this.state.hobbies
      .filter((item) => item.isChecked)
      .map((item) => item.value);
    console.log("获取爱好: ", hobbies);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleInputChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  handleAgreeChange(event) {
    this.setState({ isAgree: event.target.checked });
  }

  handleHobbiesChange(event, index) {
    const hobbies = [...this.state.hobbies];
    hobbies[index].isChecked = event.target.checked;
    this.setState({ hobbies });
  }

  render() {
    const { username, password, isAgree, hobbies, fruit } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码 */}
          <div>
            <label htmlFor="username">
              用户:
              <input
                id="username"
                type="text"
                name="username"
                value={username}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
            <label htmlFor="password">
              密码:
              <input
                id="password"
                type="password"
                name="password"
                value={password}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
          </div>

          {/* 2.checkbox单选 */}
          <label htmlFor="agree">
            <input
              id="agree"
              type="checkbox"
              checked={isAgree}
              onChange={(e) => this.handleAgreeChange(e)}
            />
            同意协议
          </label>

          {/* 3.checkbox多选 */}
          <div>
            您的爱好:
            {hobbies.map((item, index) => {
              return (
                <label htmlFor={item.value} key={item.value}>
                  <input
                    type="checkbox"
                    id={item.value}
                    checked={item.isChecked}
                    onChange={(e) => this.handleHobbiesChange(e, index)}
                  />
                  <span>{item.text}</span>
                </label>
              );
            })}
          </div>

          <div>
            <button type="submit">注册</button>
          </div>
        </form>
      </div>
    );
  }
}

export default App;

select 的单选和多选

javascript
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
      password: "",
      isAgree: false,
      hobbies: [
        { value: "sing", text: "唱", isChecked: false },
        { value: "dance", text: "跳", isChecked: false },
        { value: "rap", text: "rap", isChecked: false },
      ],
      fruit: "orange",
      fruit2: ["orange"],
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);
    const hobbies = this.state.hobbies
      .filter((item) => item.isChecked)
      .map((item) => item.value);
    console.log("获取爱好: ", hobbies);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleInputChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  handleAgreeChange(event) {
    this.setState({ isAgree: event.target.checked });
  }

  handleHobbiesChange(event, index) {
    const hobbies = [...this.state.hobbies];
    hobbies[index].isChecked = event.target.checked;
    this.setState({ hobbies });
  }

  handleFruitChange(event) {
    this.setState({ fruit: event.target.value });
  }

  handleFruitChange2(event) {
    const options = Array.from(event.target.selectedOptions);
    const values = options.map((item) => item.value);
    this.setState({ fruit2: values });

    // 额外补充: Array.from(可迭代对象)
    // Array.from(arguments) Array.from支持传入第2个参数,就是map高级函数,了解即可
    const values2 = Array.from(
      event.target.selectedOptions,
      (item) => item.value
    );
    console.log(values2);
  }

  render() {
    const { username, password, isAgree, hobbies, fruit } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码 */}
          <div>
            <label htmlFor="username">
              用户:
              <input
                id="username"
                type="text"
                name="username"
                value={username}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
            <label htmlFor="password">
              密码:
              <input
                id="password"
                type="password"
                name="password"
                value={password}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
          </div>

          {/* 2.checkbox单选 */}
          <label htmlFor="agree">
            <input
              id="agree"
              type="checkbox"
              checked={isAgree}
              onChange={(e) => this.handleAgreeChange(e)}
            />
            同意协议
          </label>

          {/* 3.checkbox多选 */}
          <div>
            您的爱好:
            {hobbies.map((item, index) => {
              return (
                <label htmlFor={item.value} key={item.value}>
                  <input
                    type="checkbox"
                    id={item.value}
                    checked={item.isChecked}
                    onChange={(e) => this.handleHobbiesChange(e, index)}
                  />
                  <span>{item.text}</span>
                </label>
              );
            })}
          </div>

          {/* 4.select单选 */}
          <select
            value={fruit}
            onChange={(e) => this.handleFruitChange(e)}
            multiple
          >
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          {/* 4.select多选 */}
          <select
            value={fruit2}
            onChange={(e) => this.handleFruitChange2(e)}
            multiple
          >
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          <div>
            <button type="submit">注册</button>
          </div>
        </form>
      </div>
    );
  }
}

export default App;

非受控组件

React 推荐大多数情况下使用 受控组件 来处理表单数据:

一个受控组件中,表单数据是由 React 组件来管理的;

另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;

如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。

我们来进行一个简单的演练:

使用 ref 来获取 input 元素;

在非受控组件中通常使用 defaultValue 来设置默认值;

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked,<select> <textarea>

持 defaultValue。

javascript
import React, { createRef, PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      username: "",
      password: "",
      isAgree: false,
      hobbies: [
        { value: "sing", text: "唱", isChecked: false },
        { value: "dance", text: "跳", isChecked: false },
        { value: "rap", text: "rap", isChecked: false },
      ],
      fruit: ["orange"],
      intro: "哈哈哈",
    };

    this.introRef = createRef();
  }

  componentDidMount() {
    // 非受控组件就是在操作DOM,开发中建议都使用受控组件
    // this.introRef.current.addEventListener
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);
    const hobbies = this.state.hobbies
      .filter((item) => item.isChecked)
      .map((item) => item.value);
    console.log("获取爱好: ", hobbies);
    console.log("获取结果:", this.introRef.current.value);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleInputChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  handleAgreeChange(event) {
    this.setState({ isAgree: event.target.checked });
  }

  handleHobbiesChange(event, index) {
    const hobbies = [...this.state.hobbies];
    hobbies[index].isChecked = event.target.checked;
    this.setState({ hobbies });
  }

  handleFruitChange(event) {
    const options = Array.from(event.target.selectedOptions);
    const values = options.map((item) => item.value);
    this.setState({ fruit: values });

    // 额外补充: Array.from(可迭代对象)
    // Array.from(arguments)
    const values2 = Array.from(
      event.target.selectedOptions,
      (item) => item.value
    );
    console.log(values2);
  }

  render() {
    const { username, password, isAgree, hobbies, fruit, intro } = this.state;

    return (
      <div>
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码 */}
          <div>
            <label htmlFor="username">
              用户:
              <input
                id="username"
                type="text"
                name="username"
                value={username}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
            <label htmlFor="password">
              密码:
              <input
                id="password"
                type="password"
                name="password"
                value={password}
                onChange={(e) => this.handleInputChange(e)}
              />
            </label>
          </div>

          {/* 2.checkbox单选 */}
          <label htmlFor="agree">
            <input
              id="agree"
              type="checkbox"
              checked={isAgree}
              onChange={(e) => this.handleAgreeChange(e)}
            />
            同意协议
          </label>

          {/* 3.checkbox多选 */}
          <div>
            您的爱好:
            {hobbies.map((item, index) => {
              return (
                <label htmlFor={item.value} key={item.value}>
                  <input
                    type="checkbox"
                    id={item.value}
                    checked={item.isChecked}
                    onChange={(e) => this.handleHobbiesChange(e, index)}
                  />
                  <span>{item.text}</span>
                </label>
              );
            })}
          </div>

          {/* 4.select */}
          <select
            value={fruit}
            onChange={(e) => this.handleFruitChange(e)}
            multiple
          >
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          {/* 5.非受控组件 */}
          <input type="text" defaultValue={intro} ref={this.introRef} />

          <div>
            <button type="submit">注册</button>
          </div>
        </form>
      </div>
    );
  }
}

export default App;

认识高阶函数

什么是高阶组件呢?

相信很多同学都知道(听说过?),也用过 高阶函数

它们非常相似,所以我们可以先来回顾一下什么是 高阶函数。

高阶函数的维基百科定义:至少满足以下条件之一:

接受一个或多个函数作为输入;

输出一个函数;

JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。

那么什么是高阶组件呢?

高阶组件的英文是 Higher-Order Components,简称为 HOC;

官方的定义:高阶组件是参数为组件,返回值为新组件的函数

我们可以进行如下的解析:

首先, 高阶组件 本身不是一个组件,而是一个函数;

其次,这个函数的参数是一个组件,返回值也是一个组件;

高阶组件的定义

高阶组件的调用过程类似于这样:

高阶函数的编写过程类似于这样:

组件的名称问题:

在 ES6 中,类表达式中类名是可以省略的;

组件的名称都可以通过 displayName 来修改;

高阶组件并不是 React API 的一部分,它是基于 React 的组合特性而形成的设计模式;

高阶组件在一些 React 第三方库中非常常见:

比如 redux 中的 connect;(后续会讲到)

比如 react-router 中的 withRouter;(后续会讲到)

javascript
import React, { PureComponent } from "react";

// 定义一个高阶组件
function hoc(Cpn) {
  // 1.定义类组件
  class NewCpn extends PureComponent {
    render() {
      {
        /*返回的新组件都有一个name属性*/
      }
      return <Cpn name="why" />;
    }
  }
  return NewCpn;

  // 定义函数组件
  // function NewCpn2(props) {

  // }
  // return NewCpn2
}

class HelloWorld extends PureComponent {
  render() {
    return <h1>Hello World</h1>;
  }
}

const HelloWorldHOC = hoc(HelloWorld);

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloWorldHOC />
      </div>
    );
  }
}

export default App;

应用一 – props 的增强

不修改原有代码的情况下,添加新的 props

定义一个组件增强函数,参数为类组件或函数式组件,返回一个新的组件。

相当于做了拦截,给想要使用增强函数的组件加了 userInfo,但是我们自己也有可能会传入一些属性,比如 banners,那么也是需要传入到高阶组件中,用法是{...this.props},那么返回的新组件也会有自己传入的 props。

App.jsx

javascript
import React, { PureComponent } from "react";
import enhancedUserInfo from "./hoc/enhanced_props";
import About from "./pages/About";

const Home = enhancedUserInfo(function (props) {
  return (
    <h1>
      Home: {props.name}-{props.level}-{props.banners}
    </h1>
  );
});

const Profile = enhancedUserInfo(function (props) {
  return (
    <h1>
      Profile: {props.name}-{props.level}
    </h1>
  );
});

const HelloFriend = enhancedUserInfo(function (props) {
  return (
    <h1>
      HelloFriend: {props.name}-{props.level}
    </h1>
  );
});

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播1", "轮播2"]} />
        <Profile />
        <HelloFriend />

        <About />
      </div>
    );
  }
}

export default App;

hoc/enhanced_props.js

javascript
import { PureComponent } from "react";

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor(props) {
      super(props);

      this.state = {
        userInfo: {
          name: "coderwhy",
          level: 99,
        },
      };
    }

    render() {
      return <OriginComponent {...this.props} {...this.state.userInfo} />;
    }
  }

  return NewComponent;
}

export default enhancedUserInfo;

pages/About.jsx

javascript
import React, { PureComponent } from "react";
import enhancedUserInfo from "../hoc/enhanced_props";

export class About extends PureComponent {
  render() {
    return <div>About: {this.props.name}</div>;
  }
}

export default enhancedUserInfo(About);

利用高阶组件来共享 Context

App.jsx

javascript
import React, { PureComponent } from "react";
import ThemeContext from "./context/theme_context";
import Product from "./pages/Product";

export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: "red", size: 30 }}>
          <Product />
        </ThemeContext.Provider>
      </div>
    );
  }
}

export default App;

context/theme_context.js

javascript
import { createContext } from "react";

const ThemeContext = createContext();

export default ThemeContext;

pages/Product.jsx

javascript
import React, { PureComponent } from "react";
import ThemeContext from "../context/theme_context";
import withTheme from "../hoc/with_theme";

// export class Product extends PureComponent {
//   render() {
//     return (
//       <div>
//         Product:
//         <ThemeContext.Consumer>
//           {
//             value => {
//               return <h2>theme:{value.color}-{value.size}</h2>
//             }
//           }
//         </ThemeContext.Consumer>
//       </div>
//     )
//   }
// }

// export default Product

export class Product extends PureComponent {
  render() {
    const { color, size } = this.props;

    return (
      <div>
        <h2>
          Product: {color}-{size}
        </h2>
      </div>
    );
  }
}

export default withTheme(Product);

hoc/with_theme.js

javascript
import ThemeContext from "../context/theme_context";

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {(value) => {
          return <OriginComponment {...value} {...props} />;
        }}
      </ThemeContext.Consumer>
    );
  };
}

export default withTheme;

应用二 – 渲染判断鉴权

在开发中,我们可能遇到这样的场景:

某些页面是必须用户登录成功才能进行进入;

如果用户没有登录成功,那么直接跳转到登录页面;

这个时候,我们就可以使用高阶组件来完成鉴权操作:

App.jsx

javascript
import React, { PureComponent } from "react";
import Cart from "./pages/Cart";

export class App extends PureComponent {
  constructor() {
    super();

    // this.state = {
    //   isLogin: false
    // }
  }

  loginClick() {
    localStorage.setItem("token", "coderwhy");

    // this.setState({ isLogin: true })
    this.forceUpdate(); // 强制刷新,开发中不推荐
  }

  render() {
    return (
      <div>
        App
        <button onClick={(e) => this.loginClick()}>登录</button>
        <Cart />
      </div>
    );
  }
}

export default App;

pages/Cart.jsx

javascript
import React, { PureComponent } from "react";
import loginAuth from "../hoc/login_auth";

export class Cart extends PureComponent {
  render() {
    return <h2>Cart Page</h2>;
  }
}

export default loginAuth(Cart);

hoc/login_auth.js

javascript
function loginAuth(OriginComponent) {
  return (props) => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token");

    if (token) {
      return <OriginComponent {...props} />;
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>;
    }
  };
}

export default loginAuth;

应用三 – 生命周期劫持

我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

查看每个页面渲染花了多少时间。

App.sjx

javascript
import React, { PureComponent } from "react";
import Detail from "./pages/Detail";

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Detail />
      </div>
    );
  }
}

export default App;

pages/Detail.jsx

javascript
import React, { PureComponent } from "react";
import logRenderTime from "../hoc/log_render_time";

export class Detail extends PureComponent {
  // UNSAFE_componentWillMount() {
  //   this.beginTime = new Date().getTime()
  // }

  // componentDidMount() {
  //   this.endTime = new Date().getTime()
  //   const interval = this.endTime - this.beginTime
  //   console.log(`当前页面花费了${interval}ms渲染完成!`)
  // }

  render() {
    return (
      <div>
        <h2>Detail Page</h2>
        <ul>
          <li>数据列表1</li>
          <li>数据列表2</li>
          <li>数据列表3</li>
          <li>数据列表4</li>
          <li>数据列表5</li>
          <li>数据列表6</li>
          <li>数据列表7</li>
          <li>数据列表8</li>
          <li>数据列表9</li>
          <li>数据列表10</li>
        </ul>
      </div>
    );
  }
}

export default logRenderTime(Detail);

hoc/log_render_time.js

javascript
import { PureComponent } from "react";

function logRenderTime(OriginComponent) {
  return class extends PureComponent {
    // 匿名类
    UNSAFE_componentWillMount() {
      // 不推荐使用这个生命周期,这里只是为了测试
      this.beginTime = new Date().getTime();
    }

    componentDidMount() {
      this.endTime = new Date().getTime();
      const interval = this.endTime - this.beginTime;
      // OriginComponent.name可以获取传进来的组件的名字
      console.log(
        `当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`
      );
    }

    render() {
      return <OriginComponent {...this.props} />;
    }
  };
}

export default logRenderTime;

高阶函数的意义

我们会发现利用高阶组件可以针对某些 React 代码进行更加优雅的处理。

其实早期的 React 有提供组件之间的一种复用方式是 mixin,目前已经不再建议使用:

Mixin 可能会相互依赖,相互耦合,不利于代码维护;

不同的 Mixin 中的方法可能会相互冲突;

Mixin 非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;

当然,HOC 也有自己的一些缺陷:

HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;

HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;

Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题

比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;

后续我们还会专门来学习 hooks 相关的知识,敬请期待;

Portals 的使用

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM

元素上的)。

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;

第二个参数(container)是一个 DOM 元素;

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

比如说,我们准备开发一个 Modal 组件,它可以将它的子组件渲染到屏幕的中间位置:

步骤一:修改 index.html 添加新的节点

public/index.html

html
<body>
  <div id="root"></div>
  <div id="modal"></div>
</body>

步骤二:编写这个节点的样式

css
##modal {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

步骤三:编写组件代码

App.jsx

javascript
import React, { PureComponent } from "react";
import { createPortal } from "react-dom";
import Modal from "./Modal";

export class App extends PureComponent {
  render() {
    return (
      <div className="app">
        <h1>App H1</h1>
        {createPortal(<h2>App H2</h2>, document.querySelector("#why"))}

        {/* 2.Modal组件 */}
        <Modal>
          <h2>我是标题</h2>
          <p>我是内容, 哈哈哈</p>
        </Modal>
      </div>
    );
  }
}

export default App;

Modal.jsx

javascript
import React, { PureComponent } from "react";
import { createPortal } from "react-dom";

export class Modal extends PureComponent {
  render() {
    return createPortal(this.props.children, document.querySelector("#modal"));
  }
}

export default Modal;

fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:

我们又希望可以不渲染这样一个 div 应该如何操作呢?

使用 Fragment

Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;

React 还提供了 Fragment 的短语法:

它看起来像空标签 <> </>;

但是,如果我们需要在 Fragment 中添加 key,那么就不能使用短语法

javascript
import React, { PureComponent, Fragment } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      sections: [
        { title: "哈哈哈", content: "我是内容, 哈哈哈" },
        { title: "呵呵呵", content: "我是内容, 呵呵呵" },
        { title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
        { title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
      ]
    }
  }

  render() {
    const { sections } = this.state

    return (
      <>
        <h2>我是App的标题</h2>
        <p>我是App的内容, 哈哈哈哈</p>
        <hr />

        {
          sections.map(item => {
            return (
              {/*有key的情况下Fragment不能省略*/}
              <Fragment key={item.title}>
                <h2>{item.title}</h2>
                <p>{item.content}</p>
              </Fragment>
            )
          })
        }
      </>
    )
  }
}

export default App

StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具:

与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;

它为其后代元素触发额外的检查和警告;

严格模式检查仅在开发模式下运行;它们不会影响生产构建

可以为应用程序的任何部分启用严格模式:

不会对 Header 和 Footer 组件运行严格模式检查;

但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;

严格模式检查的是什么?

但是检测,到底检测什么呢?

1.识别不安全的生命周期:

2.使用过时的 ref API

3.检查意外的副作用

这个组件的 constructor 会被调用两次;

这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;

在生产环境中,是不会被调用两次的;

4.使用废弃的 findDOMNode 方法

在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,不过已经不推荐使用了,可以自行学习演练一下

5.检测过时的 context API

早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的;

目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法;

App.jsx

javascript
import React, { PureComponent, StrictMode } from "react";
// import { findDOMNode } from "react-dom"
import Home from "./pages/Home";
import Profile from "./pages/Profile";

export class App extends PureComponent {
  render() {
    return (
      <div>
        <StrictMode>
          <Home />
        </StrictMode>
        <Profile />
      </div>
    );
  }
}

export default App;

pages/Home.jsx

javascript
import React, { PureComponent } from "react";

export class Home extends PureComponent {
  // UNSAFE_componentWillMount() {
  //   console.log("Home UNSAFE_componentWillMount")
  // }

  constructor(props) {
    super(props);

    console.log("Home Constructor");
  }

  componentDidMount() {
    console.log("Home componentDidMount");
  }

  render() {
    console.log("Home Render");

    return (
      <div>
        {/* <h2 ref="title">Home Title</h2> */}

        <h2>Home</h2>
      </div>
    );
  }
}

export default Home;

哪个组件想开启严格模式,就在哪个组件外面包裹 StrictMode 即可。如果给 App 开启严格模式,就意味着对所有组件都开启严格模式。

严格模式下,使用过期的生命周期,比如 UNSAFE_componentWillMount,还有使用过时的 ref API 在控制台都会有警告提示。

严格模式下,Home 组件的 constructor,componentDidMount 和 render 都会执行两次,是故意执行 2 次的,为了检查意外的副作用。

后面安装 devtools 的插件,就可以看到,第 2 次的打印会有灰色的显示进行区分。