React基础笔记

React

crossorigin

  • 提示跨域错误的详细信息(不写默认没有详细信息)

  • console.dir()可以显示一个对象的所有属性和方法

React.createElement

  • 创建一个 React 元素,称作虚拟 DOM,本质上是一个对象
  • 参数 1:元素类型,如果是字符串,一个普通的 HTML 元素
  • 参数 2:元素的属性,一个对象
  • 后续参数:元素的子节点

JSX

  • JS 的扩展语法,需要使用 babel 进行转义。

yarn

yarn 最常用最基础的命令和 npm 对比

npm init / yarn init 初始化

mkdir 文件名 / md 文件名

npm install / yarn 或 yarn install 安装依赖

npm install package –save-dev / yarn add package –dev 安装某个依赖

npm uninstall package –save-dev / yarn remove package –dev 卸载某个依赖

npm run dev 或 npm start / yarn run start 或 yarn start 运行

react 脚手架

  1. create-react-app 脚手架(yarn create 会把脚手架名称 react-app 补全,后面接的是自定义名称)
  • yarn create react-app react-learn

  • npm install -g create-react-app

  • create-react-app my-app

  • 启动工程 yarn start

  1. 凡是使用 JSX 的文件,必须导入 React(注意:React 工程里面的.js 也可以写成.jsx)

JSX

  • 每一个 jsx 的表达多需要一个根节点,可以用<>代替根节点
const h1 = (
  <>
    <div>123</div>
  </>
);

ReactDOM.render(h1, document.getElementById('root'));

//其中<></>是一个语法糖,是<React.Fragment></<React.Fragment>
const a = 1234,
  b = 4321;
const div =
  (
    <div>
      {a} * {b} = {a * b}
    </div>
  ) - 上面的jsx语法就相当于下面直接用React.createElement;
const a = 1234,
  b = 4321;
const div = React.createElement('div', {}, `${a} * ${b} = ${a * b}`);
ReactDOM.render(div, document.getElementById('root'));
复制代码

什么是 JSX

  • Facebook 起草的 JS 扩展语法
  • 本质是一个 JS 对象,会被 bable 编译,最终会被转化为 createElement
  • 每个 JSX 表达式有且仅有一个根节点
  • React.Fragment
  • 每个 JSX 元素必须结束(XML 规范)

在 JSX 中嵌入表达式

  • 将表达式作为内容的一部分
    • null,undefined,false,true 不会显示
    • 普通对象,不可以作为子元素
    • 可以放置 React 元素对象(var span = 一个 span 元素)
    • 如果是数组,会把数组元素当作子元素渲染出来
  • 将表达式作为元素属性(
  • {i}
  • )

  • 属性使用小驼峰命名法(let cls = “image” <li key={i} className={cls} style={{marginLeft:’250px’}}>{i}
  • ) //{{}} 外面的{}表示 js 表达式 里面的表示对象

  • jsx 中 className 相当于 css 中 class
  • 防止注入攻击
  • 自动编码
  • dangerouslySetInnerHTML
//默认情况下jsx有保护机制,及不能将字符串转化html代码,如果需要转换,需要在元素上添加dangerouslySetInnerHTML
const content = '<h1>I Love you very much <p>Do you like me?</p></h1>';
const h1 = (
  <li>
    <div dangerouslySetInnerHTML>{content}</div>
  </li>
);
ReactDOM.render(h1, document.getElementById('root'));

//具体使用如下dangerouslySetInnerHTML={{__html:content}}
const h1 = (
  <li>
    <div
      dangerouslySetInnerHTML={{
        __html: content,
      }}
    ></div>
  </li>
);
复制代码

元素的不可变性

  • 虽然 JSX 元素是一个对象,但是该对象中的所有属性不可更改
let num = 0;
const div = <div title="asd">{num}</div>;
div.props.title = 'twd'; //报错属性不能更改
其实是Object.freeze(div); //冻结了这个属性
复制代码
  • 如果确实需要更改元素的属性,需要重新创建 JSX 元素
setInterval(() => {
  num++;
  const h1 = <div title="asd">{num}</div>;
  ReactDOM.render(h1, document.getElementById('root'));
}, 1000);
复制代码

demo 图片切换

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import demo1 from './assets/demo1.jpg';
import demo2 from './assets/demo2.jpg';
import demo3 from './assets/demo3.jpg';
let index = 0;
let arr = [demo1, demo2, demo3]; // console.log(demo1); ///static/media/demo1.e7e78cf4.jpg
const container = document.getElementById('root');
let timer;
function start() {
  stop();
  timer = setInterval(() => {
    index = (index + 1) % 3;
    const div = <img src={arr[index]} alt=""></img>;
    render(div);
  }, 1000);
}
function render(div) {
  ReactDOM.render(div, container);
}
function stop() {
  clearInterval(timer);
}
start();

container.onmouseenter = function () {
  stop();
};
container.onmouseleave = function () {
  start();
};
复制代码

组件和组件属性

  • 组件: 包含内容 样式和功能的 UI 单元

创建一个组件

特别注意:组件的名称首字母必须大写

否则不会识别为 React 组件元素(也就是不会进行函数调用),而是普通元素

  1. 函数组件

返回一个 React 元素

function MyFuncComp(){
    return <h1>组件内容</h1>
}
ReactDOM.render(
  <div> { MyFuncComp() }</div>,          // 第一种(不常用,调试器中不显示组件,不利于调试)
  <MyFuncComp number="123"></MyFuncComp> //一般这这样子写
  document.getElementById('root')
);
复制代码
  1. 类组件

必须继承 React.Component

import React from 'react';

export default class MyClassComp extends React.Component {
  /**
   *必须有render方法 该方法必须返回React元素
   *该方法是在MyClassComp原型上面的,因此可以使用this.props
   */
  render() {
    //return 123;
    return <h1>类组件</h1>;
  }
}
复制代码

必须提供 render 函数,用于渲染组件

组件属性

  1. 对于函数组件,属性会作为一个对象的属性,传递给函数的参数
<MyFuncComp number="123"></MyFuncComp>;
export default function (props) {
  console.log(props); //{number: "123"}
  return <h1>组件内容 {props.number}</h1>;
}
复制代码
  1. 对于类组件,属性会作为一个对象的属性,传递给构造函数的参数
ReactDOM.render(
  <div>
    {comp}
    <MyFuncComp number="123"></MyFuncComp>
    <MyClassComp pittle="love" obj={{ name: 'pittle', age: 18 }}></MyClassComp>
    <MyClassComp
      pittle="love"
      obj={{ name: 'pittle', age: 18 }}
      cmp={<h1>I love you</h1>}
    ></MyClassComp>
  </div>,
  document.getElementById('root')
);
export default class MyClassComp extends React.Component {
  constructor(props) {
    super(props); //React.Component的constructor就接受参数props实现this.props = props
    //super(props)相当于 React.Component.prototype.constructor.call(this)
    console.log(this.props, props); //{pittle: "love"}
  }
  /**
   * 该方法必须返回React元素,该方法由React自动调用
   */
  render() {
    //return this.props.pittle;
    return this.props.cmp;
  }
}
复制代码

注意:组件的属性,应该使用小驼峰命名法

组件无法改变自身的属性。
之前学习的 React 元素,本质上,就是一个组件(内置组件)

React 中的哲学:数据属于谁,谁才有权力改动(单向数据流 自顶而下)

Demo 显示学生列表

// 注意: await fetch()会返回的一个带返回值结果(resp.data)的promise对象(可以在浏览器上测试上面那行代码)

**index.js**
import React from 'react';
import ReactDOM from 'react-dom';
import StudentList from './components/StudentList';
const appkey = "demo13_1545210570249"
async function FetchData(){
    var stus = await fetch("http://open.duyiedu.com/api/student/findAll?appkey=" + appkey)
    .then(res => res.json()).then(resp => resp.data);  //复习Promise
    return stus;
}
async function render(){
  ReactDOM.render(
    <div>
       "正在加载中..."
    </div>,
    document.getElementById('root')
  );
  const stus = await FetchData();
  ReactDOM.render(
    <div>
       <StudentList stus={stus}></StudentList>
    </div>,
    document.getElementById('root')
  );
}
render();


**Student.js**

import React from 'react'
export default function Student(props) {
    // 假设所有的学生信息,都是分开你传递的
    return (
        <li>
            {/* 显示一个学生的所有数据 */}
            [姓名]:{props.name}
            [email]:{props.email}
            [性别]:{props.sex === 1 ? 'man' : 'woman'}
            [出生年月]:{props.birth}
        </li>
    )
}

** StudentList.js **
import React, { Component } from 'react'
import Student from "./Student"
export default class StudentList extends Component {
    render() {
        // 约定 props.stus传递的是学生的数组
        // 获取组件数组
        const students = this.props.stus.map(item => <Student key={item.id} {...item} />);
        console.log(students);
        return (
            <div>
                {students}
            </div>
        )
    }
}
复制代码

组件状态

组件状态: 组件可以自行维护的数据

组件状态仅在类组件中有效

状态(state),本质上是类组件的一个属性,是一个对象

状态初始化

状态变化

不能直接改变状态:因为 React 无法监控到状态发生了变化

必须使用 this.setState({})改变状态

一旦调用 this.setState({}) 会导致组件重新渲染

组件中的数据

  1. props: 该数据是由组件的使用者传递的数组,所有权不属于组件自身,因此组件无法改变该数据
  2. state: 该数据是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据
ReactDOM.render(
  <div>
    <Tick number={10} />
  </div>,
  document.getElementById('root')
);

export default class Tick extends Component {
  // 初始化状态: JS Next语法,目前处于试验阶段
  // this.state = {
  //     number:this.props.number
  // }
  constructor(props) {
    super(props);
    //初始化状态
    this.state = {
      number: this.props.number,
    };
    this.timer = setInterval(() => {
      this.setState({
        number: this.state.number - 1,
      });
      if (this.state.number === 0) {
        clearInterval(this.timer);
      }
    }, 1000);
  }
  render() {
    return <div>倒计时: {this.state.number}</div>;
  }
}
复制代码

Demo 自动移动的小球

ReactDOM.render(<BallList />, document.getElementById('root'));

import React, { Component } from 'react';

export default class Ball extends Component {
  constructor(props) {
    super(props);
    //初始化状态
    // 属性中需要分别传递横纵坐标的速度 每秒移动的像素值
    // props.xSpeed props.ySpeed
    // 需要传递背景颜色 如果没有传递,则使用红色
    this.state = {
      left: props.left || 0,
      top: props.top || 0,
      xSpeed: props.xSpeed,
      ySpeed: props.ySpeed,
    };
    const duration = 16;
    this.timer = setInterval(() => {
      const xSpeed = (this.state.xSpeed * duration) / 1000;
      const ySpeed = (this.state.ySpeed * duration) / 1000;
      let newLeft = this.state.left + xSpeed;
      let newTop = this.state.top + ySpeed;
      if (newLeft <= 0) {
        newLeft = 0;
        this.setState({
          xSpeed: -this.state.xSpeed, //横坐标反向
        });
      } else if (newLeft >= document.documentElement.clientWidth - 100) {
        newLeft = document.documentElement.clientWidth - 100;
        this.setState({
          xSpeed: -this.state.xSpeed,
        });
      }

      if (newTop <= 0) {
        newTop = 0;
        this.setState({
          ySpeed: -this.state.ySpeed, //纵坐标反向
        });
      } else if (newTop >= document.documentElement.clientHeight - 100) {
        newTop = document.documentElement.clientHeight - 100;
        this.setState({
          ySpeed: -this.state.ySpeed,
        });
      }
      this.setState({
        left: newLeft,
        top: newTop,
      });
      if (this.state.number === 0) {
        clearInterval(this.timer);
      }
    }, duration);
  }
  render() {
    return (
      <div
        className="ball"
        style={{
          left: this.state.left,
          top: this.state.top,
          background: this.props.bg || '#f40',
        }}
      ></div>
    );
  }
}
复制代码

