React

75 阅读33分钟

create-react-app

Create React App 是 React 团队推出的一个构建 React 单页面应用的脚手架工具。它本身集成了 Webpack,并配置了 loader 和默认的 npm 脚本。

安装

Windows:

npm install -g create-react-app

Mac:

sudo npm install -g create-react-app

检查安装情况:

create-react-app --version

构建单页面应用

create-react-app 项目名称

项目目录:

  my-react-app
    ├── node_modules
    ├── public
    │   ├── favicon.ico
    │   ├── index.html
    │   └── manifest.json
    ├── src
    │   ├── App.css
    │   ├── App.js
    │   ├── App.test.js
    │   ├── index.css
    │   ├── index.js
    │   ├── logo.svg
    │   └── serviceWorker.js
    ├── .gitignore
    ├── package.json
    ├── README.md
    └── package-lock.json

Package.json

"dependencies": {
  "@testing-library/jest-dom": "^5.17.0",
  "@testing-library/react": "^13.4.0",
  "@testing-library/user-event": "^13.5.0",
  // 核心应用
  "react": "^18.2.0", // 这是React库的核心包。它包含了构建用户界面所需的包括组件、hooks和其他功能。
  "react-dom": "^18.2.0", // 这个包是React的DOM渲染器,用于在浏览器中呈现React组件。
  "react-scripts": "5.0.1", // 这个包封装了Webpack配置,用于控制React应用的打包过程。它提供了开发服务器、构建、测试和部署等功能。
},
// 命令
"scripts": {
  "start": "react-scripts start", // 开发环境
  "build": "react-scripts build", // 生产环境
  "test": "react-scripts test", // 单元测试
  "eject": "react-scripts eject" // 暴露Webpack配置规则,这个操作是不可逆的,在项目根目录创建一个 config 文件夹
},

Webpack配置:

如果项目中需要配置 Webpack,可以通过 npm run eject 或者 yarn eject 将配置文件夹暴露出来。

运行 npm run eject 或者 yarn eject 后,项目根目录会新增两个文件夹分别是:configscripts

Css预编译器:

创建好的项目默认使用 Scss 预编译器,如果需要使用 Less 可以自己手动通过配置处理。

脚手架只下载了 sass-loader,需要手动下载 sass 才能编译 scss/sass 文件。

开发代理服务器配置:

src 文件夹下新建 setupProxy.js设置开发服务器代理。

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
    app.use(
        createProxyMiddleware('/api', {
            target: 'http://www.baidu.com',
            changeOrigin: true, 
            pathRewrite: { '^/api': '' }
        }),
    )
}

或者是在 Package.json 中通过 proxy,设置代理,不过这种方式只接受一个字符串的值。

{
    // 一级属性
    "proxy": "http://www.baodu.com"
}

React

MVC

MVC(Model-View-Controller) 是一种软件架构模式,用于组织和管理应用程序。它将应用程序分为三个主要部分:

  1. M (Model)
    • 负责数据以及状态管理。
  2. V (View)
    • 负责展示应用程序的用户界面。
  3. C (Controller)
    • 负责处理用户交互和应用程序的逻辑。

MVC 架构中,数据的更新是单向的,用户的交互事件需要通过 Controller 层更新到 Model 层,从而数据刷新后反映到 View 层。

虚拟DOM [Virtual DOM]

Virtual DOMReact 中的一个概念,用来描述一个虚拟的 DOM 树。它是一个对象,对象中的成员都是用来描述 DOM 的相关特征。

React 使用 Virtual DOM 的目的是为了提高性能。当组件的状态改变时,React 会使用 diff 算法对 新/旧Virtual DOM 进行差异对比,计算出差异部分,只对差异部分做更新。而不是将整个 DOM 树重新渲染。

通过 Virtual DOMReact 可以在内存中进行 DOM 操作,而不是直接操作真实的 DOM。这样可以减少浏览器的重绘和回流,提高页面渲染效率。使得开发更加简洁和高效,因为开发者只需要关注组件的状态和渲染逻辑,不需要手动操作DOM。

JSX

插值:

语法:{}

{} 插值只接受 Js表达式。变量、函数调用、算术运算、条件判断等等。

变量插值:

const msg = "react";

root.render(
  <div>{ msg }</div>
);

函数调用:

const getMsg = () => "react";

root.render(

  <div>{ getMsg() }</div>

);

条件判断:

不支持 Jsif else 的形式。

  • 三元表达式:
root.render(
  <div>{true ? "react" : ""}</div>
);

算术运算:

  • 逻辑运算符:
root.render(
  <div>{true ? "react" : ""}</div>
);
  • 算术运算符:
root.render(
  <div>{1 + 1}</div>
);

需要注意的是,插值语法中只能包含单个表达式

标签内容插值的表现:

  • Number/String:内容是什么渲染出来的就是什么。
  • Boolean/Null/Undefined/Symbol:渲染的内容是空。
  • Object:渲染会报错,不支持渲染普通对象(Virtual DOM除外)。
  • Array:会把数组里面的每一项元素分别拿出来渲染,并不是将数组元素变为字符串渲染。

标签属性插值:

标签的属性插值也是使用 {}

属性插值:

root.render(
  <input type="text" disabled={ true }/>
);

style 插值:

  • 要求数据类型是一个对象,对象中的 key 就是Css属性,value 就是对应的Css属性的值。
root.render(
  <div style={ 
    {
      color: "red"
    }
  }>
    React
  </div>
);
  • 在遇到 kabab-case 的Css属性名时需要使用驼峰命名法中的小驼峰命名 camelCase
root.render(
  <div style={ 
    {
      fontSize: "20px", // font-size 采用 fontSize
      color: "red"
    }
  }>
    React
  </div>
);

class 插值:

  • 在 Jsx 中给标签定义 class 类名,需要采用 className。直接使用 class 会在控制台报错。
root.render(
  <div className="box">
    React
  </div>
);
  • 动态插入Class
const className = 'my-classname'; 
root.render(
  <div className={ `container ${className}` }>
    React
  </div>
);
  • 条件表达式动态添加Class
const isActive = true;
root.render(
  <div className={ isActive ? 'active' : 'inactive' }>
    React
  </div>
);
  • 用数组添加多个Class
const classNames = ['my-classname', 'another-classname']; 
root.render(
  <div className={ classNames.join(' ') }>
    React
  </div>
);
  • 使用对象根据条件动态添加Class
const className = { 'my-classname': true, 'another-classname': isActive, };
root.render(
  <div className={
      Object.keys(className).filter(key => className[key]).join(' ')
  }>
    React
  </div>
);

条件渲染:

两种控制元素是否显示的方案,一种是通过 display 控制,另一种是直接控制元素是否渲染。

const flag = true;
root.render(
  <>
    {/***
     * 控制是否渲染:
     *  + 只有 flag 为 true 时才会渲染 button 元素,如果为 false,那么就会渲染 null,也就是空。
     */}
    {flag ? <button>按钮1</button> : null}

    {/***
     * 控制是否显示:
     *  + 根据 flag 的值动态通过 display 属性控制元素是否显示,这个方案下的元素是必然会有 DOM 结构存在。
     */}  
    <button style={
      {
        display: flag ? 'block' : 'none'
      }
    }>按钮2</button>
  </>
);

遍历渲染:

const list = ["张三", "李四", "王五"];
root.render(
  <>
    <ul>
      {
        list.map((item, index) => (
          <li>{item}</li>
        ))
      }
    </ul>
  </>
);