事件

在 React 中,组件的事件,本质是一个属性

按照之前 React 对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法

function handClick() {
  console.log('点击');
}
//const btn = <button onClick={handClick}>点击</button>;
const btn = (
  <button
    onClick={() => {
      console.log('hello world');
    }}
    onMouseEnter={(e) => {
      console.log(e);
    }}
  >
    点击
  </button>
);

ReactDOM.render(btn, document.getElementById('root'));
复制代码

handleClick 中的 this 指向 undefined
如果没有特殊处理,在事件处理函数中,this 指向 undefined

//解决办法一:使用bind函数,绑定this
constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this);
        this.handleOver = this.handleOver.bind(this);
}
handleClick(){}
handleOver(){}
<Tick onClick={this.handleClick.bind(this)} />

//解决办法二:使用箭头函数(handleClick不在原型上,而在对象(实例)上,与之前写state一样是next语法)
handleClick = () => {}
handleOver = () => {}
render() {
  return (
  <Tick onClick={this.handleClick()} />
  )
}
复制代码

制作分页组件

深入认识 setState

setState,它对状态的改变,可能是异步的(这里是 html 事件)

如果改变状态的代码处于某个 HTML 元素的事件中,则其是异步,否则是同步(上面定时器的例子就是同步)

如果遇到某个事件中,需要同步调用多次,需要使用函数的方式得到最新状态

最佳实践:

  1. 把所有的 setState 当作是异步的
  2. 永远不要信任 setState 调用之后的状态
  3. 如果要使用改变之后的状态,需要使用回调函数(setState 的第二个参数是回调函数)
  4. 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState 第一个参数是函数)
state = {
  n:0
}
handleClick = () => {
  this.setState({
    n:this.state.n + 1
  });
  console.log(this.state.n); //先输出this.state.n 再执行render重新渲染

  // this.setState({
  //   n:this.state.n + 1
  // },() => {
  //   //状态改变之后触发,该函数运行在render函数之后
  //   console.log('compelete');
  // });

  //同时执行setState三次,最后渲染出来还是1   //事件中setState是异步的,不会立即改变state,所以每次都到的state.n多是0
  //因为每次拿到的this.state.n是一样的还是0
  // 所以执行三次都还是0+1
  // this.setState({
  //   n:this.state.n + 1
  // });
  // this.setState({
  //   n:this.state.n + 1
  // });

  //下面这种就可渲染三次,不过太恶心了
  // this.setState({
  //   n:this.state.n + 1
  // },() => {
  //   this.setState({
  //   n:this.state.n + 1
  //   },() => {
  //     this.setState({
  //       n:this.state.n + 1
  //     });
  //   });
  // });

  // setState的第一个参数还可以是函数
  // 这样写上三个this.setState,cur会保存上一个的状态
  // 源码可能是把setState的每个函数放到一个队列当中,每次执行一个,当所有的函数调用
  // 完之后,再改变最终的状态
  this.setState(cur => { //cur = this.state
    //参数cur表示当前state状态
    //该函数的返回结果 会混合(覆盖)掉之前的状态
    //该函数也是异步执行的
    return {
      n:cur.n + 1
    }
  },() => {
    //回调还是一样的
    //回调函数式在所有状态全部更新完成并且重新渲染后执行
   console.log('state更新完成', this.state.n); //3
  })
  this.setState(cur => ({n:cur.n + 1}),() => {
     //回调还是一样的
  })
  this.setState(cur => ({n:cur.n + 1}),() => {
     //回调还是一样的
  })
}
render(){
  console.log('render');
  return (<div>
  <h1>
  {this.state.n}
  </h1>
  <p><button onClick={this.handleClick}>+</button></p>
  </div>)
}
复制代码

React 会对异步的 setState 进行优化,将多次 setState 进行合并(将多次状态改变完成后,统一对 state 进行改变,触发一次 render)

constructor() {
    super();
    this.state = {
      val: 0,
    };
  }
  componentDidMount() {
    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val); //第一次结果0
    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val);//第二次结果0
    setTimeout(() => {
      console.log(this.state.val); //第三次结果1
      this.setState({ val: this.state.val + 1 });
      console.log(this.state.val);//第四次结果2
      this.setState({ val: this.state.val + 1 }); //第五次结果3
      console.log(this.state.val);
    }, 0);
  }
  //结果是0 0 1 2 3
  //说明this.setState在componentDidMount函数中是统一触发一次
  //进入到setTimeout函数中是同步执行
复制代码

生命周期

生命周期:组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。React 在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行.

生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁

旧版生命周期

  1. constructor
    同一个组件对象只会创建一次
    不能在第一次挂载到页面之前,调用 setState,为了避免问题,构造函数中严禁使用 setState

  2. componentWillMount
    正常情况下,和构造函数一样,它只会运行一次
    可以使用 setState,但是为了避免 bug,不允许使用,因为在某些特殊情况下,该函数可能被调用多次

  3. render
    返回一个虚拟 DOM,会被挂载到虚拟 DOM 树中,最终渲染到页面的真实 DOM 中
    render 可能不只运行一次,只要需要重新渲染,就会重新运行
    严禁使用 setState,因为可能会导致无限递归渲染

  4. componentDidMount
    只会执行一次
    可以使用 setState
    通常情况下,会将网络请求、启动计时器等一开始需要的操作,书写到该函数中
    组件进入活跃状态

  5. componentWillReceiveProps
    即将接收新的属性值
    参数为新的属性对象
    该函数可能会导致一些 bug,所以不推荐使用
    (只要属性重新被赋值就会触发,属性值不一定是变化的,可能两次被赋同一个值)

  6. shouldComponentUpdate
    指示 React 是否要重新渲染该组件,通过返回 true 和 false 来指定
    默认情况下,会直接返回 true

  7. componentWillUpdate
    组件即将被重新渲染

  8. componentDidUpdate
    往往在该函数中使用 dom 操作,改变元素

  9. componentWillUnmount
    通常在该函数中销毁一些组件依赖的资源,比如计时器

注意
如果组件自身的 state 更新了,那么会依次执行 shouldComponentUpdate、componentWillUpdate
render 和 componentDidUpdate
DOM 真正被添加到 HTML 中的生命周期方法是 componentDidMount 和 componentDidUpdate 方法。在这两个方法中,我们可以获取真正的 DOM 元素。在这两个方法中,我们可以获取真正的 DOM 元
素。React 提供的获取 DOM 元素的方法有两种,其中一种就是 ReactDOM 提供的 findDOMNode

componentDidMount() {
    console.log(ReactDOM.findDOMNode(this));
  }
复制代码

新版生命周期
React >= 16.0.0
React 官方认为,某个数据的来源必须是单一的

  1. getDerivedStateFromProps
    通过参数可以获取新的属性和状态
    该函数是静态的
    该函数的返回值会覆盖掉组件状态
    该函数几乎是没有什么用
  2. getSnapshotBeforeUpdate
    真实的 DOM 构建完成,但还未实际渲染到页面中。
    在该函数中,通常用于实现一些附加的 dom 操作
    该函数的返回值,会作为 componentDidUpdate 的第三个参数
export default class OldLifeCycle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      n: 0,
    };
    console.log('constructor', '一个新的组件诞生了!!!');
  }

  componentWillMount() {
    console.log('componentWillMount', '组件即将被挂载');
  }

  componentDidMount() {
    console.log('componentDidMount', '挂载完成');
  }

  componentWillReceiveProps(nextProps) {
    console.log(
      'componentWillReceiveProps',
      '接收到新的属性值',
      this.props,
      nextProps
    );
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log(
      'shouldComponentUpdate',
      '是否应该重新渲染',
      this.props,
      nextProps,
      this.state,
      nextState
    );
    if (this.props.n === nextProps.n && this.state.n === nextState.n) {
      return false;
    }
    return true;
    // return false;
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('componentWillUpdate', '组件即将被重新渲染');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log(
      'componentDidUpdate',
      '组件已完成重新渲染',
      prevProps,
      prevState
    );
  }

  componentWillUnmount() {
    console.log('componentWillUnmount', '组件被销毁');
  }

  render() {
    console.log('render', '渲染,返回的React元素会被挂载到虚拟DOM树中');
    return (
      <div>
        <h1>旧版生命周期组件</h1>
        <h2>属性n: {this.props.n}</h2>
        <h2>状态n:{this.state.n}</h2>
        <button
          onClick={() => {
            this.setState({
              n: this.state.n + 1,
            });
          }}
        >
          状态n+1
        </button>
      </div>
    );
  }
}
复制代码

传递元素内容

内置组件:div、h1、p…

<div>asdfafasfafasdfasdf</div>
复制代码
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Comp from './Comp';
ReactDOM.render(
  <Comp content1={<h2>第2组元素内容</h2>} content2={<h2>第3组元素内容</h2>}>
    <h2>第1组元素内容</h2>
  </Comp>,
  document.getElementById('root')
);
//Comp.js
import React from 'react';
export default function Comp(props) {
  console.log(props);
  return (
    <div className="comp">
      <h1>组件自身的内容</h1>
      {/* {props.children || <h1>默认值</h1>} */}
      {props.children}
      {props.content1}
      {props.content2}
    </div>
  );
}
复制代码

如果给自定义组件传递元素内容,则 React 会将元素内容作为 children 属性传递过去。

制作蒙层组件

// Test.js
import React, { Component } from 'react';
import Model from './common/Model';

export default class Test extends Component {
  state = {
    showModel: false,
  };

  showModel = () => {
    this.setState({
      showModel: true,
    });
  };

  hideModel = () => {
    this.setState({
      showModel: false,
    });
  };

  render() {
    return (
      <div>
        <img
          src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xMS41IC0xMC4yMzE3NCAyMyAyMC40NjM0OCI+CiAgPHRpdGxlPlJlYWN0IExvZ288L3RpdGxlPgogIDxjaXJjbGUgY3g9IjAiIGN5PSIwIiByPSIyLjA1IiBmaWxsPSIjNjFkYWZiIi8+CiAgPGcgc3Ryb2tlPSIjNjFkYWZiIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIi8+CiAgICA8ZWxsaXBzZSByeD0iMTEiIHJ5PSI0LjIiIHRyYW5zZm9ybT0icm90YXRlKDYwKSIvPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIiB0cmFuc2Zvcm09InJvdGF0ZSgxMjApIi8+CiAgPC9nPgo8L3N2Zz4K"
          alt=""
          width="500"
          height="500"
        />
        {this.state.showModel ? (
          <Model onClose={this.hideModel}>
            <div
              style={{
                background: '#fff',
              }}
            >
              <h1>asdfasfasfasfasdfasdf</h1>
              <button onClick={this.hideModel}>关闭朦层</button>
            </div>
          </Model>
        ) : null}
        <button onClick={this.showModel}>显示朦层</button>
      </div>
    );
  }
}

// Model.js
import React from 'react';
import './index.css';