{} 插值语法在渲染的是一个数组时,会将数组中的每一项元素拿出来单独渲染。如:

const list_1 = [<li>张三</li>, <li>李四</li>, <li>王五</li>]
root.render(
  <>
    <ul>
      {list_1}
    </ul>
  </>
);

遍历渲染的元素,需要加上 key,确保元素的唯一性,不加 key 控制台会报错。

const list = ["张三", "李四", "王五"];
root.render(
  <>
    <ul>
      {
        list.map((item, index) => (
          <li key={index}>{item}</li>
        ))
      }
    </ul>
  </>
);

不渲染数据,只是想渲染指定数量的DOM元素:

root.render(
  <>
    {
      new Array(5).fill(null).map((_, index) => {
        return <button key={ index }>按钮{ index }</button>
      })
    }
  </>
);

利用 new Array(5) 创建长度为 5 的稀疏数组稀疏数据中的元素都是 empty 在循环遍历的时候会被跳过。所以需要通过 fill 对数组进行填充,让其变为 密集数组,最后使用 map 方法创建5个 button

事件处理:

可以通过 onXxx 属性来绑定事件,其中 Xxx 是具体的事件名称,例如:onClickonChange等。将事件处理函数作为属性值传递给 onXxx 属性。

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  handleClick() {
    console.log("处理点击");
  }
  render() {
    return (
      <>
        <button onClick={ this.handleClick }>Click</button>
      </>
    );
  }
}

export default Demo;
事件函数this指向:

在上面的例子中 handleClick 事件函数的 this 指向是 undefined,而非组件实例。

正常原生Js事件触发 this 应该指向触发事件的元素,因为是用的 React合成事件,它内部处理了,所以这里的 this 指向 undefined

如果需要在 handleClick 中操作组件实例,那么就需要通过 this 绑定。

箭头函数:

  • 通过箭头函数的 this 绑定,实现 this 指向实例。

  • 需要注意的是,在类中通过 = 声明的属性都会挂载到实例上,而非原型。

import React from "react";

class Demo extends React.Component {
  handleClick = () => {
    console.log(this);
  }
  render() {
    return (
      <>
        <button onClick={ this.handleClick }>Click</button>
      </>
    );
  }
}

export default Demo;

普通函数:

  • 通过 bind 方法创建一个新的函数,并改变函数的 this 指向,从而实现 this 指向实例。
import React from "react";

class Demo extends React.Component {
  handleClick() {
    console.log(this);
  }
  render() {
    return (
      <>
        <button onClick={ this.handleClick.bind(this) }>Click</button>
      </>
    );
  }
}

export default Demo;

普通函数+箭头函数:

import React from "react";

class Demo extends React.Component {
  handleClick() {
    console.log(this);
  }
  render() {
    return (
      <>
        <button onClick={ () => this.handleClick() }>Click</button>
      </>
    );
  }
}

export default Demo;
  • 就是绑定一个箭头函数,当箭头函数执行时调用 handleClick

  • () => 才是 onClick 事件处理函数,而 handleClick 是事件处理函数里面执行的内容。

事件函数传参:

方式1:

  • 通过 bind 绑定 this 并传参,例如:this.handleClick.bind(this, "Hello")
import React from "react";

class Demo extends React.Component {
  handleClick(msg) {
    console.log(this);
    console.log(msg);
  }
  render() {
    return (
      <>
        <button onClick={ this.handleClick.bind(this, "Hello") }>Click</button>
      </>
    );
  }
}

export default Demo;
  • 如果需要使用到 事件对象,在最后面定义参数接收,事件对象会作为最后一个参数传入。

方式2:

  • 使用箭头函数的方式。
import React from "react";

class Demo extends React.Component {
  handleClick(msg) {
    console.log(this);
    console.log(msg);
  }
  render() {
    return (
      <>
        <button onClick={ () => this.handleClick("Hello") }>Click</button>
      </>
    );
  }
}

export default Demo;
  • 如果需使用 事件对象 可以这样:
<button onClick={ (event) => this.handleClick("Hello", event) }>Click</button>

方式3:

  • 通过闭包实现。
import React from "react";

class Demo extends React.Component {
  handleClick(msg) {
    return (event) => {
      console.log(this);
      console.log(msg);
      console.log(event);
    }
  }
  render() {
    return (
      <>
        <button onClick={ this.handleClick("Hello") }>Click</button>
      </>
    );
  }
}

export default Demo;

例子中 handleClick 返回了一个函数,这个函数才是事件处理函数,return 的函数是箭头函数,目的是为了绑定 this 指向组件实例。

合成事件:

React 中,合成事件是对浏览器原生事件进行封装和优化出来的一套事件机制。合成事件提供一致的跨浏览器事件处理方式,你可以理解为 React 帮我们对不同浏览器的事件做了兼容,并且统一出一套属于 React 的事件处理机制。

onXxxonXxxCapture 就是 React 提供的事件处理方式。例如:onClickonClickCapture。这两个都是 React 提供的点击事件。

带有 Capture 结尾的是捕获事件。默认事件传播机制是冒泡。

合成事件对象:

React合成事件对象(SyntheticBaseEvent )是一个纯 Js对象,代替了原生的浏览器事件对象(PointerEvent)

React 也对合成事件对象做了跨浏览器兼容,使得开发者不需要处理兼容问题,只需要使用统一的方法来处理事件。

合成事件对象提供部分了与原生事件相同的属性和方法,如 typetarget 等,其中 nativeEvent 就是原生的事件对象。

import React from "react";

class Demo extends React.Component {
  handleClick(e) {
    console.log(e); // 合成事件对象
    console.log(e.nativeEvent); // 原生事件对象
  }
  render() {
    return (
      <button onClick={ this.handleClick }>Click</button>
    );
  }
}

export default Demo;

事件对象中的stopPropagation:

理解 React 的事件委托后再看这节。

  • stopPropagation 用于阻止事件传播。

  • 合成事件对象(SyntheticBaseEvent) 中的 stopPropagation 方法能够阻止合成事件以及原生事件的事件传播。

  • 浏览器事件对象(PointerEvent) 中的 stopPropagation 方法只能阻止原生事件的事件传播。

  • 例子:

import React from "react";

class Demo extends React.Component {