export default function Model(props) {
  var defaultProps = {
    //默认属性
    bg: 'rgba(0,0,0,.5)',
  };
  var datas = Object.assign({}, defaultProps, props);

  return (
    <div
      onClick={(e) => {
        if (e.target.className === 'model') {
          //防止事件冒泡,点击弹窗的时候冒泡也会执行onClick
          datas.onClose();
        }
      }}
      className="model"
      style={{
        background: datas.bg,
      }}
    >
      <div className="model-center">{datas.children}</div>
    </div>
  );
}
复制代码

表单

受控组件和非受控组件

受控组件:组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全受到属性的控制。

非受控组件:组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制。

表单组件,默认情况下是非受控组件,一旦设置了表单组件的 value 属性,则其变为受控组件(单选和多选框需要设置 checked)

{/* 默认情况下,它是一个非受控组件 */}
{/* <input type="text" /> */}
//受控组件
<input
  type="text"
  value={this.state.val}
  onChange={(e) => {
    this.setState({
      val: e.target.value,
    });
  }}
/>

 state = {
    val: '123',
    // checked: true
    loves: ['足球', '篮球', '音乐', '其他'],
    chooseLoves: ['篮球', '音乐'],
    selVal: 'beijing',
  };
const checkboxes = this.state.loves.map((it) => (
      <label key={it}>
        <input
          type="checkbox"
          checked={this.state.chooseLoves.includes(it)}
          onChange={(e) => {
            if (e.target.checked) {
              this.setState({
                chooseLoves: [...this.state.chooseLoves, it], //添加
              });
            } else {
              this.setState({
                chooseLoves: this.state.chooseLoves.filter((l) => l !== it), //移除
              });
            }
          }}
        />
        {it}
      </label>
复制代码

封装表单组件

属性

  1. datas:一个数组,数组每一项是一个对象,对应一个多选框
    1. 对象具有 value 和 text 属性
    2. value:多选框的值
    3. text:多选框的文本

例如:

datas = [
  { value: 'football', text: '足球' },
  { value: 'basketball', text: '篮球' },
  { value: 'movie', text: '电影' },
];
复制代码
  1. name:每一个多选框的 name 属性值
  2. value: 当前选中的 value 值
  3. onChange:当选中项发生改变时的事件

属性默认值 和 类型检查

属性默认值
通过一个静态属性 defaultProps 告知 react 属性默认值

属性类型检查
使用库:prop-types

 //commonTypes.js
import PropTypes from "prop-types"

export default {
    children: PropTypes.node,
    groupDatas: PropTypes.arrayOf(PropTypes.shape({
        value: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired
    })), //多选框组、单选框组、下拉列表的数据源
    chooseDatas: PropTypes.arrayOf(PropTypes.string),
}

import types from "../../../utils/commonTypes"
/**
 * 默认属性值
 */
static defaulProps = {
    datas: [],
    value: ""
}

static propTypes = {
    datas: types.groupDatas.isRequired,
    name: PropTypes.string.isRequired,
    value: PropTypes.string.isRequired,
    onChange: PropTypes.func
}
复制代码

对组件使用静态属性 propTypes 告知 react 如何检查属性

PropTypes.any://任意类型
PropTypes.array://数组类型
PropTypes.bool://布尔类型
PropTypes.func://函数类型
PropTypes.number://数字类型
PropTypes.object://对象类型
PropTypes.string://字符串类型
PropTypes.symbol://符号类型

a:PropTypes.number.isRequired//表示a属性必填不能为null和undefined
//PropTypes.number其实是一个函数 可以用console.dir打印
// PropTypes.number.isRequired也是一个函数

PropTypes.node://任何可以被渲染的内容,字符串、数字、React 元素
PropTypes.element://react 元素
PropTypes.elementType://react 元素类型
PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
PropTypes.oneOf([xxx, xxx])://枚举
PropTypes.oneOfType([xxx, xxx]); //属性类型必须是数组中的其中一个
PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
PropTypes.exact({...})://对象必须精确匹配传递的数据
//自定义属性检查,如果有错误,返回错误对象即可
属性: function(props, propName, componentName) {
//...
}
复制代码

HOC 高阶组件

HOF:Higher-Order Function 高阶函数 以函数作为参数 并返回一个函数
HOC:Higher-Order Component 高阶组件 以组件作为参数 并返回一个组件

function Comp(props){
  return <h1>aaaa</h1>
}
class Comp{}

<Comp /> React元素
<h1></h1>  React Html Element
复制代码

ref

reference: 引用

场景:希望直接使用 dom 元素中的某个方法,或者希望直接使用自定义组件中的某个方法

  1. ref 作用于内置的 html 组件,得到的将是真实的 dom 对象
  2. ref 作用于类组件,得到的将是类的实例
  3. ref 不能作用于函数组件
  4. ref 不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出

字符串

import React, { Component } from 'react';

class A extends Component {
  method() {
    console.log('调用了组件A的方法');
  }
  render() {
    return <h1>组件A</h1>;
  }
}

// function B(){
//     return <h1>组件B</h1>
// }

export default class Comp extends Component {
  handleClick = () => {
    console.log(this);
    this.refs.txt.focus();
    this.refs.compA.method();
  };

  render() {
    return (
      <div>
        <input ref="txt" type="text" />
        <A ref="compA" />
        {/* <B ref="compB" /> */}
        <button onClick={this.handleClick}>聚焦</button>
      </div>
    );
  }
}
复制代码

目前,ref 推荐使用对象或者是函数

对象

export default class Comp extends Component {
  constructor(props) {
    super(props);
    this.txt = React.createRef(); //相当于this.txt = {current:null}
    //这里直接使用this.txt = {current:null}也是可以的
    // React.createRef()只不过是做了效率的优化,保持current不变
    //执行render函数的时候会自动给current属性赋值
  }
  handleclick = () => {
    console.log(this);
    console.log(this.txt); //{current: input}
    this.txt.current.focus();
  };
  render() {
    return (
      <div>
        <input ref={this.txt} type="text" />
        <button onClick={this.handleclick}>聚焦</button>
      </div>
    );
  }
}
复制代码

通过 React.createRef 函数创建

函数

export default class Comp extends Component {
  constructor(props) {
    super(props);
  }
  handleclick = () => {
    console.log(this);
    this.setState({});
    this.txt.focus();
  };
  componentDidMount() {
    console.log(this.txt, 'didmount');
  }
  render() {
    return (
      <div>
        <input
          ref={(el) => {
            //刚开始渲染的时候就会执行这个函数,并且是在componentDidMount之前执行
            console.log(el, '111'); //每次执行this.setState({});这里会执行两次 第一次el为null,第二次el为input对象
            this.txt = el;
          }}
          type="text"
        />
        <button onClick={this.handleclick}>聚焦</button>
      </div>
    );
  }
}
复制代码
export default class Comp extends Component {
  constructor(props) {
    super(props);
    // this.txt = React.createRef();
  }
  handleclick = () => {
    console.log(this);
    this.setState({});
    this.txt.focus();
  };
  componentDidMount() {
    console.log(this.txt, 'didmount');
  }
  getRef = (el) => {
    //这里ref就没有变动所以只有在初次渲染执行render函数的时候执行一次,其他时候不会执行的
    console.log(el, '111');
    this.txt = el;
  };

  render() {
    return (
      <div>
        <input ref={this.getRef} type="text" />
        <button onClick={this.handleclick}>聚焦</button>
      </div>
    );
  }
}
复制代码

函数的调用时间:

componentDidMount 的时候会调用该函数
在 componentDidMount 事件中可以使用 ref
如果 ref 的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在 componentDidUpdate 之前
旧的函数被调用时,传递 null
新的函数被调用时,传递对象
如果 ref 所在的组件被卸载,会调用函数

谨慎使用 ref

能够使用属性和状态进行控制,就不要使用 ref。

调用真实的 DOM 对象中的方法
某个时候需要调用类组件的方法

import React, { Component } from 'react';

export default class Comp extends Component {
  state = {
    show: true,
  };

  handleClick = () => {
    // this.txt.focus();
    this.setState({
      show: !this.state.show,
    });
  };

  componentDidMount() {
    console.log('didMount', this.txt);
  }

  getRef = (el) => {
    console.log('函数被调用了', el);
    this.txt = el;
  };

  render() {
    return (
      <div>
        {this.state.show && <input ref={this.getRef} type="text" />}
        <button onClick={this.handleClick}>显示/隐藏</button>
      </div>
    );
  }
}
复制代码

ref 转发

forwardRef

forwardRef 方法:

参数,传递的是函数组件,不能是类组件,并且,函数组件需要有第二个参数来得到 ref
返回值,返回一个新的组件

refs

  • 值得注意的是,在 React 中使用 DOM 原生事件时,一定要在组件卸载时手动移除,否则很
    可能出现内存泄漏的问题。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class NativeEventDemo extends Component {
  componentDidMount() {
    this.refs.button.addEventListener('click', (e) => {
      this.handleClick(e);
    });
  }
  handleClick(e) {
    console.log(e);
  }
  componentWillUnmount() {
    this.refs.button.removeEventListener('click');
  }
  render() {
    return <button ref="button">Test</button>;
  }
}
复制代码
import React, { Component } from 'react';
function A(props, ref) {
  return (
    <h1 ref={ref}>
      组件A
      <span>{props.word}</span>
    </h1>
  );
}
// 传递函数组件A 得到一个新的组件NewA
const NewA = React.forwardRef(A);
export default class App extends React.Component {
  ARef = React.createRef();
  componentDidMount() {
    console.log(this.ARef);
  }
  render() {
    return (
      <div>
        <NewA ref={this.ARef} word="props属性" />
      </div>
    );
  }
}
复制代码

类组件传递 ref 通过普通属性

//方法1
class A extends React.Component {
  render() {
    return (
      <h1 ref={this.props.ref1}>
        组件A
        <span>{this.props.word}</span>
      </h1>
    );
  }
}
export default class App extends React.Component {
  ARef = React.createRef();
  componentDidMount() {
    console.log(this.ARef);
  }
  render() {
    return (
      <div>
        <A ref1={this.ARef} word="props属性" />
      </div>
    );
  }
}
// 方法2
class A extends React.Component {
  render() {
    return (
      <h1 ref={this.props.ref1}>
        组件A
        <span>{this.props.word}</span>
      </h1>
    );
  }
}
// 传递函数组件A 得到一个新的组件NewA
const NewA = React.forwardRef((props, ref) => {
  return <A {...props} ref1={ref} />;
});
export default class App extends React.Component {
  ARef = React.createRef();
  componentDidMount() {
    console.log(this.ARef);
  }
  render() {
    return (
      <div>
        <NewA ref={this.ARef} word="props属性" />
      </div>
    );
  }
}
复制代码

Context

上下文:Context,表示做某一些事情的环境

React 中的上下文特点:

当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性 props)
一般情况下,用于第三方组件(通用组件)

旧的 API

创建上下文

只有类组件才可以创建上下文

给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。
使用上下文中的数据

要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型

可以在组件的构造函数中,通过第二个参数,获取上下文数据
从组件的 context 属性中获取
在函数组件中,通过第二个参数,获取上下文数据
上下文的数据变化

上下文中的数据不可以直接变化,最终都是通过状态改变

在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据

import React, { Component } from 'react';
import PropTypes from 'prop-types';

const types = {
  a: PropTypes.number,
  b: PropTypes.string.isRequired,
  onChangeA: PropTypes.func,
};

class ChildA extends Component {
  static contextTypes = types;

  static childContextTypes = {
    a: PropTypes.number,
    c: PropTypes.string,
  };

  getChildContext() {
    return {
      a: 789,
      c: 'hello',
    };
  }

  render() {
    return (
      <div>
        <h1>ChildA</h1>
        <h2>
          a:{this.context.a},b:{this.context.b}
        </h2>
        <ChildB />
      </div>
    );
  }
}

class ChildB extends React.Component {
  /**
   * 声明需要使用哪些上下文中的数据
   */
  static contextTypes = {
    ...types,
    c: PropTypes.string,
  };

  render() {
    return (
      <p>
        ChildB,来自于上下文的数据:a: {this.context.a}, b:{this.context.b}
        ,c: {this.context.c}
        <button
          onClick={() => {
            this.context.onChangeA(this.context.a + 2);
          }}
        >
          子组件的按钮,a+2
        </button>
      </p>
    );
  }
}

export default class OldContext extends Component {
  /**
   * 约束上下文中数据的类型
   */
  static childContextTypes = types;

  state = {
    a: 123,
    b: 'abc',
  };

  /**
   * 得到上下文中的数据
   */
  getChildContext() {
    console.log('获取新的上下文');
    return {
      a: this.state.a,
      b: this.state.b,
      onChangeA: (newA) => {
        this.setState({
          a: newA,
        });
      },
    };
  }

  render() {
    return (
      <div>
        <ChildA />
        <button
          onClick={() => {
            this.setState({
              a: this.state.a + 1,
            });
          }}
        >
          a加1
        </button>
      </div>
    );
  }
}
复制代码

新版 API

旧版 API 存在严重的效率问题,并且容易导致滥用

创建上下文

上下文是一个独立于组件的对象,该对象通过 React.createContext(默认值)创建

返回的是一个包含两个属性的对象

Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值

同一个 Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次
Consumer 属性:后续讲解
使用上下文中的数据

在类组件中,直接使用 this.context 获取上下文数据
要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
在函数组件中,需要使用 Consumer 来获取上下文数据
Consumer 是一个组件
它的子节点,是一个函数(它的 props.children 需要传递一个函数)
注意细节

如果,上下文提供者(Context.Provider)中的 value 属性发生变化(Object.is 比较),会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化(无论 shouldComponentUpdate 函数返回什么结果),会跳过 shouldComponentUpdate 进行强制重新渲染

eg1:

import React, { Component } from 'react';

const ctx = React.createContext();
class ChildB extends React.Component {
  //写法一:
  // static contextType = ctx;
  // render() {
  //   return (
  //     <h1>
  //       a:{this.context.a},b:{this.context.b}
  //       <button
  //         onClick={() => {
  //           this.context.changeA(this.context.a + 2);
  //         }}
  //       >
  //         后代组件的按钮,点击a+2
  //       </button>
  //     </h1>
  //   );
  // }
  //写法二:
  render() {
    return (
      <ctx.Consumer>
        {(value) => (
          <h1>
            a:{this.context.a},b:{value.b}
            <button
              onClick={() => {
                this.context.changeA(value.a + 2);
              }}
            >
              后代组件的按钮,点击a+2
            </button>
          </h1>
        )}
      </ctx.Consumer>
    );
  }
}

function ChildA(props) {
  return (
    <div>
      <h1>childA</h1>
      <h2>
        <ctx.Consumer>
          {(value) => (
            <>
              {value.a},{value.b}
            </>
          )}
        </ctx.Consumer>
      </h2>
      <div>
        <ChildB />
      </div>
    </div>
  );
}

export default class NewContext extends Component {
  state = {
    a: 0,
    b: 'abc',
    changeA: (newA) => {
      this.setState({
        a: newA,
      });
    },
  };

  render() {
    console.log(this.state);
    return (
      <ctx.Provider value={this.state}>
        <div>
          <ChildA />

          <button
            onClick={() => {
              this.setState({
                a: this.state.a + 1,
              });
            }}
          >
            父组件的按钮,a加1
          </button>
        </div>
      </ctx.Provider>
    );
  }
}
复制代码

eg2:如果 ctx 是对象,改变 ctx.a 需要重新改变这个对象(下面的例子是憨批写法,以后别看了)

import React, { Component } from 'react';

const ctx = React.createContext();
class ChildB extends React.Component {
  static contextType = ctx;

  // shouldComponentUpdate(nextProps, nextState) {
  //   console.log('运行了优化');
  //   return false;
  // }

  render() {
    console.log(this.context, 'context');
    return (
      <h1>
        a:{this.context.a},b:{this.context.b}
        <button
          onClick={() => {
            this.context.changeA(this.context.a + 2);
          }}
        >
          后代组件的按钮,点击a+2
        </button>
      </h1>
    );
  }
}

export default class NewContext extends Component {
  state = {
    ctx: {
      a: 0,
      b: 'abc',
      changeA: (newA) => {
        this.setState({
          ctx: {
            a: newA,
            b: this.state.ctx.b,
            changeA: (newA) => {
              this.setState({
                ctx: {
                  a: newA,
                  b: this.state.ctx.b,
                  changeA: this.state.ctx.changeA,
                },
              });
            },
          },
        });
      },
    },
  };

  render() {
    console.log(this.state);
    return (
      <ctx.Provider value={this.state.ctx}>
        <div>
          <ChildB />
          <button
            onClick={() => {
              this.setState({
                ctx: {
                  a: this.state.ctx.a + 1,
                  b: this.state.ctx.b,
                },
              });
            }}
          >
            父组件的按钮,a加1
          </button>
        </div>
      </ctx.Provider>
    );
  }
}
复制代码

eg3:每次 this.setState({})会给 state 设置一个新的对象,这个新的对象会继承更新前的 state 属性,并且同名新的属性会覆盖旧的,可能就是 Object.assign(新的 state,旧的 state)
<ctx.Provider value={this.state}>的 value 属性比较的是 this.state 的引用,所以 state 的引用变化会导致上下文刷新

import React, { Component } from 'react';

const ctx = React.createContext();

function ChildA(props) {
  return (
    <div>
      <h1>childA</h1>
      <h2>
        <ctx.Consumer>
          {(value) => (
            <>
              {value.a},{value.b}
            </>
          )}
        </ctx.Consumer>
      </h2>
    </div>
  );
}

export default class NewContext extends Component {
  datas = [
    {
      a: 0,
      b: 'abc',
      changeA: (newA) => {
        this.setState({
          a: newA,
        });
      },
    },
  ];

  state = this.datas[0];

  render() {
    console.log(this.state);
    return (
      <ctx.Provider value={this.state}>
        <div>
          <ChildA />

          <button
            onClick={() => {
              this.setState({}, () => {
                this.datas.push(this.state);
                console.log(this.datas[0] === this.datas[1]);
              });
            }}
          >
            父组件的按钮,a加1
          </button>
        </div>
      </ctx.Provider>
    );
  }
}
复制代码

PureComponent

纯组件,用于避免不必要的渲染(运行 render 函数)

优化:如果一个组件的属性和状态都没有发生变化,重新渲染该组件是没有必要的

PureComponent 是一个组件,如果某个组件继承改组件,则该组件的 shouldComponentUpdate 会进行优化,对属性和状态进行钱比较,如果相等则不会重新渲染

相当于下面的代码

shouldComponentUpdate(nextProps, nextState) {
    //参数:新的state和新的props。由于每次重新渲染,prop都会被赋上新的对象
    // 所以shouldComponentUpdate的这个函数是在重新渲染之前比较一下,新的state/props和旧的state/props的区别
    if (
      ObjectEqual(nextProps, this.props) &&
      ObjectEqual(this.state, nextState)
    ) {
      return false;
    }
    return true;
}

// 浅比较
// export function ObjectEqual(obj1, obj2) {
//   for (let prop in obj1) {
//     if (!Object.is(obj1[prop], obj2[prop])) {
//       return false;
//     }
//   }
//   return true;
// }
复制代码

注意

  1. PureComponent 是浅比较
    为了效率 应该尽量使用 PureComponent
    要求不要改动之前的状态 永远是创建新的状态覆盖之前的状态(Immutable,不可变对象)
    有一个第三方 js 库,Immutable.js,它专门用于制作不可变对象
  2. 函数组件, 使用 React.memo 函数制作纯组件
function Task(props) {
  console.log('Task');
  return <li className={props.isFinish ? 'isFinish' : ''}>{props.name}</li>;
}
Task.propTypes = {
  name: propTypes.string.isRequired,
  isFinish: propTypes.bool.isRequired,
};

export default React.memo(Task);
复制代码

React.memo 原理(就是一个高阶组件)

function memo(FunComp) {
  return class Memo extends PureComponent {
    // render(){
    //   return <FunComp {...this.props}/>;
    // }
    render() {
      return <>{FunComp(this.props)}</>;
    }
  };
}
复制代码

注意:避免下面这种写法
this.state.tasks.push(newTask)会在之前的 tasks 数组中添加一项,然后把之前的数组重新渲染
所以,数组的引用是没有变化的,如果把这个 tasks 作为属性传给 TaskList 这个 PureComponent 组件,
由于 tasks 引用没有变化,所以 TaskList 组件就不会重新渲染

state = {
  tasks: [],
};
handleAdd = (newTask) => {
  this.state.tasks.push(newTask);
  this.setState({
    tasks: this.state.tasks,
  });
};
<TaskList tasks={this.state.tasks} />;
复制代码
handleAdd = (newTask) => {
    this.setState({
      tasks: [...this.state.tasks, newTask],
    });
};
<AddTask onAdd={this.handleAdd} />
//下面这种写法和上面写法的区别在于,下面的写法每次重新渲染属性onAdd函数是不同的引用,上面的写法每次都是同一个函数引用
<AddTask onAdd={(newTask) => {
    this.setState({
      tasks: [...this.state.tasks, newTask],
    });
}} />

复制代码

render props

有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式任选其一来解决重复代码的问题(横切关注点)

  1. render props
    某个组件,需要某个属性
    该属性是一个函数,函数的返回值用于渲染
    函数的参数会传递为需要的数据
    注意
    纯组件的属性(尽量避免每次传递的 render props 的地址不一致)
    通常该属性的名字叫做 render
  2. HOC
import MouseListener from './MouseListener';
import React from 'react';

const renderPoint = (mouse) => (
  <>
    横坐标:{mouse.x},纵坐标:{mouse.y}
  </>
);
const renderDiv = (mouse) => (
  <>
    <div
      style={{
        width: 100,
        height: 100,
        background: '#008c8c',
        position: 'absolute',
        left: mouse.x - 50,
        top: mouse.y - 50,
      }}
    ></div>
  </>
);

export default function Test() {
  return (
    <div>
      <MouseListener render={renderPoint} />
      <MouseListener render={renderDiv} />
    </div>
  );
}
复制代码
import React, { PureComponent } from 'react';
import './style.css';

/**
 * 该组件用于监听鼠标的变化
 */
export default class MouseListener extends PureComponent {
  state = {
    x: 0,
    y: 0,
  };

  divRef = React.createRef();

  handleMouseMove = (e) => {
    //更新x和y的值
    const { left, top } = this.divRef.current.getBoundingClientRect();
    const x = e.clientX - left;
    const y = e.clientY - top;
    this.setState({
      x,
      y,
    });
  };

  render() {
    return (
      <div
        ref={this.divRef}
        className="point"
        onMouseMove={this.handleMouseMove}
      >
        {this.props.render ? this.props.render(this.state) : '默认值'}
      </div>
    );
  }
}
复制代码

Portals

插槽:将一个 React 元素渲染到指定 DOM 容器中

ReactDOM.createPortal(React 元素, 真实的 DOM 容器),该函数返回一个 React 元素

import React from 'react';
import ReactDOM from 'react-dom';

function ChildA() {
  return ReactDOM.createPortal(
    <div
      className="child-a"
      style={{
        marginTop: 200,
      }}
    >
      <h1>ChildA</h1>
      <ChildB />
    </div>,
    document.querySelector('.modal')
  );
}

function ChildB() {
  return (
    <div className="child-b">
      <h1>ChildB</h1>
    </div>
  );
}

export default function App() {
  return (
    <div
      className="app"
      onClick={(e) => {
        console.log('App被点击了', e.target);
      }}
    >
      <h1>App</h1>
      <ChildA />
    </div>
  );
}
复制代码

注意事件冒泡
React 中的事件是包装过的
它的事件冒泡是根据虚拟 DOM 树来冒泡的,与真实的 DOM 树无关。

错误边界

默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载

错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播

让某个组件捕获错误

编写生命周期函数 getDerivedStateFromError (DOM has not yet been updated)
静态函数
运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
注意:只有子组件发生错误,才会运行该函数
该函数返回一个对象,React 会将该对象的属性覆盖掉当前组件的 state
参数:错误对象
通常,该函数用于改变状态

编写生命周期函数 componentDidCatch (DOM has already been updated)
参数:错误对象,错误信息
实例方法
运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
通常,该函数用于记录错误消息(该函数是卸载所有组件后再重新构造组件重新渲染)

getDerivedStateFromError:渲染备用 UI
componentDidCatch:打印错误信息

import React, { PureComponent } from 'react';

export default class ErrorBound extends PureComponent {
  state = {
    hasError: false,
  };

  static getDerivedStateFromError(error) {
    console.log('发生错误了', error);
    return {
      hasError: true,
    };
  }

  // componentDidCatch(error, info) {
  //   console.log('记录错误信息');
  // }

  render() {
    // setTimeout(() => {
    //     throw new Error("asfasdfasf");
    // }, 1000);
    if (this.state.hasError) {
      return <h1>出现错误了!</h1>;
    }
    return this.props.children;
  }
}
复制代码

细节

某些错误,错误边界组件无法捕获

自身的错误
异步的错误
事件中的错误
服务端渲染
总结:仅处理渲染子组件期间的同步错误

React 中的事件

这里的事件:React 内置的 DOM 组件中的事件

给 document 注册事件
几乎所有的元素的事件处理,均在 document 的事件中处理
一些不冒泡的事件,是直接在元素上监听
一些 document 上面没有的事件,直接在元素上监听
在 document 的事件处理,React 会根据虚拟 DOM 树的完成事件函数的调用
React 的事件参数,并非真实的 DOM 事件参数,是 React 合成的一个对象,该对象类似于真实 DOM 的事件参数
stopPropagation,阻止事件在虚拟 DOM 树中冒泡
nativeEvent,可以得到真实的 DOM 事件对象
为了提高执行效率,React 使用事件对象池来处理事件对象

import React from 'react';
export default function App() {
  return (
    <div>
      <button
        onClick={(e) => {
          console.log('react1: 按钮被点击了', e.nativeEvent);
          e.nativeEvent.stopPropagation();
        }}
      >
        按钮
      </button>
    </div>
  );
}

document.querySelector('#root').onclick = function (e) {
  console.log('root: 阻止onclick事件冒泡');
  e.stopPropagation();
};

document.onclick = function (e) {
  console.log('document: 阻止onclick事件冒泡');
  // e.stopPropagation();
};
复制代码

注意事项

如果给真实的 DOM 注册事件,阻止了事件冒泡,则会导致 react 的相应事件无法触发
如果给真实的 DOM 注册事件,事件会先于 React 事件运行
通过 React 的事件中阻止事件冒泡,无法阻止真实的 DOM 事件冒泡
可以通过 nativeEvent.stopImmediatePropagation(),阻止 document 上剩余事件的执行
在事件处理程序中,不要异步的使用事件对象,如果一定要使用,需要调用 persist 函数

import React from 'react';
var prev;
export default function App() {
  return (
    <div
      onClick={(e) => {
        console.log(prev === e);
        console.log('react: div被点击了');
      }}
    >
      <input
        type="text"
        onFocus={(e) => {
          console.log('react:文本获得了焦点');
        }}
      />
      <button
        onClick={(e) => {
          console.log('react: 按钮被点击了');
          prev = e;
          e.persist();
          setTimeout(() => {
            console.log(e.type);
          }, 1000);
          // e.nativeEvent.stopImmediatePropagation()
          // console.log(e.isPropagationStopped());
          // e.stopPropagation();
          // console.log(e.isPropagationStopped());
        }}
      >
        按钮
      </button>
    </div>
  );
}

document.querySelector('#root').onFocus = function (e) {
  console.log('阻止focus事件冒泡');
  e.stopPropagation();
};
复制代码

渲染过程

渲染:生成用于显示的对象,以及将这些对象形成真实的 DOM 对象

  • React 元素:React Element,通过 React.createElement 创建(语法糖:JSX)
    例如:
<div><h1>标题</h1></div>
<App />
复制代码
  • React 节点:专门用于渲染到 UI 界面的对象,React 会通过 React 元素,创建 React 节点,ReactDOM 一定是通过 React 节点来进行渲染的

  • 节点类型:
    React DOM 节点:创建该节点的 React 元素类型是一个字符串(“h1″,”p”)
    React 组件节点:创建该节点的 React 元素类型是一个函数或是一个类
    React 文本节点:由字符串、数字创建的
    React 空节点:由 null、undefined、false、true
    React 数组节点:该节点由一个数组创建
    真实 DOM:通过 document.createElement 创建的 dom 元素

import React from 'react';
class CompA extends React.Component {
  state = {
    a: 123,
    b: 'abc',
  };

  componentDidUpdate(prevProps, prevState) {
    console.log('CompA', prevProps, prevState, this.state);
    console.log('CompA componentDidUpdate');
  }

  render() {
    return (
      <div>
        <h1>{this.state.a}</h1>
        <CompB n={this.state.b} />
        <button
          onClick={() => {
            this.setState({
              a: 321,
              b: 'cba',
            });
          }}
        >
          点击
        </button>
      </div>
    );
  }
}

function CompB(props) {
  return (
    <div>
      <h1 id="title">{props.n}</h1>
      <CompC n={props.n} />
    </div>
  );
}

class CompC extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    console.log('CompC', prevProps, prevState, this.props);
    console.log('CompC componentDidUpdate');
  }

  render() {
    var title = document.getElementById('title');
    if (title) {
      console.log(title.innerHTML);
    } else {
      console.log(title);
    }
    return (
      <div>
        <h1>{this.props.n}</h1>
      </div>
    );
  }
}