  render() {
    return (
      <div
        className="inner"
        onClick={() => { console.log("inner-冒泡 【React合成事件】"); }}
        onClickCapture={() => { console.log("inner-捕获 【React合成事件】"); }}
        style={
          {
            backgroundColor: "tomato",
            width: 100,
            height: 100,
          }
        }
      >
        <div className="box"
          onClick={(e) => { 
            console.log("box-冒泡 【React合成事件】");
            // 合成事件对象
            e.stopPropagation();
            // 原生事件对象
            // e.nativeEvent.stopPropagation();
          }}
          onClickCapture={() => { 
            console.log("box-捕获 【React合成事件】");
          }}
          style={
            {
              backgroundColor: "skyblue",
              width: 50,
              height: 50,
            }
          }
        >
        </div>
      </div>
    );
  }
  componentDidMount() {
    /**
     * 事件传播机制:
     *    捕获:
     *      - window -> document -> html -> body -> root -> inner
     *    冒泡:
     *      - inner -> root -> body -> html -> document -> window
     * */

    // window
    window.addEventListener("click", () => {
      console.log("window-冒泡");
    }, false);
    window.addEventListener("click", () => {
      console.log("window-捕获");
    }, true);

    // document
    document.addEventListener("click", () => {
      console.log("document-冒泡");
    }, false);
    document.addEventListener("click", () => {
      console.log("document-捕获");
    }, true);

    // html
    document.querySelector("html").addEventListener("click", () => {
      console.log("html-冒泡");
    }, false);
    document.querySelector("html").addEventListener("click", () => {
      console.log("html-捕获");
    }, true);

    // body
    document.querySelector("body").addEventListener("click", () => {
      console.log("body-冒泡");
    }, false);
    document.querySelector("body").addEventListener("click", () => {
      console.log("body-捕获");
    }, true);

    // #root
    document.querySelector("#root").addEventListener("click", () => {
      console.log("#root-冒泡");
    }, false);
    document.querySelector("#root").addEventListener("click", () => {
      console.log("#root-捕获");
    }, true);

    // .inner
    document.querySelector(".inner").addEventListener("click", () => {
      console.log(".inner-冒泡");
    }, false);
    document.querySelector(".inner").addEventListener("click", () => {
      console.log(".inner-捕获");
    }, true);

    // .box
    document.querySelector(".box").addEventListener("click", () => {
      console.log(".box-冒泡");
    }, false);
    document.querySelector(".box").addEventListener("click", () => {
      console.log(".box-捕获");
    }, true);
  }
}

export default Demo;
  • 可以注释掉 e.stopPropagation 或者 e.nativeEvent.stopPropagation 看下打印结果。

  • 需要记住所有合成事件的处理都是在 #root 事件中进行,所以不管调用 原生 (stopPropagation) 或是 合成 (stopPropagation),上面例子的 冒泡阶段 打印结果都会在 #root-冒泡 后中断。不同的是合成事件调用后的打印。

合成事件池

  • React 16 中,合成事件池是一种优化机制,用于重用合成事件对象,目的是为了减少事件对象创建和销毁的开销。

  • 当一个事件触发时, React 会从事件池中获取一个合成事件对象并传递给事件处理函数。当事件函数处理完成后,合成事件对象会被重置并返回事件池中。(可以调用事件对象中的 persist() 避免事件对象重置)

  • React 17 及以后版本不再使用合成事件池的概念。

事件委托:

React 中,事件处理函数并不是直接绑在JSX元素上的,而是绑定在根节点上,通过事件传播机制,如:冒泡捕获 的方式拿到对应触发事件的子元素,并执行它绑定的合成事件属性。

目的是为了减少事件处理函数的调用次数,提高性能,并且可以在动态元素上正确的处理事件。

Js实现React事件委托原理:

  • 整个应用的视图部分。
<style>
    #root {
        width: 100px;
        height: 100px;
        background-color: tomato;
        margin: auto;
        overflow: hidden;
    }
    .inner {
        width: 50px;
        height: 50px;
        background-color: skyblue;
        margin: auto;
        margin-top: 25px;
    }
</style>
<!-- #root 为 React 中整个应用的根节点 -->
<div id="root">
    <!-- 这个就是我们开发者定义的JSX元素 -->
    <div class="inner"></div>
</div>
  • 实现事件委托Js部分:
// React 17及以上版本是对 #root 元素做了事件委托。
const rootDOM = document.querySelector("#root");

// 冒泡
rootDOM.addEventListener("click", (e) => {
    /**
     *  e.composedPath():
     *      - 该方法返回一个数组,表示事件触发时从最内层元素到最外层元素的路径。
     *      - e.path 和这方法的作用一样,不支持 path 属性的浏览器中可以使用 composedPath。
     * */
    [...e.composedPath()].reverse().forEach(ele => {
        const handle = ele.onClick;
        if (handle) handle();
    });
}, false);

// 捕获
rootDOM.addEventListener("click", (e) => {
    [...e.composedPath()].forEach(ele => {
        const handle = ele.onClickCapture;
        if (handle) handle();
    });
}, false);

// 模拟:对JSX元素绑定合成事件,如:onClick、onClickCapture
const innerDOM = document.querySelector(".inner");

// 冒泡
innerDOM.onClick = () => {
    console.log("inner-冒泡");
};

// 捕获
innerDOM.onClickCapture = () => {
    console.log("inner-捕获");
};

/*
    上面的代码等价于:
        render() {
            return <div onClick={ () => {...} } onClickCapture={ () => {...} }>inner</div>
        }
    原理:
    React 并没有真正对 JSX 元素做了事件绑定,而是给元素加了一个合成事件属性,如:onClick、onClickCapture。
        在点击元素时会被事件委托的 #root 获取到,并执行对应的绑定了合成事件属性值(也就是事件处理函数)。
*/
  • 例子中是实现了 React 17 及以上版本的事件委托概念。

  • 而在 React 17 以下的是对 document 做的事件委托,并且只实现 冒泡 方式。

分析 React 中的事件委托:

  • v17 版本的例子:
import React from "react";

class Demo extends React.Component {

  render() {
    return (
      <div 
        className="inner"
        onClick={() => { console.log("inner-冒泡 【React合成事件】"); }} 
        onClickCapture={() => { console.log("inner-捕获 【React合成事件】"); }}
      >Click</div>
    );
  }
  componentDidMount() {
    /**
     * 事件传播机制:
     *    捕获:
     *      - window -> document -> html -> body -> root -> inner
     *    冒泡:
     *      - inner -> root -> body -> html -> document -> window
     * */ 
    // window
    window.addEventListener("click", () => {
      console.log("window-冒泡");
    }, false);
    window.addEventListener("click", () => {
      console.log("window-捕获");
    }, true);

    // document
    document.addEventListener("click", () => {
      console.log("document-冒泡");
    }, false);
    document.addEventListener("click", () => {
      console.log("document-捕获");
    }, true);

    // html
    document.querySelector("html").addEventListener("click", () => {
      console.log("html-冒泡");
    }, false);
    document.querySelector("html").addEventListener("click", () => {
      console.log("html-捕获");
    }, true);

    // body
    document.querySelector("body").addEventListener("click", () => {
      console.log("body-冒泡");
    }, false);
    document.querySelector("body").addEventListener("click", () => {
      console.log("body-捕获");
    }, true);

    // #root
    document.querySelector("#root").addEventListener("click", () => {
      console.log("#root-冒泡");
    }, false);
    document.querySelector("#root").addEventListener("click", () => {
      console.log("#root-捕获");
    }, true);

    // .inner
    document.querySelector(".inner").addEventListener("click", () => {
      console.log(".inner-冒泡");
    }, false);
    document.querySelector(".inner").addEventListener("click", () => {
      console.log(".inner-捕获");
    }, true);
  }
}

export default Demo;
  • 上面例子中点击 Click 按钮后打印出来的结果:
window-捕获
document-捕获
html-捕获
body-捕获
.inner-捕获 【React合成事件】
#root-捕获
.inner-捕获
.inner-冒泡
.inner-冒泡 【React合成事件】
#root-冒泡
body-冒泡
html-冒泡
document-冒泡
window-冒泡
  • 从结果上能看出 捕获阶段 的顺序有问题 inner-捕获 【React合成事件】 输出在 #root-捕获 之前。可以看出,它并没有按照我们理解的事件传播机制来。

  • 前面也说了 Reactv17 版本后对整个应用的 根节点(#root)事件委托(冒泡、捕获,都做了)。实际上就是在点击 Click 执行的顺序是这样的:

window-捕获
document-捕获
html-捕获
body-捕获
#root-捕获  <- 【这一个是 React 内部注册的,但是我们看不到,这里只是补全了让打印信息变得清晰】 
.inner-捕获 【React合成事件】
...  <- 其他的合成事件,会在这个阶段全部执行,按照Js捕获事件传播的顺序来。
#root-捕获 <- 这个是我们例子中自己为 #root 注册的事件。
.inner-捕获
.inner-冒泡
#root-捕获  <- 【这一个是 React 内部注册的,但是我们看不到,这里只是补全了让打印信息变得清晰】
.inner-冒泡 【React合成事件】
...   <-  其他的合成事件,会在这个阶段全部执行,按照Js冒泡事件传播的顺序来。
#root-冒泡 <- 这个是我们例子中自己为 #root 注册的事件。
body-冒泡
html-冒泡
document-冒泡
window-冒泡
  • 例子中我们补全了 React 内部注册的一个事件,在这个内部注册的事件里 React 就会拿到所有的合成事件执行,按照事件传播机制的顺序执行。

  • 可以给 inner 元素再加一层结构求证在 #root 内置的事件处理中是否拿到了所有的合成事件执行,例如:.box


class Demo extends React.Component {
  render() {
    return (
      <div
        className="inner"
        onClick={() => { console.log("inner-冒泡 【React合成事件】"); }}
        onClickCapture={() => { console.log("inner-捕获 【React合成事件】"); }}
        style={
          {
            backgroundColor: "tomato",
            width: 100,
            height: 100,
          }
        }
      >
        <div className="box"
          onClick={() => { console.log("box-冒泡 【React合成事件】"); }}
          onClickCapture={() => { console.log("box-捕获 【React合成事件】"); }}
          style={
            {
              backgroundColor: "skyblue",
              width: 50,
              height: 50,
            }
          }
        >
        </div>
      </div>
    );
  }
  componentDidMount() {
    //省略已经写过的内容... 
   
    // .box
    document.querySelector(".box").addEventListener("click", () => {
      console.log(".box-冒泡");
    }, false);
    document.querySelector(".box").addEventListener("click", () => {
      console.log(".box-捕获");
    }, true);
  }
}

export default Demo;
  • 再次点击 Click 按钮,打印出来的信息会是:
#root-捕获 <- 【这一个是 React 内部注册的,但是我们看不到,这里只是补全了让打印信息变得清晰】
inner-捕获 【React合成事件】
box-捕获 【React合成事件】
#root-捕获 <- 这个是我们例子中自己为 #root 注册的事件。
.inner-捕获
.box-捕获
.box-冒泡
.inner-冒泡
#root-捕获 <- 【这一个是 React 内部注册的,但是我们看不到,这里只是补全了让打印信息变得清晰】
box-冒泡 【React合成事件】
inner-冒泡 【React合成事件】
#root-冒泡 <- 这个是我们例子中自己为 #root 注册的事件。
  • 打印信息我只截取了 #root 部分的,其余的用不上。可以看出在 React 内置为 #root 注册的事件阶段,拿到了所有的合成事件如:.inner、.box 执行了。

  • 由此可以看出 React 并没有为 .inner、.box 这些元素真正注册事件,而是通过 事件委托 的方式对合成事件触发。

  • React 16 中同理,是对 document 做了事件委托,但是只做了 冒泡阶段。如果 JSX元素 注册了 捕获 事件,其实也是在 document冒泡阶段 时执行。这个想要求证可以将上面的例子改改,并且改下 React 的版本。 (React 16 中合成事件中的冒泡事件、捕获事件都是统一在 document 的冒泡阶段处理)

React不同版本中事件委托的区别:

  • v17 版本后事件委托的目标是 #root 根元素,即整个应用的根节点,对于 冒泡捕获 都做了处理 。

  • 而在 v17 之前的版本中是对 document 做事件委托,并且只处理了 冒泡 方式的事件传播。

组件化开发

React 中,组件没有明确全局和局部的概念,可以理解为都是局部组件。不过可以将组件注册在 React 上,这样也能实现组件全局使用。

React 组件分为:函数组件类组件Hooks组件(在函数组件中使用 React Hooks 函数)

函数组件:

定义函数组件:

// Demo.jsx
export default () => {
  return (
    <div>我是 Demo 组件</div>
  )
}

调用:

// 导入函数组件
import Demo from "@/views/Demo";

root.render(
  <>
    {/* 调用函数组件 */}
    <Demo />
  </>
);

调用组件的方式可以是:

  • <Demo /> (单闭合)
  • <Demo></Demo> (双闭合)

组件的命名需要采用 PascalCase 命名法,首字母大写,否则React会在控制台抛出错误。

函数组件接收组件参数:
  • 可以通过定义标签属性来给组件传递参数。例如:<Demo title="我是 Demo 组件"/>,其中 title="我是 Demo 组件" 就是传递给 Demo 组件的参数。
  • 函数组件可以通过接收 props 参数来获取传递的参数。函数组件接收的第一参数就是 props
// Demo组件
function Demo(props) {
  return (
    <>
      <div>{props.title}</div>  
      <div>x: {props.x}</div>
      <div>y: {props.y}</div>
    </>
  )
}
export default Demo;

// 调用 Demo 组件,并且传递参数。
import Demo from "@/views/Demo";
root.render(
  <>
    {/* 给函数组件传递参数 */}
    <Demo title="我是 Demo 组件" x={ 10 } y={ 20 }/>
  </>
);
  • 传递的数据如果是非 String 类型,可以使用 {} 插值。
  • props 是个只读对象,不允许修改,直接修改控制台会抛出错误。
// Demo组件
function Demo(props) {
  props.title = "修改 Props 的属性"; // 控制台会报错
  return (
    <>
      <div>{props.title}</div>  
      <div>x: {props.x}</div>
      <div>y: {props.y}</div>
    </>
  )
}
export default Demo;
  • 因为 props 对象被冻结了,可以调用 Object.isFrozen(props) 求证,结果为 true 则表示该对象被冻结了。
  • 冻结的对象不可以 新增、删除、修改,以及使用 Object.defineProperty 劫持属性
对象的操作规则:
    - 冻结:
        冻结对象:Object.freeze(obj)
        检测是否被冻结:Object.isFrozen(obj): true/falsetrue:冻结,false:非冻结)
        被冻结的对象:
            不能修改、新增、删除、以及属性劫持(Object.defineProperty)。
    - 密封:
        密封对象:Object.seal(obj)
        检测是否被密封:Object.isSealed(obj): true/falsetrue:密封,false:非密封)
        被密封的对象:
            不能新增、删除、数据劫持。可以修改
    - 不可扩展
        设置为不可扩展:Object.preventExtensions(obj)
        检测是否可扩展:Object.isExtensible(obj): true/falsetrue:可扩展,false:不可扩展)
        设置不可扩展后:
            可以删除、修改、劫持。不可以新增。
        
使用注意:
    这些方法都是浅层操作,既只影响对象本身的属性,而不能影响深层嵌套的引用类型的属性。
    如果对象的属性值是一个引用类型,那么这个引用类型的可变性不受这些方法的影响。
  • props 对象中的属性如果存在引用类型,那么这个引用类型中的属性是可以变的。如:
// Demo组件
function Demo(props) {
  props.info.name = "李四"; // 不报错,因为操作的不是 Props 自身的属性。
  props.info = { name: "李四", gender: "男" }; // 报错,操作的是 Props 自身的属性。
  return (
    <>
      <div>{props.title}</div>  
    </>
  )
}
export default Demo;
// 调用组件
root.render(
  <>
    <Demo title="我是 Demo 组件" info={ { name: "张三", gender: "" } }></Demo>
  </>
);
Props默认值:

通过 defaultProps 设置默认值,在调用组件时没有传递参数,就会拿到这个默认值。

// Demo组件
function Demo(props) {
  return (
    <>
      <div>{props.title}</div>  
    </>
  )
}
// 设置默认值
Demo.defaultProps = {
  title: "默认的 title 属性"
};
export default Demo;
Props规则校验:

设置 props 参数类型、是否必传、等等规则。

参数校验需要依赖到 React 官方的一个插件 prop-types,这个插件在脚手架拉取项目时不会内置,需要手动下载。

npm install prop-types

基础数据类型:

import PropTypes from "prop-types";

// Child组件
function Child(props) {
  console.log(props, "props");
  return (
    <div>Child组件</div>
  )
}
// 规定参数类型
Child.propTypes = {
  title: PropTypes.string, // 字符串
  count: PropTypes.number, // 数值
  info: PropTypes.object, // 对象
  list: PropTypes.array, // 数组
  callback: PropTypes.func, // 函数
}

// 父组件
function Parent() {
  return (
    <Child 
      title="我是title" 
      count={ 10 } 
      info={ { name: "张三" } } 
      list={ [10, 20] } 
      callback={ () => { } } 
    />
  )
}

export default Parent;

设置必传属性:

  • isRequired 设置属性为必传,例如:PropTypes.string.isRequired,就表示数据类型是字符串且必传。
Child.propTypes = {
  // 必传
  title: PropTypes.string.isRequired,
}

多个数据类型:

  • 数据类型需要符合数组中的一项。
Child.propTypes = {
   count: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}

校验属性值:

  • 校验属性的值必须符合 ["10", 10] 中的一项。
Child.propTypes = {
   count: PropTypes.oneOf(["10", 10]),
}

校验数组类型:

Child.propTypes = {
   list: PropTypes.arrayOf(PropTypes.number),
}

校验对象的类型:

Child.propTypes = {
   info: PropTypes.objectOf(PropTypes.string),
}

校验对象的形状:

Child.propTypes = {
   info: PropTypes.shape(
    {
      name: PropTypes.string.isRequired
    }
  ),
}
  • 属性可以多传,但是一定要包含 name

校验节点信息:

  • 通过 PropTypes.node 校验节点信息,数据类型是可以任意类型,包括基本数据类型、对象、数组、React 元素(JSX)Fragment

  • 一般使用场景在 prop.children

import PropTypes from "prop-types";

// Child组件
function Child(props) {
  console.log(props, "props");
  return (
    <>
      <div>Child组件</div>
      { props.children }
    </>
  )
}
// 规定参数类型
Child.propTypes = {
  children: PropTypes.node.isRequired
}

// 父组件
function Parent() {
  return (
    <Child>
      <span>Hi</span>
    </Child>
  )
}

校验React元素信息:

  • 只接收单个 React 元素(JSX),校验传递的参数是不是一个有效的 React 元素(JSX)
import PropTypes from "prop-types";

// Child组件
function Child(props) {
  console.log(props, "props");
  return (
    <>
      <div>Child组件</div>
          { props.element }
    </>
  )
}
// 规定参数类型
Child.propTypes = {
  element: PropTypes.element.isRequired,
}

// 父组件
function Parent() {
  return (
    <Child element={ <span>Hi</span> }></Child>
  )
}

export default Parent;
函数组件的特点:

简洁:相对于类组件来说,代码更简洁,易于编写和理解。只需要定义一个函数,函数内部返回一个 JSX 元素即可。

无状态:函数组件是无状态的 ,没有内部状态(state)。如果需要在函数组件中使用,可以使用 Hooks 函数。

无生命周期:函数组件无生命周期,也可以使用 Hooks 提供的生命周期。

类组件:

类组件相比于函数组件,它更具有丰富的功能以及生命周期。

类组件的特点:

有状态:类组件可以有内部状态,状态发生更新会重新触发组建的 render 函数。

生命周期:具有一系列的生命周期方法,可以在组件不同阶段执行特定的操作。

类组件的定义:

定义类并继承 React.Component 类。

import React  from "react";

class Demo extends React.Component {
  render() {
    return (
      <div>Class组件</div>
    )
  }
}

export default Demo;

类内部需要定义一个 render 函数返回一个 JSX 元素。

类组件使用 Props:

除了在 constructor 外,其他位置可以直接通过 this.props 进行访问。

import React from "react";

class Child extends React.Component {
  render() {
    return (
      <>
        <div>Child组件</div>
        {/* 可以访问到 this.props */}
        <div>{ this.props.title }</div>
      </>
    )
  }
}

class Parent extends React.Component {
  render() {
    return (
      <Child title="父组件传递的Props内容" />
    )
  }
}

export default Parent;

constructor 中访问 props,在 constructor 会接收 props 作为第一参数,如果需要是使用 props 可以定义参数接收。

import React from "react";

class Child extends React.Component {
  constructor(props) {
    super(); // Class的使用方式,必须执行super关键字,它表示父类。
    console.log(props); // { title: "父组件传递的Props内容" }
    console.log(this.props); // undefined 
  }
  render() {
    return (
      <div>Child组件</div>
    )
  }
}

class Parent extends React.Component {
  render() {
    return (
      <Child title="父组件传递的Props内容" />
    )
  }
}

export default Parent;

但是在 constructor 无法访问实例上的 propsthis.props,因为此时 props 还未初始化完成,如果直接访问拿到的会是 undefined

可以通过 super() 关键字,将接收到的 props 参数传递给父类(既 React.Component)。进行初始化后就能访问实例的 props

import React from "react";

class Child extends React.Component {
  constructor(props) {  
    super(props); // 让父类既 React.Component 初始化 Props。
    console.log(props); // { title: "父组件传递的Props内容" }
    console.log(this.props); // { title: "父组件传递的Props内容" }
  } 
  render() {
    return (
      <div>Child组件</div>
    )
  }
}

class Parent extends React.Component {
  render() {
    return (
      <Child title="父组件传递的Props内容" />
    )
  }
}

export default Parent;
类组件对 Props 校验/默认值:

设置默认值:

import React from "react";

class Child extends React.Component {
  // 设置默认值
  static defaultProps = {
    title: "我是默认内容"
  }
  render() {
    return (
      <div>{ this.props.title }</div>
    )
  }
}

export default Child;

校验props:

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

class Child extends React.Component {
  // 设置校验规则
  static propTypes  = {
    title: PropTypes.string
  }
  render() {
    return (
      <div>{ this.props.title }</div>
    )
  }
}

export default Child;

需要通过 static 关键字,将 defaultPropspropTypes 设置为私有属性。

static 定义的属性会被挂载到类上,只有通过类可以访问。

状态state

初始化状态:

  • 初始化状态可以在 constructor 中通过 this.state 定义初始化状态。
import React from "react";

class Demo extends React.Component {
  constructor() {
    super();
    // 初始化状态
    this.state = {
      title: "我是State定义的状态"
    };
  }
  render() {
    // 使用状态
    let { title } = this.state;
    return (
      <div>{ title }</div>
    )
  }
}