export default class App extends React.Component {
  render() {
    return (
      <div>
        <CompA />
      </div>
    );
  }
}
复制代码

首次渲染(新节点渲染)

  1. 通过参数的值创建节点
  2. 根据不同的节点,做不同的事情
  • 文本节点:通过 document.createTextNode 创建真实的文本节点
  • 空节点:什么都不做
  • 数组节点:遍历数组,将数组每一项递归创建节点(回到第 1 步进行反复操作,直到遍历结束)
  • DOM 节点:通过 document.createElement 创建真实的 DOM 对象,然后立即设置该真实 DOM 元素的各种属性,然后遍历对应 React 元素的 children 属性,递归操作(回到第 1 步进行反复操作,直到遍历结束)
  • 组件节点
    a. 函数组件:调用函数(该函数必须返回一个可以生成节点的内容),将该函数的返回结果递归生成节点(回到第 1 步进行反复操作,直到遍历结束)
    b. 类组件:

    • 创建该类的实例
    • 立即调用对象的生命周期方法:static getDerivedStateFromProps
    • 运行该对象的 render 方法,拿到节点对象(将该节点递归操作,回到第 1 步进行反复操作)
    • 将该组件的 componentDidMount 加入到执行队列(先进先出,先进先执行),当整个虚拟 DOM 树全部构建完毕,并且将真实的 DOM 对象加入到容器中后,执行该队列
  1. 生成出虚拟 DOM 树之后,将该树保存起来,以便后续使用
  2. 将之前生成的真实的 DOM 对象,加入到容器中。