export default Demo;
  • 或者在类中通过 state 定义。
import React from "react";

class Demo extends React.Component {
  state = {
    title: "我是State定义的状态"
  }
  render() {
    // 使用状态
    let { title } = this.state;
    return (
      <div>{ title }</div>
    )
  }
}

export default Demo;

如果在类中定义了 state 同时在 constructor 中又定义了 this.state

那么 this.state 定义的状态会覆盖掉类中定义的 state

import React from "react";

class Demo extends React.Component {
  state = {
    title: "我是State定义的状态"
  }
  constructor() {
    super();
    this.state = {
      msg: "msg"
    };
  }

  render() {
    console.log(this.state); // { msg: "msg" }
    return <div></div>
  }
}

export default Demo;

状态更新:

需要通过 React.Component 原型提供的 setState 方法对 state(状态) 进行更新。使用这个方法更新的目的是为了刷新视图,既重新执行 render 函数。

如果你直接 this.state.属性名 = xxx 的方式更新状态是可以的,但是视图并未刷新。所以需要依靠 setState 对状态更新。

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  }
  render() {
    let { count } = this.state;
    return (
      <>
        <div>{ count }</div>
        <button 
          onClick={ 
            () => {
              this.setState({
                count: ++count
              });
            }
          }
        >点击</button>
      </>
    )
  }
}

export default Demo;

需要注意的是使用 setState 更新状态时,React 会将传入的对象与当前的 state 进行浅合并

浅合并Object.assign 进行合并。

浅合并这意味着 state 中的某个属性如果是引用类型的数据,并且你更新了这个属性中的某个子属性,那么 React 会将整个属性替换成一个新的对象。如:

import React from "react";

class Demo extends React.Component {
  state = {
    userInfo: {
      name: "张三",
      age: 18
    }
  }
  render() {
    console.log(this.state.userInfo) // 点击修改后打印为:{ name: "李四" }
    return (
      <>
        <button 
          onClick={ 
            () => {
              this.setState({
                userInfo: {
                  name: "李四"
                }
              });
            }
          }
        >修改</button>
      </>
    )
  }
}

export default Demo;

例子中的 state.userInfo 采用的不是合并,而是替换。导致 age 属性丢失。

正确的做法是需要将整个 userInfo 对象的数据重新写入更新。

使用 forceUpdate 强制刷新:

  • 前面说过使用 this.state.属性名 = xxx 的方式可以更新状态,但是 render 并未重新执行。

  • 所以可以利用 forceUpdate 强制组件重新渲染。forceUpdate 也是 React.Component 原型提供的方法。

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  }
  render() {
    let { count } = this.state;
    return (
      <>
        <div>{ count }</div>
        <button 
          onClick={ 
            () => {
              this.state.count++;
              this.forceUpdate();
            }
          }
        >点击</button>
      </>
    )
  }
}

export default Demo;
  • 需要注意的是使用 forceUpdate 强制刷新不会执行 shouldComponentUpdate 生命周期。
refs:

ref 的作用是用于获取组件实例或者DOM元素的引用。

字符串方式:

React 16.3 之前的版本,可以通过字符串方式来创建 refReact 16.3 后这种方式已经被废弃,不推荐使用。

class Demo extends React.Component {
  render() {
    return (
      <div ref="myRef">Demo组件</div>
    )
  }
  componentDidMount() {
    console.log(this.refs.myRef);
  }
}

export default Demo;
函数方式:
class Demo extends React.Component {
  render() {
    return (
      <div ref={ x => x = this.myRef = x }>Demo组件</div>
    )
  }
  componentDidMount() {9
    console.log(this.myRef);
  }
}

export default Demo;

函数会接收到 React元素 的引用作为参数,将它赋值到一个新的变量中,最后通过 this.变量名 进行访问。

createRef方式(对象方式):

使用 React.createRef() 方法来创建 ref 对象,并将其赋值给一个实例属性。

对相应的 React元素 绑定 ref 对象,最后在视图挂载完毕后通过 this.属性名.current 进行访问。

class Demo extends React.Component {
  // 创建ref对象
  myRef = React.createRef()
  render() {
    return (
      // 绑定ref对象
      <div ref={ this.myRef }>Demo组件</div>
    )
  }
  componentDidMount() {
    // 访问ref对象
    console.log(this.myRef.current);
  }
}

export default Demo;
注意:

React 中,函数组件没有实例的概念,因此无法直接使用 refref 只能在 Calss组件 中使用。

如果你需要在函数组件中使用 ref,可以使用 React.forwardRef 来创建一个包装组件。

这个包装组件的第一参数是 props,而第二参数接收到的就是传递过来的 ref

在包装组件中拿到这个 ref参数 后绑定到需要的 DOM元素 或者 组件实例 上,在父组件中就能够被访问到了。

import React from "react";

const Child = React.forwardRef((props, ref) => {
  return (
    <>
      {/* 将接受过来的ref对象进行绑定。 */}
      <div ref={ ref }>Hello World</div>
    </>
  );
});

class Parent extends React.Component {
  myRef = React.createRef();
  render() {
    // React.createRef对象方式
    return <Child ref={ this.myRef } />;
    // 字符串方式
    return <Child ref="myRef" />;
    // 函数方式
    return <Child ref={ x => this.myRef_1 = x } />;
  }
  componentDidMount() {
    // React.createRef对象方式
    console.log(this.myRef.current); // 拿到的就会是:<div>Hello World</div>
    // 字符串方式
    console.log(this.refs.myRef); // 拿到的就会是:<div>Hello World</div>
    // 函数方式
    console.log(this.myRef_1); // 拿到的就会是:<div>Hello World</div>
  }
}

export default Parent;

例子里用来三种 ref 的绑定方式,可以在 Parent 组件中的 render 注释对应的 JSX 返回就能看到打印结果。

生命周期

React 生命周期是抽象的概念,是描述组件从被渲染到销毁的整个过程。

React 组件提供了一些生命周期方法,可以让开发者在不同的时间段执行特定的操作。

生命周期方法也被称为 生命周期函数钩子函数

Class组件的生命周期函数:

Class组件的生命周期分为三个阶段:

  • 挂载阶段(Mounting)

  • 更新阶段(Updating)

  • 卸载阶段(Unmounting)

新/旧生命周期:

React 16.3 版本引入了新的生命周期。

旧的生命周期:

image.png

React 16.3 开始废弃了 componentWillMountcomponentWillReceivePropscomponentWillUpdate,三个生命周期方法。并为这几个方法提供了别名:

  • UNSAFE_componentWillMount

  • UNSAFE_componentWillReceiveProps

  • UNSAFE_componentWillUpdate

UNSAFE标记方法名前缀,表示它们可能会在未来的版本中被移除。

新的生命周期:

image.png

constructor:

第一个执行,只在组初始化的时候执行一次。

一般在构造函数中对 state 初始化或对自定义方法进行 this 绑定。

constructor 接收到 props 作为第一参数,此方法中 this.props 还未被初始化,如果需要使用 this.props 可以在调用 super 时传递 props,让父类对其进行初始化。

componentWillMount:

在组件即将被挂载到前调用。该钩子函数中一般是做一些初始化动作。

v16.3 后被移除,可以通过 UNSAFE_componentWillMount 定义。

getDerivedStateFromProps:

在组件初次渲染、props更新、setState()forceUpdate() 时都会调用。