更新节点

更新的场景:

重新调用 ReactDOM.render,触发根节点更新
在类组件的实例对象中调用 setState,会导致该实例所在的节点更新
节点的更新

如果调用的是 ReactDOM.render,进入根节点的对比(diff)更新
如果调用的是 setState
运行生命周期函数,static getDerivedStateFromProps
运行 shouldComponentUpdate,如果该函数返回 false,终止当前流程
运行 render,得到一个新的节点,进入该新的节点的对比更新
将生命周期函数 getSnapshotBeforeUpdate 加入执行队列,以待将来执行
将生命周期函数 componentDidUpdate 加入执行队列,以待将来执行
后续步骤:

更新虚拟 DOM 树
完成真实的 DOM 更新
依次调用执行队列中的 componentDidMount
依次调用执行队列中的 getSnapshotBeforeUpdate
依次调用执行队列中的 componentDidUpdate

对比更新

将新产生的节点,对比之前虚拟 DOM 中的节点,发现差异,完成更新

问题:对比之前 DOM 树中哪个节点

React 为了提高对比效率,做出以下假设

  1. 假设节点不会出现层次的移动(对比时,直接找到旧树中对应位置的节点进行对比)

  2. 不同的节点类型会生成不同的结构
    相同的节点类型:节点本身类型相同,如果是由 React 元素生成,type 值还必须一致
    其他的,都属于不相同的节点类型

  3. 多个兄弟通过唯一标识(key)来确定对比的新节点

    key 值的作用:用于通过旧节点,寻找对应的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中的相同 key 值的节点,进行对比。

key 值应该在一个范围内唯一(兄弟节点中),并且应该保持稳定

找到了对比的目标

判断节点类型是否一致

一致
根据不同的节点类型,做不同的事情

空节点:不做任何事情

DOM 节点:

直接重用之前的真实 DOM 对象
将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)
遍历该新的 React 元素的子元素,递归对比更新

文本节点:

直接重用之前的真实 DOM 对象
将新的文本变化记录下来,将来统一完成更新

组件节点:

函数组件:重新调用函数,得到一个节点对象,进入递归对比更新

类组件:

重用之前的实例
调用生命周期方法 getDerivedStateFromProps
调用生命周期方法 shouldComponentUpdate,若该方法返回 false,终止
运行 render,得到新的节点对象,进入递归对比更新
将该对象的 getSnapshotBeforeUpdate 加入队列
将该对象的 componentDidUpdate 加入队列
数组节点:遍历数组进行递归对比更新

不一致
整体上,卸载旧的节点,全新创建新的节点

创建新节点

进入新节点的挂载流程

卸载旧节点

文本节点、DOM 节点、数组节点、空节点、函数组件节点:直接放弃该节点,如果节点有子节点,递归卸载节点
类组件节点:
直接放弃该节点
调用该节点的 componentWillUnMount 函数
递归卸载子节点

import React, { Component } from 'react';

class CompA extends React.Component {
  state = {
    a: 123,
    b: 'abc',
  };

  componentDidUpdate(prevProps, prevState) {
    console.log('CompA componentDidUpdate');
  }

  render() {
    return (
      <div>
        <h1>{this.state.a}</h1>
        <CompB n={this.state.b} />
        <button
          onClick={() => {
            this.setState({
              a: 321,
              b: 'cba',
            });
          }}
        >
          点击
        </button>
      </div>
    );
  }
}

function CompB(props) {
  return (
    <div>
      <h1 id="title">{props.n}</h1>
      <CompC n={props.n} />
    </div>
  );
}

class CompC extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    console.log('CompC componentDidUpdate');
  }

  render() {
    var title = document.getElementById('title');
    if (title) {
      console.log(title.innerHTML);
    } else {
      console.log(title);
    }
    return (
      <div>
        <h1>{this.props.n}</h1>
      </div>
    );
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <CompA />
      </div>
    );
  }
}
复制代码
没有找到对比的目标

新的 DOM 树中有节点被删除

新的 DOM 树中有节点添加

创建新加入的节点
卸载多余的旧节点

工具

严格模式

StrictMode(React.StrictMode),本质是一个组件,该组件不进行 UI 渲染(React.Fragment <> </>),它的作用是,在渲染内部组件时,发现不合适的代码。

识别不安全的生命周期
关于使用过时字符串 ref API 的警告
关于使用废弃的 findDOMNode 方法的警告
检测意外的副作用
React 要求,副作用代码仅出现在以下生命周期函数中
ComponentDidMount
ComponentDidUpdate
ComponentWillUnMount
副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:

异步处理
改变参数值
setState
本地存储
改变函数外部的变量
相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数

在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)

检测过时的 context API

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
复制代码

Profiler

性能分析工具

分析某一次或多次提交(更新),涉及到的组件的渲染时间

火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间

排序图:得到某一次提交,每个组件自身渲染时间的排序

组件图:某一个组件,在多次提交中,自身渲染花费的时间

HOOK 简介

HOOK 是 React16.8.0 之后出现

组件:无状态组件(函数组件)、类组件

类组件中的麻烦:

this 指向问题

繁琐的生命周期

其他问题

HOOK 专门用于增强函数组件的功能(HOOK 在类组件中是不能使用的),使之理论上可以成为类组件的替代品

官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件

HOOK(钩子)本质上是一个函数(命名上总是以 use 开头),该函数可以挂载任何功能

HOOK 种类:
useState
useEffect
其他…

State Hook

State Hook 是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态

useState

  1. 第 N 次调用 useState
  2. 检查该节点的状态数组是否存在下标 N
  3. 不存在
    使用默认值创建一个状态
    将该状态加入到状态数组中 下标为 N
  4. 存在
    忽略掉默认值
    直接得到状态值
import React, { useState } from 'react';

export default function App() {
  //uses 快捷键
  const [n, setN] = useState(0);
  return (
    <div>
      <button
        onClick={() => {
          setN(n - 1);
        }}
      >
        -
      </button>
      <span>{n}</span>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        +
      </button>
    </div>
  );
}
复制代码

函数有一个参数,这个参数的值表示状态的默认值
函数的返回值是一个数组,该数组一定包含两项
第一项:当前状态的值
第二项:改变状态的函数(调用这个函数会导致重新渲染)
一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。

注意的细节

  1. useState 最好写到函数的起始位置,便于阅读
  2. useState 严禁出现在代码块(判断、循环)中
  3. useState 返回的函数(数组的第二项),引用不变(节约内存空间)
import React, { useState } from 'react';

window.arr = [];

export default function App() {
  const [visible, setVisible] = useState(true);
  const [n, setN] = useState(0);
  window.arr.push(setN); //在浏览器上比较arr[0] === arr[1]结果是true
  return (
    <div>
      <p style={{ display: visible ? 'block' : 'none' }}>
        <button
          onClick={() => {
            setN(n - 1);
          }}
        >
          -
        </button>
        <span>{n}</span>
        <button
          onClick={() => {
            setN(n + 1);
          }}
        >
          +
        </button>
      </p>
      <button
        onClick={() => {
          setVisible(!visible);
        }}
      >
        显示/隐藏
      </button>
    </div>
  );
}
复制代码
  1. 使用函数改变数据,若数据和之前的数据完全相等(使用 Object.is 比较),不会导致重新渲染,以达到优化效率的目的。
import React, { useState } from 'react';

export default function App() {
  console.log('App Render');
  const [visible, setVisible] = useState(true);
  const [n, setN] = useState(0);
  return (
    <div>
      <p style={{ display: visible ? 'block' : 'none' }}>
        <button
          onClick={() => {
            setN(n - 1);
          }}
        >
          -
        </button>
        <span>{n}</span>
        <button
          onClick={() => {
            setN(n + 1);
          }}
        >
          +
        </button>
      </p>
      <button
        onClick={() => {
          // setVisible(!visible);  //这样是会重新渲染的
          setVisible(visible); //这里没有改变visible的值 就不会重新渲染
        }}
      >
        显示/隐藏
      </button>
    </div>
  );
}
复制代码
  1. 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
import React, { useState } from 'react';

export default function App() {
  console.log('App Render');
  const [data, setData] = useState({
    x: 1,
    y: 2,
  });
  return (
    <div>
      <p>
        x: {data.x},y:{data.y}
        <button
          onClick={() => {
            setData({
              ...data,
              x: data.x + 1,
            });
            //下面的这种写法会把{x: 1,y: 2}直接替换成{x: data.x + 1}
            // setData({
            //     x: data.x + 1
            // })
          }}
        >
          x+1
        </button>
      </p>
    </div>
  );
}
复制代码
  1. 如果要实现强制刷新组件
    类组件:使用 forceUpdate 函数
import React, { Component } from 'react';
export default class App extends Component {
  render() {
    return (
      <div>
        <button
          onClick={() => {
            //不会运行shouldComponentUpdate
            this.forceUpdate(); //强制重新渲染
          }}
        >
          强制刷新
        </button>
      </div>
    );
  }
}
复制代码

函数组件:使用一个空对象的 useState

import React, { useState } from 'react';
export default function App() {
  console.log('App Render');
  const [, forceUpdate] = useState({});
  return (
    <div>
      <p>
        <button
          onClick={() => {
            forceUpdate({});
          }}
        >
          强制刷新
        </button>
      </p>
    </div>
  );
}
复制代码
  1. 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象

  2. 和类组件的状态一样,函数组件中改变状态可能是异步的(在 DOM 事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。

import React, { useState } from 'react';
export default function App() {
  console.log('App render');
  const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
  return (
    <div>
      <button
        onClick={() => {
          // setN(n - 1);
          // setN(n - 1);
          setN((prevN) => prevN - 1); //传入的函数,在事件完成之后统一运行
          setN((prevN) => prevN - 1);
        }}
      >
        -
      </button>
      <span>{n}</span>
      <button
        onClick={() => {
          // setN(n + 1) //不会立即改变,事件运行完成之后一起改变
          // setN(n + 1) //此时,n的值仍然是0
          setN((prevN) => prevN + 1); //传入的函数,在事件完成之后统一运行
          setN((prevN) => prevN + 1); //使用函数会依次执行每个函数所以结果是2
        }}
      >
        +
      </button>
    </div>
  );
}
复制代码

Effect Hook

Effect Hook:用于在函数组件中处理副作用

副作用:

ajax 请求
计时器
其他异步操作
更改真实 DOM 对象
本地存储
其他会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数

下面例子表示 卸载该函数组件 useEffect 是不会执行的,但是清理函数会执行

import React, { useState, useEffect } from 'react';
const ref = React.createRef();
window.timer = null; //计时器ID

function stop() {
  clearInterval(window.timer); //清空之前的计时器
  window.timer = null;
}
/**
 * 一个可移动的块,该组件每次渲染完成后,始终从0,0坐标在10秒钟内,移动到目标点坐标
 * @param {*} props
 * props.left,要移动到的目标点横坐标
 * props.top,要移动到的目标点纵坐标
 */
function MovableBlock(props) {
  useEffect(() => {
    console.log('render');
    //渲染完成后
    const div = ref.current;
    let curTimes = 0; //当前移动的次数
    const disX = props.left / 1000; //横坐标上每次移动的距离
    const disY = props.top / 1000; //纵坐标上每次移动的距离
    window.timer = setInterval(() => {
      curTimes++; //移动次数+1
      const newLeft = curTimes * disX;
      const newTop = curTimes * disY;
      div.style.left = newLeft + 'px';
      div.style.top = newTop + 'px';
      if (curTimes === 1000) {
        stop();
      }
    }, 10);
    return stop; //直接使用stop作为清理函数
  });

  return (
    <div
      ref={ref}
      style={{
        width: 100,
        height: 100,
        left: 0,
        top: 0,
        position: 'fixed',
        background: '#f40',
      }}
    ></div>
  );
}

export default class App extends React.Component {
  state = { x: 0, y: 0, visible: true };
  render() {
    console.log('App render');
    return (
      <div
        style={{
          paddingTop: 200,
        }}
      >
        {this.state.visible && (
          <div>
            x:{' '}
            <input
              type="number"
              value={this.state.x}
              onChange={(e) => {
                this.setState({
                  x: parseInt(e.target.value),
                });
              }}
            />
            y: <input
              type="number"
              value={this.state.y}
              onChange={(e) => {
                this.setState({
                  y: parseInt(e.target.value),
                });
              }}
            />
            <MovableBlock left={this.state.x} top={this.state.y} />
          </div>
        )}

        <button
          onClick={() => {
            this.setState({
              visible: !this.state.visible,
            });
            console.log(window.timer);
          }}
        >
          显示/隐藏
        </button>
      </div>
    );
  }
}
复制代码

useEffect()的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副效应不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。

细节

  1. 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
    与类组件中 componentDidMount 和 componentDidUpdate 的区别
    componentDidMount 和 componentDidUpdate,更改了真实 DOM,但是用户还没有看到 UI 更新,同步的。
    useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的。
  2. 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中。
  3. useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
    该函数运行时间点,在每次运行副作用函数之前
    首次渲染组件不会运行
    组件被销毁时一定会运行
  4. useEffect 函数,可以传递第二个参数
    第二个参数是一个数组
    数组中记录该副作用的依赖数据
    当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
    所以,当传递了依赖数据之后,如果数据没有发生变化
    副作用函数仅在第一次渲染后运行
    清理函数仅在卸载组件后运行

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

import React, { useState, useEffect } from 'react';

function Test() {
  useEffect(() => {
    console.log('副作用函数,仅挂载时运行一次');
    return () => {
      console.log('清理函数,仅卸载时运行一次');
    };
  }, []); //使用空数组作为依赖项,则副作用函数仅在挂载的时候运行,如果不写第二个参数,每次渲染都会执行useEffect
  console.log('渲染组件');
  const [, forceUpdate] = useState({});

  return (
    <h1>
      Test组件{' '}
      <button
        onClick={() => {
          forceUpdate({});
        }}
      >
        刷新组件
      </button>
    </h1>
  );
}

export default function App() {
  const [visible, setVisible] = useState(true);
  return (
    <div>
      {visible && <Test />}
      <button
        onClick={() => {
          setVisible(!visible);
        }}
      >
        显示/隐藏
      </button>
    </div>
  );
}
复制代码
  1. 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
import React, { useState, useEffect } from 'react';

export default function App() {
  const [n, setN] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log(n); //n指向,当前App函数调用时的n
    }, 5000);
  });
  return (
    <div>
      <h1>{n}</h1>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        n+1
      </button>
    </div>
  );
}
复制代码

错误写法