React 生命周期中的一个静态方法,在该方法中的 this 指向 undefined

getDerivedStateFromProps 方法接收两个参数:getDerivedStateFromProps(props, state)

getDerivedStateFromProps 方法必须要有返回值,返回值的内容会影响 state,如果返回的是 null 这不会对 state 改变。

componentWillReceiveProps:

props 更新,既父组件更新时调用。

componentWillReceiveProps 方法接收一个参数 nextProps,表示新的 props

v16.3 后被移除,可以通过 UNSAFE_componentWillReceiveProps 定义。

shouldComponentUpdate:

props 更新或 setState() 调用,主要是控制组件是否更新,该方法必须返回一个布尔值。true 则表示更新,会进行走对应的更新操作,false 则不更新。

需要注意的是,forceUpdate() 不会触发该方法,forceUpdate() 是强制更新,会跳过该方法。

getSnapshotBeforeUpdate

在更新前调用,目的是为了获取组件更新前的信息,定义了 getSnapshotBeforeUpdate 就必须定义 componentDidUpdate,否则会报错。

定义了 getSnapshotBeforeUpdateReact 认为你有意在获取更新前的信息,所以需要定义 componentDidUpdate

getSnapshotBeforeUpdate 会拿到更新前的 propsstate。必须要 return 内容。

return 的内容会被 componentDidUpdate 的第三参数接收。

componentWillUpdate:

组件更新前调用,在 v16.3 后被移除,可以通过 UNSAFE_componentWillUpdate 定义。

componentDidUpdate:

组件更新完毕后调用,接收三个参数,props(更新前的props), state(更新前的state)snapshot(由getSnapshotBeforeUpdate方法返回的值)

render:

render 是组件的必须方法,用于描述组件的UI结构和内容。每个 React组件 都必须实现 render 方法。

render 方法返回一个或一组 React元素

render 方法是一个纯函数,它只负责UI的渲染。

componentDidMount:

视图挂载关闭后执行。

componentWillUnmount:

组件卸载前执行,一般是在这里处理清理副作用、定时器等操作。

挂载阶段(Mounting):

只说v16.3的执行顺序,v16.3以前看上面的图。

image.png

更新阶段(Updating):

image.png

卸载阶段(Unmounting):

卸载阶段就只会执行 componentWillUnmount

父组件嵌套子组件的生命周期执行顺序:
挂载阶段:

image.png

更新阶段:

image.png

插槽

React 中没有直接的内置插槽概念,但可以通过使用 propsprops.children(组件的子元素) 来实现类似的功能。

props.children实现插槽:

组件开闭合标签中插入的内容会被包含在组件的 props 中,可以在 props 对象中的 children 属性获取到。

// Child组件
function Child(props) {
  let { children } = props;
  return (
    <>
      { children }
    </>
  )
}

// Parent组件
function Parent() {
  return (
    <Child>
      <span>React</span>
    </Child>
  )
}

export default Parent;

内容如果是一个 Virtual DOM 可以被 { } 插值语法解析。以上的形式就实现了 “默认插槽” 的概念。

具名插槽:

import React from "react";

// Child组件
function Child(props) {
  let { children } = props;
  // 通过 React.Children.toArray 数组
  children = React.Children.toArray(children);
  let header = [],
      footer = [], 
      defaultSlot =[];
  // 校验 props 中的 slot 字段。
  children.forEach(child => {
    const { slot } = child.props;
    if (slot === "header") {
      header.push(child);
    } else if (slot === "footer") {
      footer.push(child);
    } else {
      defaultSlot.push(child);
    }
  });
  return (
    <>
      <div className="header">{ header }</div>
      <div className="default">{ defaultSlot }</div>
      <div className="footer">{ footer }</div>
    </>
  )
}

// Parent组件
function Parent() {
  return (
    <Child>
      <span slot="header">我是头部</span>
      <span slot="default">我是默认内容</span>
      <span slot="footer">我是底部</span>
    </Child>
  )
}

export default Parent;

props实现插槽:

// Child组件
function Child(props) {
  let { header, content, footer } = props;
  
  return (
    <>
      <div className="header">{ header }</div>
      <div className="content">{ content }</div>
      <div className="footer">{ footer }</div>
    </>
  )
}

// Parent组件
function Parent() {
  const header = <span>我是头部</span>
  const content = <span>我是内容</span>
  const footer = <span>我是底部</span> 
  return (
    <Child header={ header } content={ content } footer={ footer }></Child>
  )
}

export default Parent;

React API

React.Component/React.PureComponent [创建Class组件的基类,也称为父类]

React.Component:

React.Component 它是所有自定义组件的父类,当创建一个组件时,可以继承 React.Component ,并实现其中的 render 方法定义组件的UI。

React.Component 还提供了一系列的生命周期方法、状态更新机制。

React.PureComponent:

React.PureComponent 是另一个组件基类,它继承自 React.Component

React.Component 不同而是,React.PureComponent 实现了一个浅比较的 shouldComponentUpdate 方法。在组件更新时会进行浅比较,如果 propsstate 没有发生变化,则不会触发组件更新、重新渲染。这样可以提高性能,避免不必要的渲染。

浅比较,既只比较 propsstate 一层数据,如果属性中出现引用类型数据,则只有在引用地址发生改变时才会被视为改变、需要更新。

React.Fragment [占位标签]

React 提供的一个组件,用来包裹多个子元素,并且渲染时不会创建额外的 DOM 节点。

React.Fragment 有两种语法,可以是 <></> 或者 <React.Fragment></React.Fragment>

render() {
  return (
    <React.Fragment>
      <div>Element 1</div>
      <div>Element 2</div>
      <div>Element 3</div>
    </React.Fragment>
  );
}

React.createElement [创建Virtual DOM]

React.createElementReact 中用于创建 Virtual DOM 的方法。

它接收三个参数:

  • React.createElement(ele, props, ...children);
ele:
 - 组件类型,可以是一个字符串表示HTML标签名,也可以是一个React组件。
props:
 - 属性对象,包含了组件的属性和事件处理函数等信息。
children:
 - 子元素,可以是一个或者多个,也可以是文本节点。

使用 React.createElement 创建的虚拟DOM对象能够被{}作为标签内容渲染。

root.render(
  <>
    {/* 通过 React.createElement 创建的虚拟DOM,能作为标签内容被 {} 编译渲染 */}
    {
      React.createElement(
        // 组件类型
        "button",  
        // 属性对象
        { 
          className: "btn", 
          style: { color: "red", fontSize: "18px" } 
        }, 
        // 子元素
        "注册账号"
      )
    }
  </>
);

React.Children [处理props.children数据格式]

React.ChildrenReact元素 的工具类。它提供了一些方法来处理和遍历 React 元素的子元素。

React元素 既是 JSX,也可以理解为组件。而 React.Children 提供了一些方法来处理组件的子元素,既 props.children

React.Children.map(children, function [, thisArg]) :
    - 概述:
        子元素进行遍历,即便 props.children 不是一个数组也可以调用,方法内部会将其转化为一个数组。
        React.Children.map 调用后会返回一个新数组,回调函数需要写 return 否则返回的新数组没内容。
        如果 return 的值是空也不会出现在新数组中。
    - 语法:
        @1 children:
            + 既 props.children。
        @2 function+ 回调函数,接收两个参数:遍历项,索引值。
        @3 thisArg:
            + 指定回调函数中的 this 指向,如果不传则函数内部的 this 指向是 undefined。
            
React.Children.forEach(children, function [, thisArg]):
    - 概述:
        和 React.Children.map 的区别是 React.Children.forEach 没有返回值。其他的都是一致。
        
React.Children.count(children):
    - 概述:
        计算 children 的长度/数量。
        
React.Children.only(children):
    - 概述:
        确保只有一个 children 元素。
        
React.Children.toArray(children):
    - 概述:
        将 children 转化为数组。

React.createRef [创建ref对象]

React.createRefReact 提供的一个用于创建 ref对象 的方法。

通过调用 React.createRef() 创建一个实例,并赋予给一个实例属性。

constructor(props) {
  super(props);
  this.myRef = React.createRef();
}

JSX 中绑定 ref属性ref对象

render() {
  return <div ref={this.myRef}>Hello World</div>;
}

在视图渲染完毕后通过 this.属性名.current 访问组件实例DOM元素

componentDidMount() {
  console.log(this.myRef.current); // 访问引用的组件实例或DOM元素
}

React.Component.setState [状态更新]

setStateReact 组件中更新组件状态的方法,它是一个异步方法。

语法:setState([partialState], [callback])

setState两个参数都是可选的。

partialState:
    类型:
        - object | function
    Object:
        - 需要更新的数据对象,会和 state 浅合并。
    function:
        - 拿到上一次的 state 作为参数,需要return一个数据对象作为更新的内容,也会和 state 浅
合并。
        - 当前组件实例作为第二参数。
callback:
    类型:
        - function
    function:
        - 执行时机会在 componentDidUpdate 后,且不会被 shouldComponentUpdate 影响。
        - 也就是说这个回调函数必然会执行,一般会在这个函数中中一些更新后的操作。

对象方式:

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState({
      count: this.state.count + 1
    });
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;

传入 setState 的对象会和原有的 state 对象进行 浅合并,而非对象替换。

函数方式:

使用函数方式更新状态的主要目的是确保我们基于最新的状态进行计算,而不会受到异步批量处理的影响。通过 prevState 可以拿到上一次操作的状态值。

例子:

  • 例子输出的结果是:Render次数:1,count的值:20
import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState((prevState) => {
      return {
        count: prevState.count + 1
      }
    });
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;
  • 如果将例子中 setState 改成对象的方式,那么输出的结果是 Render次数:1,count的值:1

  • 因为 setState 更新是异步的,我们无法基于最新的值进行计算。

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    for (let i = 0; i < 20; i++) {
      this.setState({
        count: this.state.count + 1
      });
    }
  };
  render() {
    console.log("render");
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;
  • 而函数方式更新我们可以通过 prevState 拿到上一次操作的值,获取到最新的状态进行计算。

函数 return 的状态会和 state 浅合并。

更新后的回调函数:

import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState((state) => {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log("更新完成后的Callback");
    });
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;

callback 会在 componentDidUpdate 之后执行,并且不会被 shouldComponentUpdate 方法 return false; 打断。 也就是这个回调必然会执行。

异步更新:

setState 的更新是异步的,这样做的目的是为了减少更新的次数,避免重复渲染造成的性能消耗。

一次 setState 更新需要执行相应的生命周期、Virtual DOM创建DOM-DIFF等操作,如果 setState 的更新是同步的会造成性能消耗。

所以 React 创建了一个异步更新队列,将当前执行栈中调用了 setState 的更新全部合并到一起,只有等执行栈空闲时才会执行更新队列,对数据、视图重新渲染。

例子:

  • increase 函数中调用了多次 setState,但是实际上 redner 只执行了一次,这说明了多次更新操作被合并了。
import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState({
      count: 1
    });
    this.setState({
      count: 2
    });
    this.setState({
      count: 3 
    });
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;
  • increase 函数中调用了 setState 后,立刻获取最新的值,发现值并没有更新,这说明了 setState 是异步的。
import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState({
      count: 1
    });
    console.log("同步:", this.state.count); // 0
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;
  • 既然是异步的,那就可以开启一个异步任务,在异步任务执行的时候就能获取到最新的值了。
import React from "react";

class Demo extends React.Component {
  state = {
    count: 0
  };
  increase = () => {
    this.setState({
      count: 1
    });
    console.log("同步:", this.state.count); // 0
    setTimeout(() => {
      console.log("异步:",this.state.count); // 1
    });
  };
  render() {
    return (
      <>
        <p>{ this.state.count }</p>
        <button onClick={ this.increase }>增加</button>
      </>
    );
  }
}

export default Demo;
  • 也可以通过使用 flushSync,实现同步更新。

React18和React16异步更新的表现:

React18:

  • v18 中,不论在什么地方执行 setState,它都是异步更新的。

React16:

  • v16 中,setState 在异步操作中使用,它将变为同步更新。

React.forwardRef [ref转发]

函数组件没有实例,无法通过正常的 ref 属性绑定来获取实例,需要通过 React.forwardRefref 做转发给其子组件。

import React from "react";

const Child = React.forwardRef((props, ref) => {
  return (
    <>
      {/* 将接受过来的ref对象进行绑定。 */}
      <div ref={ ref }>Hello World</div>
    </>
  );
});

class Parent extends React.Component {
  myRef = React.createRef();
  render() {
    // React.createRef对象方式
    return <Child ref={ this.myRef } />;
    // 字符串方式
    return <Child ref="myRef" />;
    // 函数方式
    return <Child ref={ x => this.myRef_1 = x } />;
  }
  componentDidMount() {
    // React.createRef对象方式
    console.log(this.myRef.current); // 拿到的就会是:<div>Hello World</div>
    // 字符串方式
    console.log(this.refs.myRef); // 拿到的就会是:<div>Hello World</div>
    // 函数方式
    console.log(this.myRef_1); // 拿到的就会是:<div>Hello World</div>
  }
    }

export default Parent;

React DOM API

flushSync [同步更新]

flushSyncReact 提供的一个同步更新的方法。在 React 中,通常会使用异步更新机制批量处理更新操作,以提高性能。但有时候,我们需要立即执行某个更新操作。并希望在更新完成之前阻塞其他操作。这时可以使用 flushSync

当调用 flushSync 时,React 会立即执行所有待处理的更新操作,并阻塞其他后续操作,只有等更新完成后才会继续执行。

import React from "react";
import { flushSync } from "react-dom";

class Demo extends React.Component {
  state = {
    name: "张三",
    age: 18
  };
  handleChange = () => {
    this.setState({
      name: "李四"
    });
    console.log(this.state); // { name: "张三", age: 18 }
    // 当调用 flushSync 时,会执行立即所有待处理的更新操作,并阻塞后面的操作,只有等更新完成后才会继续执行。
    flushSync(() => { 
      this.setState({
        age: 20
      });
    });
    // 因为调用了 flushSync,只有当更新完毕后才会输出 log。
    console.log(this.state);  // { name: "李四", age: 20 }
  };
  render() {
    console.log("render");
    return (
      <>
        <p>
          <span>姓名:{ this.state.name }</span>
          <br/>
          <span>年龄:{ this.state.age }</span>
        </p>
        <button onClick={ this.handleChange }>Change</button>
      </>
    );
  }
}

export default Demo;

需要注意的是,当 flushSync 执行时,会立即执行所有待处理的更新操作。包括在 flushSync 前的更新操作,以及 flushSync 回调函数中的更新操作。