import React, { useState, useEffect } from 'react';
//以下是错误的做法
export default function App() {
  const [n, setN] = useState(10);
  useEffect(() => {
    //仅挂载后运行
    const timer = setInterval(() => {
      const newN = n - 1;
      console.log(newN); //这里会一直打印9,说明这里的n并不是实时的n,每次拿到的n都是闭包10
      setN(newN);
      if (newN === 0) {
        clearInterval(timer);
      }
    }, 1000);
    return () => {
      //函数卸载时运行
      clearInterval(timer);
    };
  }, []); //没有依赖,说明只会执行一次
  return (
    <div>
      <h1>{n}</h1>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        n+1
      </button>
    </div>
  );
}
复制代码

正确写法

import React, { useState, useEffect } from 'react';
let timer;
export default function App() {
  const [n, setN] = useState(10);
  useEffect(() => {
    if (n === 0) {
      return;
    }
    //某一次渲染完成后,需要根据当前n的值,1秒后重新渲染
    timer = setTimeout(() => {
      setN(n - 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [n]);
  return (
    <div>
      <h1>{n}</h1>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        n+1
      </button>
    </div>
  );
}
复制代码
  1. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
import React, { useState, useEffect } from 'react';

let n = 1;

function func1() {
  console.log('odd 副作用函数');
  return () => {
    console.log('odd 清理函数');
  };
}

function func2() {
  console.log('even 副作用函数');
  return () => {
    console.log('even 清理函数');
  };
}

export default function App() {
  const [, forceUpdate] = useState({});
  useEffect(n % 2 === 0 ? func2 : func1);
  n++;
  return (
    <div>
      <button
        onClick={() => {
          forceUpdate({});
        }}
      >
        强制刷新
      </button>
    </div>
  );
}
复制代码

自定义 HOOK

State Hook: useState Effect Hook:useEffect

自定义 Hook:将一些常用的、跨越多个组件的 Hook 功能,抽离出去形成一个函数,该函数就是自定义 Hook,自定义 Hook,由于其内部需要使用 Hook 功能,所以它本身也需要按照 Hook 的规则实现:

  1. 函数名必须以 use 开头
  2. 调用自定义 Hook 函数时,应该放到顶层

例如:

  1. 很多组件都需要在第一次加载完成后,获取所有学生数据
// App.js
import React from 'react';
import useAllStudents from './components/common/CustomizeUse/useAllStudents';
function Test() {
  const stus = useAllStudents();
  const list = stus.map((it) => <li key={it.id}>{it.name}</li>);
  return <ul>{list}</ul>;
}
export default function App() {
  return (
    <div>
      <Test />
    </div>
  );
}
// useAllStudents.js
import { useEffect, useState } from 'react';
import { getAllStudents } from '../../service/index';
/**
 * 当组件首次加载完成后,获取所有学生数据
 */
export default function useAllStudents() {
  const [students, setStudents] = useState([]);
  useEffect(() => {
    (async () => {
      const stus = await getAllStudents();
      setStudents(stus);
    })();
  }, []);
  return students;
}
复制代码
  1. 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载
    使用 Hook 的时候,如果没有严格按照 Hook 的规则进行,eslint 的一个插件(eslint-plugin-react-hooks)会报出警告
    /* eslint "react-hooks/exhaustive-deps": "off" */
// App.js
import React, { useState } from 'react';
import useTimer from './components/common/CustomizeUse/useTimer';
function Test(props) {
  useTimer(() => {
    console.log('Test组件的一些副作用操作');
  }, 1000);
  return <h1>Test组件</h1>;
}

export default function App() {
  const [visible, setVisible] = useState(true);
  return (
    <div>
      {visible && <Test />}
      <button
        onClick={() => {
          setVisible(!visible);
        }}
      >
        隐藏/显示
      </button>
    </div>
  );
}

//useTimer.js
/* eslint "react-hooks/exhaustive-deps": "off" */
import { useEffect } from 'react';
/**
 * 组件首次渲染后,启动一个Interval计时器
 * 组件卸载后,清除该计时器
 */
export default (func, duration) => {
  useEffect(() => {
    const timer = setInterval(func, duration);
    return () => {
      clearInterval(timer);
    };
  }, []);
};
复制代码
  1. 利用高阶组件
import React from 'react';
import { getAllStudents } from './components/service/index';

function withAllStudents(Comp) {
  return class AllStudentsWrapper extends React.Component {
    state = {
      stus: [],
    };

    async componentDidMount() {
      const stus = await getAllStudents();
      this.setState({
        stus,
      });
    }

    render() {
      return <Comp {...this.props} stus={this.state.stus} />;
    }
  };
}

function Test(props) {
  const list = props.stus.map((it) => <li key={it.id}>{it.name}</li>);
  return <ul>{list}</ul>;
}

const TestStudents = withAllStudents(Test);

export default function App() {
  return (
    <div>
      <TestStudents />
    </div>
  );
}
复制代码
  1. 利用 prop render
import React from 'react';
import { getAllStudents } from './components/service/index';

class AllStudents extends React.Component {
  state = {
    stus: [],
  };

  async componentDidMount() {
    const stus = await getAllStudents();
    this.setState({
      stus,
    });
  }

  render() {
    if (typeof this.props.render === 'function') {
      return this.props.render(this.state.stus);
    }
    return null;
  }
}

function Test(props) {
  const list = props.stus.map((it) => <li key={it.id}>{it.name}</li>);
  return <ul>{list}</ul>;
}

export default function App() {
  return (
    <div>
      <AllStudents render={(stus) => <Test stus={stus} />} />
    </div>
  );
}
复制代码
  1. 练习 usePageStudent
//App.js
import React, { useState } from 'react';
import usePageStudents from './components/common/CustomizeUse/usePageStudents';

function Test() {
  const [page, setPage] = useState(1);
  const resp = usePageStudents(page, 10);
  if (resp) {
    const list = resp.findByPage.map((it) => <li key={it.id}>{it.name}</li>);
    return (
      <div>
        <h1>数据总数:{resp.cont}</h1>
        <ul>{list}</ul>
        <input
          type="number"
          value={page}
          onChange={(e) => {
            setPage(parseInt(e.target.value));
          }}
        />
      </div>
    );
  }
  return null;
}

export default function App() {
  return (
    <div>
      <Test />
    </div>
  );
}

// usePageStudent.js
import { useEffect, useState } from 'react';
import { getStudents } from '../../service/index';
/**
 * 根据页码和页容量获取学生数据,得到一个响应结果
 * 并且,当页码和页容量变化时,将重新获取学生数据
 */
export default function useAllStudents(page = 1, limit = 10) {
  const [resp, setResp] = useState();
  useEffect(() => {
    (async () => {
      const resp = await getStudents(page, limit);
      setResp(resp);
    })();
  }, [page, limit]);
  return resp;
}
复制代码

Reducer Hook

Reducer Hook
Flux:Facebook 出品的一个数据流框架

  1. 规定了数据是单向流动的
  2. 数据存储在数据仓库中(目前,可以认为 state 就是一个存储数据的仓库)
  3. action 是改变数据的唯一原因(本质上就是一个对象,action 有两个属性)
    type:字符串,动作的类型
    payload:任意类型,动作发生后的附加信息
    例如,如果是添加一个学生,action 可以描述为:
    { type:”addStudent”, payload: {学生对象的各种信息} }
    例如,如果要删除一个学生,action 可以描述为:
    { type:”deleteStudent”, payload: 学生 id }
  4. 具体改变数据的是一个函数,该函数叫做 reducer
    该函数接收两个参数
    state:表示当前数据仓库中的数据
    action:描述了如何去改变数据,以及改变数据的一些附加信息
    该函数必须有一个返回结果,用于表示数据仓库变化之后的数据
    Flux 要求,对象是不可变的,如果返回对象,必须创建新的对象
    reducer 必须是纯函数,不能有任何副作用
    如果要触发 reducer,不可以直接调用,而是应该调用一个辅助函数 dispatch
    该函数仅接收一个参数:action
    该函数会间接去调用 reducer,以达到改变数据的目的
window.arr = [];

import { useState } from 'react';
/**
 * 通用的useReducer函数
 * @param {function} reducer reducer函数,标准格式
 * @param {any} initialState 初始状态
 * @param {function} initFunc 用于计算初始值的函数
 */
export default function useReducer(reducer, initialState, initFunc) {
  const [state, setState] = useState(
    initFunc ? initFunc(initialState) : initialState
  );

  function dispatch(action) {
    const newState = reducer(state, action);
    console.log(`日志:n的值  ${state}->${newState}`);
    setState(newState);
  }

  return [state, dispatch];
}
复制代码
// import React, { useReducer } from 'react'
import React from 'react';
import useReducer from './components/other/useReducer';
/**
 * 该函数,根据当前的数据,已经action,生成一个新的数据
 * @param {*} state
 * @param {*} action
 */
function reducer(state, action) {
  switch (action.type) {
    case 'increase':
      return state + 1;
    case 'decrease':
      if (state === 0) {
        return 0;
      }
      return state - 1;
    default:
      return state;
  }
}

export default function App() {
  console.log('重新渲染'); //useReducer中每次 setState(newState);都会直接导致重新执行App函数重新渲染(也就是依赖State Hook的组件会重新渲染)
  const [n, dispatch] = useReducer(reducer, 10, (args) => {
    console.log(args); //第三个参数是函数,该函数将useReducer的第二个参数作为参数
    return 100;
  });
  //window.arr.push(dispatch); //arr[0]===arr[1] 结果是false,说明每次重新渲染后的dispatch不是同一个
  return (
    <div>
      <button
        onClick={() => {
          dispatch({ type: 'decrease' });
        }}
      >
        -
      </button>
      <span>{n}</span>
      <button
        onClick={() => {
          dispatch({ type: 'increase' });
        }}
      >
        +
      </button>
    </div>
  );
}
复制代码

Context Hook

用于获取上下文数据

import React, { useContext } from 'react';

const ctx = React.createContext();

// function Test() {
//     return <ctx.Consumer>
//         {value => <h1>Test,上下文的值:{value}</h1>}
//     </ctx.Consumer>
// }

function Test() {
  const value = useContext(ctx);
  return <h1>Test,上下文的值:{value}</h1>;
}

export default function App() {
  return (
    <div>
      <ctx.Provider value="abc">
        <Test />
      </ctx.Provider>
    </div>
  );
}
复制代码

Callback Hook

函数名:useCallback

用于得到一个固定引用值的函数,通常用它进行性能优化

useCallback:

该函数有两个参数:

函数,useCallback 会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
数组,记录依赖项
该函数返回:引用相对固定的函数地址

import React, { useState } from 'react';

class Test extends React.PureComponent {
  // 由于每次传过来的props.onClick都是一个新的函数,所以每次父组件刷新就会导致子组件重新渲染
  render() {
    console.log('Test Render');
    return (
      <div>
        <h1>{this.props.text}</h1>
        <button onClick={this.props.onClick}>改变文本</button>
      </div>
    );
  }
}

function Parent() {
  console.log('Parent Render');
  const [txt, setTxt] = useState(123);
  const [n, setN] = useState(0);
  return (
    <div>
      {/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
      <Test
        text={txt}
        onClick={() => {
          setTxt(Math.random());
        }}
      />
      <input
        type="number"
        value={n}
        onChange={(e) => {
          setN(parseInt(e.target.value));
        }}
      />
    </div>
  );
}

export default function App() {
  return (
    <div>
      <Parent />
    </div>
  );
}
复制代码

下面这种写法,直接也是返回一个新的函数,还是会导致重新渲染

import React, { useState } from 'react';

class Test extends React.PureComponent {
  // 由于每次传过来的props.onClick每次都是一个新的函数,所以每次父组件刷新就会导致子组件重新渲染
  render() {
    console.log('Test Render');
    return (
      <div>
        <h1>{this.props.text}</h1>
        <button onClick={this.props.onClick}>改变文本</button>
      </div>
    );
  }
}

function handleClick(setN) {
  //返回的是新的函数
  return function (e) {
    setN(parseInt(e.target.value));
  };
}

function Parent() {
  console.log('Parent Render');
  const [txt, setTxt] = useState(123);
  const [n, setN] = useState(0);
  return (
    <div>
      {/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
      <Test
        text={txt}
        onClick={() => {
          setTxt(Math.random());
        }}
      />
      <input type="number" value={n} onChange={handleClick(setN)} />
    </div>
  );
}

export default function App() {
  return (
    <div>
      <Parent />
    </div>
  );
}
复制代码

解决办法:使用 useCallback

import React, { useState, useCallback } from 'react';

class Test extends React.PureComponent {
  render() {
    console.log('Test Render');
    return (
      <div>
        <h1>{this.props.text}</h1>
        <button onClick={this.props.onClick}>改变文本</button>
      </div>
    );
  }
}

function Parent() {
  console.log('Parent Render');
  const [txt, setTxt] = useState(1);
  const [n, setN] = useState(0);
  const handleClick = useCallback(() => {
    setTxt(txt + 1);
  }, [txt]);

  return (
    <div>
      {/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
      <Test text={txt} onClick={handleClick} />
      <input
        type="number"
        value={n}
        onChange={(e) => {
          setN(parseInt(e.target.value));
        }}
      />
    </div>
  );
}

export default function App() {
  return (
    <div>
      <Parent />
    </div>
  );
}
复制代码

Memo Hook

用于保持一些比较稳定的数据,通常用于性能优化

如果 React 元素本身的引用没有发生变化,一定不会重新渲染

import React, { useState, useMemo } from 'react';
class Test extends React.PureComponent {
  render() {
    console.log('Test Render');
    return (
      <div>
        <h1>{this.props.text}</h1>
        <button onClick={this.props.onClick}>改变文本</button>
      </div>
    );
  }
}
function Parent() {
  console.log('Parent Render');
  const [txt, setTxt] = useState(1);
  const [n, setN] = useState(0);
  //useMemo与useCallback的区别是
  // useMemo是基于返回值的,只要是依赖的数据不变(这里是txt),它的返回值总是指向同一个引用,不会发生变化(也就是handleClick),返回值可以是任何类型
  // useCallback只能绑定的是一个函数,依赖的数据不变,函数引用就不变
  const handleClick = useMemo(() => {
    return () => {
      setTxt(txt + 1);
    };
  }, [txt]);

  return (
    <div>
      {/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
      <Test text={txt} onClick={handleClick} />
      <input
        type="number"
        value={n}
        onChange={(e) => {
          setN(parseInt(e.target.value));
        }}
      />
    </div>
  );
}
export default function App() {
  return (
    <div>
      <Parent />
    </div>
  );
}
复制代码

Ref Hook

useRef 函数:

一个参数:默认值
返回一个固定的对象,{current: 值}

使用 createRef 每次都会创建一个新的 ref

//arr[0] == arr[1]  //false
import React, { useState, useRef } from 'react';
window.arr = [];
export default function App() {
  const inpRef = React.createRef();
  window.arr.push(inpRef); //这里和之前ref是函数的场景一样的,旧的传null,新的传input组件
  //因此点击重新渲染三次后window.arr结果如下
  // 0: {current: null}
  // 1: {current: null}
  // 2: {current: input}
  const [n, setN] = useState(0);
  return (
    <div>
      <input ref={inpRef} type="text" />
      <button
        onClick={() => {
          console.log(inpRef.current.value);
        }}
      >
        得到input的值
      </button>
      <input
        type="number"
        value={n}
        onChange={(e) => {
          setN(e.target.value); //每次点击重新渲染
        }}
      ></input>
    </div>
  );
}
复制代码

使用 useRef 后每次都是同一个 ref

// arr[0] == arr[1] //true
import React, { useState, useRef } from 'react';
window.arr = [];
export default function App() {
  const inpRef = useRef();
  window.arr.push(inpRef);
  const [n, setN] = useState(0);
  return (
    <div>
      <input ref={inpRef} type="text" />
      <button
        onClick={() => {
          console.log(inpRef.current.value);
        }}
      >
        得到input的值
      </button>
      <input
        type="number"
        value={n}
        onChange={(e) => {
          setN(e.target.value);
        }}
      ></input>
    </div>
  );
}
复制代码

使用 setInterval 定时器(差不多就相当于 setTimeout,和直接写 setTimeout 没有区别)

import React, { useState, useEffect } from 'react';
let timer = null;
export default function App() {
  const [n, setN] = useState(10);
  useEffect(() => {
    //仅挂载后运行
    if (n === 0) {
      clearInterval(timer);
      return;
    }
    timer = setInterval(() => {
      //这里的setInterval可换成setTimeout
      console.log(n - 1);
      setN(n - 1);
    }, 1000);
    return () => {
      //函数卸载时运行
      clearInterval(timer);
    };
  }, [n]);
  return (
    <div>
      <h1>{n}</h1>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        n+1
      </button>
    </div>
  );
}
复制代码

使用 useRef 将 timer 放入每个 App 组件中(这样可以避免多个 App 组件的时候使用同一个 timer 的情况)

import React, { useState, useRef, useEffect } from 'react';
export default function App() {
  const [n, setN] = useState(10);
  const timerRef = useRef(); //这里如果你写let timer; 请看下一个js代码
  useEffect(() => {
    if (n === 0) {
      return;
    }
    timerRef.current = setTimeout(() => {
      console.log(n);
      setN(n - 1);
    }, 1000);
    return () => {
      clearTimeout(timerRef.current);
    };
  }, [n]);
  return (
    <div>
      <h1>{n}</h1>
    </div>
  );
}
复制代码
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
  const [n, setN] = useState(10);
  // const timerRef = useRef(); //这里如果你写let timer;好像也没有问题
  let timer;
  useEffect(() => {
    if (n === 0) {
      return;
    }
    timer = setTimeout(() => {
      console.log(n);
      setN(n - 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [n]);
  return (
    <div>
      <h1>{n}</h1>
    </div>
  );
}
复制代码

使用 useRef 中的属性(每个组件中的 useRef 是不同的)

import React, { useState, useRef, useEffect } from 'react';
export default function App() {
  const [n, setN] = useState(10);
  const nRef = useRef(n); // {current:10}
  //这里可以直接把nRef理解成一个属性 flag = n 然后再下面控制flag(请看下面代码)
  useEffect(() => {
    const timer = setInterval(() => {
      nRef.current--;
      setN(nRef.current);
      if (nRef.current === 0) {
        clearInterval(timer);
      }
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return (
    <div>
      <h1>{n}</h1>
    </div>
  );
}
复制代码
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
  const [n, setN] = useState(10);
  // const nRef = useRef(n); // {current:10}
  var flag = n;
  useEffect(() => {
    const timer = setInterval(() => {
      flag--;
      setN(flag);
      if (flag === 0) {
        clearInterval(timer);
      }
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return (
    <div>
      <h1>{n}</h1>
    </div>
  );
}
复制代码

ImperativeHandle Hook

函数:useImperativeHandleHook

import React, { useRef } from 'react';
class Test extends React.Component {
  method() {
    console.log('Test method called');
  }

  render() {
    return <h1>Test Component</h1>;
  }
}
export default function App() {
  const testRef = useRef();
  return (
    <div>
      <Test ref={testRef} />
      <button
        onClick={() => {
          testRef.current.method();
        }}
      >
        点击调用Test组件的method方法
      </button>
    </div>
  );
}
复制代码
import React, { useRef, useImperativeHandle } from 'react';

function Test(props, ref) {
  useImperativeHandle(
    ref,
    () => {
      //如果不给依赖项,则每次运行函数组件都会调用该方法
      //如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
      // return 1;
      // return 1;相当于给 ref.current = 1
      return {
        method() {
          console.log('Test Component Called');
        },
      };
    },
    []
  );
  return <h1>Test Component</h1>;
}

const TestWrapper = React.forwardRef(Test);

// class Test extends React.Component {

//     method() {
//         console.log("Test method called");
//     }

//     render() {
//         return <h1>Test Component</h1>
//     }
// }

export default function App() {
  // const [, forceUpdate] = useState({})
  const testRef = useRef();
  return (
    <div>
      <TestWrapper ref={testRef} />
      <button
        onClick={() => {
          testRef.current.method();
          // console.log(testRef)
          // forceUpdate({})
        }}
      >
        点击调用Test组件的method方法
      </button>
    </div>
  );
}
复制代码

官网的例子

function FancyInput(props, ref) {
  // 这里ref拿到的是父组件的testRef
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    // useImperativeHandle第二个参数函数的返回值的父组件的testRef.current
    focus: () => {
      inputRef.current.focus();
    },
  }));
  return <input ref={inputRef} />;
}

const TestWrapper = React.forwardRef(FancyInput);

export default function App() {
  // const [, forceUpdate] = useState({})
  const testRef = useRef();
  return (
    <div>
      <TestWrapper ref={testRef} />
      <button
        onClick={() => {
          // testRef.current.method();
          console.log(testRef); //current: {focus: ƒ}
          // forceUpdate({})
        }}
      >
        点击调用Test组件的method方法
      </button>
    </div>
  );
}
复制代码

LayoutEffect Hook

useEffect:浏览器渲染完成后,用户看到新的渲染结果之后
useLayoutEffectHook:完成了 DOM 改动,但还没有呈现给用户

应该尽量使用 useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用 useLayoutEffectHook
原因:如果在 useLayoutEffectHook 中调用了 5 秒钟的 dom 操作,就会导致浏览器的渲染延迟,用户会看不到效果,同样体验不好

  1. 对 DOM 进行了改动同步(下面的 setN 函数)
  2. componentDidMount&componentDidUpdate(类组件中的生命钩子函数) 这里说函数组件 HOOK 就是 useLayoutEffectHook
  3. 浏览器下一次渲染时间点达到 对比差异,进行渲染 用户看到新的效果
  4. 调用 useEffect(所以这里改动 dom 会有闪烁的效果(重新渲染的效果))
import React, { useState, useLayoutEffect, useRef } from 'react';
export default function App() {
  const [n, setN] = useState(0);
  const h1Ref = useRef();
  useLayoutEffect(() => {
    //无闪烁
    //调用时间相当于类组件的componentDidMount&componentDidUpdate
    h1Ref.current.innerText = Math.random().toFixed(2);
  });
  // useEffect(() => { //有闪烁
  //   h1Ref.current.innerText = Math.random().toFixed(2);
  // })
  return (
    <div>
      <h1 ref={h1Ref}>{n}</h1>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        +
      </button>
    </div>
  );
}
复制代码

DebugValue Hook

useDebugValue:用于将自定义 Hook 的关联数据显示到调试栏

如果创建的自定义 Hook 通用性比较高,可以选择使用 useDebugValue 方便调试

import React, { useState, useEffect, useDebugValue } from 'react';

function useTest() {
  const [students] = useState([]);
  useDebugValue('学生集合');
  return students;
}

export default function App() {
  useState(0);
  useState('abc');
  useEffect(() => {
    console.log('effect');
  }, []);
  useTest();
  return <div></div>;
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享