跳到主要内容

React 核心

认识 React​

定义和特点​

image-20230729144152143
  • React 是用于构建 Web 和原生交互界面的 JavaScript 库

  • React 由 Facebook 更新和维护,大量流行的其他框架借鉴了 React 的思想

  • React 的特点

    • 声明式编程

      image-20230729145809536
    • 组件化开发

      img
    • 跨平台编写

      image-20230729145840146

开发依赖​

  • 开发 React 依赖的三个库

    • react:包含 react 所必须的核心代码
    • react-dom:react 渲染在不同平台所需要的核心代码
    • @babel/preset-react:将 jsx 转译成 React 代码的工具
  • react-dom 针对 web 端 和 native 端所完成的事情不同

    • web 端:将 jsx 最终渲染成真实的 DOM,显示在浏览器中
    • native 端:将 jsx 最终渲染成原生的控件

JSX 语法​

认识 JSX​

  • JSX 是 一种 JavaScript 的语法扩展

    const element = <h1>Hello JSX!</h1>;
    • 在很多地方也称之为 JavaScript XML ,像是一段 XML 语法
    • 它用于描述 UI 界面,并且可以和 JavaScript 融合在一起使用
  • React 认为渲染逻辑本质上与其他 UI 逻辑存在内在耦合

    • UI 需要绑定事件
    • UI 中需要展示数据状态
    • 在某些状态发生改变时,又需要更新 UI
  • 渲染逻辑和 UI 逻辑密不可分

    • 因此 React 没有将标记分离到不同的文件中
    • 而是将它们组合到了一起 ,这个地方就是组件

JSX 使用规则​

只能返回一个根元素

  • 如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来

    const element = <div><br/><br/></div>;
  • 如果不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替(Fragment 的语法糖)

    const element = <><br/><br/></>;
    const element2 = <Fragment><br/><br/></Fragment>;
  • 编写的元素结构需要换行时,可以使用一个 () 将结构包括起来

    const element = (
    <Fragment>
    <h2>React</h2>
    <p>用于构建 Web 和原生交互界面的库</p>
    </Fragment>
    );
  • 为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹 ?

    • JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象
    • 不能在一个函数中返回多个对象,除非用一个数组把他们包装起来
    • 添加一个父标签进行包裹后,这些被包裹的子元素会作为父标签的 children,会转化为一个数组

标签必须闭合

  • JSX 要求标签必须正确闭合

    const element = <div>Hello JSX</div>;
  • 对于单标签元素,必须添加结束符 /

    const element = <br />;
  • 对于双标签元素,如果不传递 children,可以写成一个单标签元素

    const element = <div title="无子元素" />;

大多数属性使用驼峰命名法

  • JSX 最终会被转化为 JavaScript

    • JSX 中的属性也会变成 JavaScript 对象中的键值对
    • 但 JavaScript 对变量的命名有限制,例如不能使用关键字和保留字
    • 所以大多数属性需要采用小驼峰命名法
    const element = <div onClick={() => {}}>Hello JSX</div>;
  • 由于 class 是一个关键字,所以在 React 中需要用 className 来代替

    const element = <div className="info">Hello JSX</div>;
  • 由于历史原因,aria-* 和 data-* 属性是以带 - 符号的 HTML 格式书写的

JSX 使用语法​

  • 注释:{/*标签*/}

    const element = (
    <div className="app">
    {/*<span style={{marginLeft: '50px'}}>haha</span>*/}
    </div>
    );
  • 通过大括号 {} 可以插入 JavaScript 表达式,只能在以下两种场景中使用大括号

    • 用作 JSX 标签内的元素

      const element = <div>{ new Date().toString() }</div>;
    • 用作紧跟在 = 符号后的属性

      const className = 'app';
      const element = (
      <div className={className}>
      Hello JSX
      </div>
      );
  • JSX 嵌入变量作为子元素

    • 当变量是 Number/String/Array 类型时,会直接渲染出来(注意数字 0 也会被渲染)
    • 当变量是 null/undefined/Boolean 类型时,不会进行渲染
    • 对象类型 不能作为子元素
  • JSX 中的内联样式和对象,需要使用 “双大括号” {{}}

    • JSX 中的对象表示

      const element = <ul style={{ color: 'pink' }}>;
    • 内联 style 属性,需要使用驼峰命名法编写

      const element = <ul style={{ backgroundColor: 'black' }}>;

JSX 语法本质​

  • JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖

  • 该函数可以传递三个参数

    • type:当前 ReactElement 的类型

      • 如果是标签元素,那么就使用字符串表示
      • 如果是组件元素,那么就直接使用组件的名字
    • props:传递的 props,以对象的形式存储

    • children:存放在 标签/组件 中的子元素

  • JSX 和 createElement 的对比

    • 使用 JSX 编写

      const element = (
      <div className="div">
      <h1 style={{ color: 'red' }}>11</h1>
      <span id="span">22</span>
      </div>
      );
    • 使用 createElement 编写

      const element = React.createElement(
      "div",
      { className: "div" },
      React.createElement("h1", { style: { color: 'red' }}, "11"),
      React.createElement("span", { id: "span" }, "22")
      );

描述 UI​

条件渲染​

条件渲染:根据不同的情况显示不同的内容

  • 条件判断语句 if/else

    function Demo({ number }) {
    if (number % 2 === 0) {
    return <h1>偶数</h1>;
    } else {
    return <h1>奇数</h1>;
    }
    }
  • 三元运算符 ? :

    function Demo({ number }) {
    return (
    <h1>{ number % 2 === 0 ? '偶数' : '奇数' }</h1>
    );
    }
  • 与运算符 &&

    • 左侧(我们的条件)为 true 时,它则返回其右侧的值

      function Demo({ completed }) {
      return (
      <div className={ completed ? 'completed' : '' }>
      代办任务
      { completed && <span>已完成</span> }
      </div>
      );
      }
    • 切勿直接将数字放在 && 左侧,因为数字 0 也会被渲染,除非强制转换为布尔类型(!!)

      function Demo({ number }) {
      return <h1>{ !!number && 'Hello' }</h1>;
      }
  • 在一些情况下,不想有任何东西进行渲染,可以直接返回 null

列表渲染​

列表渲染:操作数组中的数据,从而将一个数据集渲染成多个相似的组件

  • 在 React 中,展示列表最多的方式就是使用数组的 map 高阶函数

    const names = ['Amy', 'Jack', 'Mike'];
    const element = (
    <ul>
    { names.map(name => <li key={name}>{name}</li>) }
    </ul>
    );
  • 可以使用数组的 filter 函数对列表项进行过滤

    const numbers = [1, 2, 3, 4];
    const oldNumber = (
    <ul>
    {
    numbers.filter(n => n % 2 !== 0).map(n => <li key={n}>{n}</li>)
    }
    </ul>
    );

指定列表项的 key

  • 列表渲染时,使用 key 保持列表项的顺序

  • key 的作用

    • 从众多的兄弟元素中唯一标识出某一项
    • 即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中追踪到它,以便进行复用
  • key 需要满足的条件

    • key 值不要求全局唯一,但在兄弟节点之间必须是唯一的
    • key 值应该是稳定不变的,即不要在渲染时动态地生成 key,比如 key={ Math.random() } 的错误方式
  • React 默认会将列表项的索引作为 key

    • 数组项的顺序在插入、删除或者重新排序等操作中,会产生一些问题
    • 最佳的设定方式:来自数据库的数据 或 本地产生唯一数据
  • 组件不会把 key 当作 props 的一部分

事件处理​

  • 为了响应界面的操作,需要对元素添加事件处理函数

  • 在 React 中添加事件处理

    • React 事件的命名采用小驼峰式(如 onClick)
    • 通过 {} 传入一个事件处理函数作为元素的 prop,这个函数会在事件发生时被自动执行
    • React 中的事件是合成事件,生成的事件对象为 SyntheticEvent 的实例
    const element = <div onClick={e => console.log(e)}>Hello React</div>;
  • 处理函数被执行时,默认会将事件对象作为参数传递过来

    const element = (
    <div onClick={() => console.log('div click')}>
    <span onClick={e => e.stopPropagation()}>
    阻止事件向上传播
    </span>
    </div>
    );

事件传播

  • 事件分三个阶段传播
    • 它向下传播,调用所有的 onClickCapture 处理函数(捕获阶段)
    • 它执行被点击元素的 onClick 处理函数(执行阶段)
    • 它向上传播,调用所有的 onClick 处理函数(冒泡阶段)
  • 在 React 中所有事件都会传播,除了 onScroll,它仅适用于附加到的 JSX 标签

组件化开发​

定义组件​

  • 组件是 React 的核心概念之一,它们是构建用户界面(UI)的基础
    • 根据组件的定义方式,可以分为函数组件和类组件
    • 组件必须要像一个纯函数那样进行编写
  • 不同于 Vue.js,使用组件不需要进行注册操作,引入后直接使用即可

类组件​

  • 组件的名称必须是大写字母开头
  • 类组件需要继承自 Component/PureComponent
  • 必须实现 render 函数,可以返回 React 元素、数组、Portals、字符串、数值、布尔值、null、undefined
import React from 'react';
import ReactDOM from 'react-dom/client';

class App extends React.Component {
render() {
return <h2>Hello React</h2>;
}
}

ReactDOM
.createRoot(document.getElementById('root'))
.render(<App />);

函数组件​

  • 组件的名称同样必须以大写字母开头
  • 函数式组件没有生命周期和自己的状态,需要借助 Hooks 来实现对应的功能
function App() {
return <h2>Hello React</h2>;
}

组件的状态 state​

定义和读取状态​

  • state 的作用

    • 保留 渲染之间的数据
    • 触发 React 使用新数据渲染组件(重新渲染)
  • state 是隔离且私有的,如果渲染同一个组件两次,每个副本都会有完全隔离的状态

  • 声明和使用状态:通过读取组件实例上的 state 对象

    class App extends React.Component {
    state = {
    name: 'Taylor',
    age: 42
    };

    render() {
    const { name, age } = this.state;
    return (
    <div>
    <h2>{ name }</h2>
    <h2>{ age }</h2>
    </div>
    );
    }
    }

更新状态​

  • React 不推荐直接修改 state,这可能会导致一系列的问题

  • 更新组件的状态,需要调用组件实例上的 setState 方法

    • 调用后,组件上的 render 函数会自动重新执行
    • 然后组件会重新渲染,UI 会使用最新的状态数据进行展示
    /**
    * 计数器
    */
    class App extends React.Component {
    state = {
    count: 0
    };

    increment = () => {
    const { count } = this.state;
    this.setState({ count: count + 1 });
    }

    render() {
    const { count } = this.state;
    return (
    <>
    <h2>{ count }</h2>
    <button onClick={this.increment}>+1</button>
    </>
    );
    }
    }
  • 想要更新一个对象类型的 state 时,不能直接修改原有的对象(数组也是如此)

    • 需要创建一个新对象
    • 或者生成一个原对象的拷贝副本
    class App extends React.Component {
    state = {
    info: { name: '', age: 10 }
    };

    changeInfo = () => {
    const { info } = this.state;
    // 需要创建一个新对象,或者拷贝一份原对象
    this.setState({ info: { ...info, name: 'React' } });
    }

    render() {
    const { name, age } = this.state.info;
    return (
    <>
    <h2>姓名:{ count }</h2>
    <h2>年龄:{ age }</h2>
    <button onClick={this.changeInfo}>+1</button>
    </>
    );
    }
    }

setState 解析​

扩展使用

  • setState 可以传入一个回调函数

    • React 在更新状态时会调用这个函数并将上一次的状态传递过来
    • 这个回调函数的返回值会作为需要更新的状态
    class App extends React.Component {
    handleIncreaseAge = () => {
    this.setState(prevState => ({
    age: prevState.age + 1
    }));
    }
    }
  • setState 的更新默认是异步的

    • 设计为异步,可以显著的提升性能(批处理)

    • 如果同步更新了 state ,但还没有执行 render 函数,会导致 state 和 props 不能保持同步

      class App extends React.Component {
      handleClick() {
      console.log(this.state.name); // "Taylor"
      this.setState({ name: 'Robin' });
      console.log(this.state.name); // 依然是 "Taylor"!
      }
      }
    • 如果需要获取到异步的结果

      • 可以传递第二个回调函数作为参数
      • React 将在提交更新后调用这个回调函数
      class App extends React.Component {
      handleClick() {
      console.log(this.state.name); // "Taylor"
      this.setState({
      name: 'Robin'
      }, () => {
      console.log(this.state.name); // 更新为 "Robin"
      });
      }
      }
  • 连续调用 setState 更新,进行批处理

    • React 会将多次更新合并成一个对象,然后进行更新
    • 在源码中,通过 do {} while(true) 循环进行遍历链表
    function handleUpdate() {
    // count 初始值为 0
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // 只执行一次更新,count 的值更新为 1
    }

异步的版本变化

  • 在 React 18 之前,在 setTimeout 和 原生 DOM 事件 中会做同步处理

    class App extends React.Component {
    handleClick() {
    setTimeout(() => {
    this.setState({ message: 'Hello' });
    console.log(this.state.message) // 'Hello'
    }, 0);
    }

    componentDidMount() {
    document.getElementById('btn').addEventListener('click', () => {
    this.setState({ message: 'Hello' });
    console.log(this.state.message) // 'Hello'
    });
    }
    }
  • 在 React 18 之后,所有的 setState 更新都变成异步处理(自动批处理)

    image-20230811234120763
    • 在极少数情况下,需要强制同步应用特定的 state 更新,此时可以将其包装在 flushSync 中

      ReactDOM.flushSync(() => {
      this.setState({ message: 'Hello' });
      });
      console.log(this.state.message) // 'Hello'

组件的参数 props​

认识 props​

  • React 组件使用 props 来互相通信

    • 每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它
    • 相较于 HTML 属性,可以通过 props 传递任何 JavaScript 值,包括对象、数组和函数,甚至是 React 元素
  • props 是只读的,禁止子组件通过引用直接修改父组件传递的数据

传递 props​

  • 要传递 props,需要将它们添加到 JSX,就像使用 HTML 属性一样

    const Img = () => <img alt="一张图片" />;
  • 使用 JSX 展开语法传递 props(避免重复的传递操作)

    const Img = (props) => <img {...props} />
  • 还可以只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去

    const Button = (props) => {
    const { kind, ...other } = props;
    const className = kind === "primary" ? "primary-button" : "secondary-button";
    return <button className={className} {...other} />;
    };
  • 当内容嵌套在 JSX 标签中时,组件可以在名为 children 的 prop 中接收到该内容

    const Button = ({ children }) => <button>{ children }</button>;
    const Demo = () => <Button>按钮</Button>
  • 在传递 props 时,可以直接将 React 元素传递给子组件,子组件可以直接进行渲染(实现类似插槽的功能)

    const Demo = ({ leftSlot, rightSlot }) => {
    <div>
    { leftSlot }
    使用 props 实现插槽
    { rightSlot }
    </div>
    };

    const Warpper = () => {
    return (
    <Demo
    leftSlot={<button>按钮</button>}
    rightSlot={<span>文本</span>}
    />
    );
    };

读取 props​

  • 对于类组件,可以从 this.props 中获取到父组件传递的数据

    class Header extends React.Component {
    render() {
    const { title, fontSize } = this.props;
    return <h1 style={{ fontSize }}>{ title }</h1>;
    }
    }
  • 对于函数式组件,函数的第一个参数 就是接收到的 props 对象

    const Header = ({ title, fontSize }) => <h1 style={{ fontSize }}>{ title }</h1>;

组件之间通信​

父传子:props​

  • 父组件想要给子组件传递数据,直接使用 props 传递即可

    const Button = ({ title }) => <button>{ title }</button>;
    const Demo = () => <Button title="播放" />;
  • 在子组件中,可以对接收的 props 进行校验和指定默认值

    • 使用组件的 propTypes 属性进行校验

      import PropTypes from 'prop-types';

      const App = () => {};

      App.propTypes = {
      optionalArray: PropTypes.array.isRequired, // 数组类型且必传
      optionalBool: PropTypes.bool,
      optionalFunc: PropTypes.func
      }
    • 使用组件的 defaultProps 属性指定默认值

      const App = () => {};

      App.defaultProps = {
      dataSource: []
      };

子传父:回调函数​

子组件想要给父组件传递数据通过回调函数

  • 父组件传递一个回调函数给子组件
  • 在合适的时候,子组件执行这个函数,执行时可以携带一些数据传递给父组件
const App = ({ play }) => {
return (
<button onClick={() => play('播放')}>播放</button>
);
}

数据共享:Context​

Context 上下文的作用

  • 组件能够无需通过显式传递参数的方式将信息逐层传递
  • 在多个子组件中进行数据共享

创建 Context

  • 通过 React.createContext(defaultValue) 创建一个上下文对象

    • 该函数接收一个参数:默认值
    • 如果不传递默认值,则默认状态为 null
  • 该 context 对象本身不包含任何信息, 它只表示其他组件读取或提供的 那个 context

    • Context.Provider 可以为被它包裹的组件提供上下文的值
    • Context.Consumer 是一个很少会用到的备选方案,它用于读取上下文的值

提供上下文数据

  • 要想为后代子组件提供数据,需要使用 <Context.Provider> 组件

    • 可以使用该组件的 value 属性提供数据
    function App() {
    const [theme, setTheme] = useState('light');

    return (
    <ThemeContext.Provider value={theme}>
    <OtherComponent />
    </ThemeContext.Provider>
    );
    }
  • 如果 value 的值来自 state 和 props,当 value 的值发生变化时,后代组件会自动重新渲染

使用上下文数据

  • 使用 <Context.Consumer> 组件获取数据

    • 该组件的 children 为一个函数
    • 函数的第一个参数即为被共享的 value 值
    function Button() {
    return (
    <ThemeContext.Consumer>
    { theme => <button className={theme} /> }
    </ThemeContext.Consumer>
    );
    }
  • 对于类组件,可以使用静态属性 contextType

    • 使用该属性绑定上下文后,组件实例中 this.context 即为被共享的 value 值
    class Button extends React.Component {
    static contextType = ThemeContext;

    render() {
    const theme = this.context;
    const className = 'button-' + theme;
    return (
    <button className={className}>
    {this.props.children}
    </button>
    );
    }
    }
  • 对于函数式组件,直接使用 useContext Hooks 即可

    function Button() {
    const theme = useContext(ThemeContext);

    return <button className={theme} />;
    }

事件监听:事件总线​

  • 通过全局事件总线,在一个地方发出事件并携带参数,在其他任意地方监听这个事件并作出响应
  • 一些第三方库如 mitt 就可以实现类似的功能

组件的生命周期​

image-20230801230944626

  • 生命周期是一个 抽象的概念 ,在生命周期的整个过程,分成了很多个阶段

    • 挂载阶段:组件第一次在 DOM 树中被渲染的过程
    • 更新阶段:组件状态发生变化,重新渲染的过程
    • 卸载阶段:组件从 DOM 树中被移除的过程
  • 当组件到达某个生命周期时,会对组件内部实现的某些函数进行回调,这些函数就是生命周期函数

  • 需要注意的时,函数式组件没有上述的生命周期函数,可以借助 Hooks 进行模拟

  • 常见生命周期的用途

    • constructor:初始化组件状态和方法绑定
    • render:返回组成该组件内容的元素
    • componentDidMount:可以进行网络请求、获取 DOM、绑定事件监听等
    • componentWillUnmount:取消事件监听、定时器等

React 内置组件​

Fragment​

  • 作用:允许在不添加额外节点的情况下将子元素组合,可以使用语法糖 <>...</> 代替

    const element = (
    <>
    <h2>Hello React</h2>
    <p>用于构建 Web 和原生交互界面的 JavaScript 库</p>
    </>
    )
  • 该组件只接收一个唯一的 prop —— key

StrictMode​

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

    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
    • 它为其后代元素触发额外的检查和警告
    • 严格模式检查仅在开发模式下运行,不会影响生产构建
    import React, { StrictMode } from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';

    ReactDOM
    .createRoot(document.getElementById('root'))
    .render((
    <StrictMode>
    <App />
    </StrictMode>
    ));
  • 严格模式检查的内容

    • 识别不安全的生命周期
    • 过时的 API
    • 检查意外的副作用 —— 后代组件会被渲染两次

Suspense​

  • 作用:允许显示一个退路方案(fallback)直到它的子组件完成加载,参数:

    • children:实际的 UI 渲染内容
    • fallback:一个在实际的 UI 未渲染完成时代替其渲染的备用 UI
    <Suspense fallback={<Loading />}>
    <SomeComponent />
    </Suspense>
  • 该组件常常用于包裹使用了 懒加载 lazy 的组件

    • 使用了懒加载的组件,在打包时会被独立分包,加快首屏渲染速度
    const SomeComponent = React.lazy('./someComponent');

    function App() {
    return (
    <React.Suspense fallback={<Loading />}>
    <SomeComponent />
    </React.Suspense>
    );
    }

其他扩展​

(非) 受控组件​

受控组件

  • 在 HTML 中,表单元素通常自己维护 state,并根据用户输入进行更新

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

    • 将两者结合起来,使 React 的 state 成为“唯一数据源”
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作
    • 被 React 以这种方式控制取值的表单输入元素就叫做 “受控组件"(即表单的值由 React 状态进行控制)
  • 当表单元素绑定 React 状态后,变为受控组件

    • 此时表单元素不可以再次手动输入编辑
    • 通常需要监听受控元素的 onChange 事件来更新状态
    export default class Test extends React.Component {
    state = {
    input: ''
    }

    changeInput = (e) => {
    this.setState({ input: e.target.value });
    }

    render() {
    return <input value={this.state.input} onChange={this.changeInput} />
    }
    }
  • 如果绑定了状态,输入仍可编辑,可能是意外地绑定了 undefined/null

    const element = <input type="checkbox" checked={undefined} />;

非受控组件

  • 非受控组件:数据将交由 DOM 节点来处理,需要获取数据时,通过绑定 ref 来访问 DOM
  • 在非受控组件中通常使用 defaultValue 来设置默认值
export default class Test extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}

handleInputChange = () => {
console.log('value', this.inputRef.current.value);
}

render() {
return (
<input
ref={this.inputRef}
defaultValue="Hello React"
onChange={this.handleInputChange}
/>
);
}
}

记忆值:Ref​

  • 作用:希望组件“记住”某些信息,但又不想让这些信息触发新的渲染

获取原生 DOM

  • 在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 元素,需要给 DOM 绑定 ref 属性来获取

  • 对于类组件,绑定 DOM 元素主要有两种方式

    • 传入一个 Ref 对象

      • 该对象通过 React.createRef 创建出来
      • 通过该对象的 current 属性即可访问绑定的 DOM 元素
      class Demo extends React.Component {
      constructor(props) {
      super(props);
      this.inputRef = React.createRef();
      }

      componentDidMount() {
      // 输入框自动聚焦
      this.inputRef.current.focus();
      }

      render() {
      return <input type="text" ref={this.inputRef} />;
      }
      }
    • 传入一个回调函数

      • 该函数会在 DOM 被挂载时进行回调,这个回调函数会传入一个元素对象
      • 陷阱:如果 ref 回调函数是以内联函数的方式定义的(相当于每次更新都创建了一个新的函数对象),在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素
      • 解决办法:将该获取 ref 的函数挂载在组件实例上,不要每次都创建一个新的函数对象
      export default class Demo extends React.Component {
      componentDidMount() {
      // 输入框自动聚焦
      this.inputRef.focus();
      }

      getRef = (ref) => {
      this.inputRef = ref;
      }

      render() {
      return <input type="text" ref={this.getRef} />;
      }
      }
  • 对于函数式组件,如果要获取函数组件内部的 DOM,需要借助 React.forwardRef 高阶函数

    const Input = React.forwardRef((props, ref) => {
    return <input type="text" ref={ref} />;
    });

    export default class Demo extends React.Component {
    componentDidMount() {
    // 输入框自动聚焦
    this.inputRef.focus();
    }

    getRef = (ref) => {
    this.inputRef = ref;
    }

    render() {
    return <Input ref={this.getRef} />;
    }
    }

获取组件实例

  • 对于类组件,直接给组件绑定 ref 属性,即可获取到组件实例

    class Counter extends React.Component {
    state = {
    count: 0
    }

    increment = () => {
    this.setState({ count: this.state.count + 1 });
    }

    render() {
    return <h2>{this.state.count}</h2>;
    }
    }

    export default class Demo extends React.Component {
    getRef = (ref) => {
    this.counterRef = ref;
    }

    add = () => {
    // 让子组件中的计数器 +1
    this.counterRef.increment();
    }

    render() {
    return (
    <>
    <Counter ref={this.getRef} />
    <button onClick={this.add}>+1</button>
    </>
    )
    }
    }
  • 对于函数式组件,不存在组件实例,需要借助 React.forwardRef 和 useImperativeHandle 结合

改变节点位置:Portals​

  • createPortal 允许将 JSX 作为 children 渲染至 DOM 的不同部分,参数:

    • children:需要渲染的 React 元素
    • domNode:目标 DOM 节点,会作为子元素转移到目标 DOM 节点的内部
    import { createPortal } from 'react-dom';

    export default function MyComponent() {
    return (
    <div style={{ border: '2px solid black' }}>
    <p>这个子节点被放置在父节点 div 中。</p>
    {createPortal(
    <p>这个子节点被放置在 document body 中。</p>,
    document.body
    )}
    </div>
    );
    }
  • 注意点:portal 中的事件传播遵循 React 树,而不是 DOM 树

    import { createPortal } from 'react-dom';

    export default function MyComponent() {
    useEffect(() => {
    const bodyClick = () => console.log('body 事件监听');

    document.body.addEventListener('click', bodyClick);

    return () => {
    document.body.removeEventListener('click', bodyClick);
    }
    }, []);

    const handleClick = (e) => {
    e.stopPropagation();
    console.log('这个事件监听会被触发,并且阻止冒泡是无法生效的');
    }

    return (
    <div onClick={handleClick}>
    <p>这个子节点被放置在父节点 div 中。</p>
    { createPortal(<div>事件监听</div>, document.body) }
    {/**
    * 要想 handleClick 不触发,需要手动阻止 portal 元素的冒泡
    * 但是无法阻止冒泡到 body 上
    * 如果需要阻止,需要在 body 事件监听中通过event.target 对该元素的点击进行过滤判断
    **/}
    {/* { createPortal(<div onClick={e => e.stopPropagation()}>事件监听</div>, document.body) } */}
    </div>
    );
    }

高阶组件​

  • 高阶组件(Higher-Order Components,简称为 HOC)是参数为组件,返回值为新组件的函数

    • 高阶组件本身不是一个组件,而是一个函数
    • 主要作用:对组件进行劫持和增强
    • 主要用于类组件,Hooks 出现后,通过自定义 Hook 基本可以实现类似高阶组件的功能
  • 实际应用

    • props 增强(比如 React-Router V6 后,类组件无法使用 navigate 方法)

      import React, { PureComponent, ReactElement } from 'react';
      import { useNavigate } from 'react-router-dom';

      // 高阶组件
      function withRouter(WrapperComponent) {
      return (props) => {
      const navigate = useNavigate();

      return <WrapperComponent {...props} navigate={navigate} />;
      };
      }

      class App extends PureComponent {
      handleClick = () => {
      // 经过高阶组件增强后,类组件实现了跳转路由的功能
      this.props.navigate('/demo');
      }

      render() {
      return <button onClick={this.handleClick}>跳转</button>;
      }
      }

      export default withRouter(App);
    • 自定义 react-redux 的 connect 函数

      import React, { PureComponent } from 'react';
      import store from './redux-store';

      export default function connect(mapStateToProps, mapDispatchToProps) {
      return function handleMapCpn(WrapperComponent) {
      return class extends PureComponent {
      super(props, context) {
      this.state = {
      storeState: mapStateToProps(store.getState())
      }
      }

      componentDidMount() {
      this.unSubscribe = store.subscribe(() => {
      this.setState({
      storeState: mapDispatchToProps(store.getState())
      })
      })
      }

      componentWillUnmount() {
      this.unSubscribe();
      }

      render() {
      return (
      <WrapperComponent
      {...this.props}
      {...mapStateToProps(store.getState())}
      {...mapDispatchToProps(store.dispatch)}
      />
      )
      }
      }
      }
      }

性能优化​

类组件

  • 默认情况下,只要父组件重新渲染或者当前组件调用了更新状态(即使状态并未改变)的方法

    • 此时组件就会重新渲染,同时当前组件的所有子组件都会重新渲染
    • 这会造成无意义的 diff 比较 和 更新 DOM
    export default class Counter extends React.Component {
    state = {
    count: 0
    }

    increment = () => {
    // 状态并未改变,也进行了重新渲染
    this.setState({ count: this.state.count });
    }

    render() {
    console.log('render');

    return (
    <>
    <h2>{this.state.count}</h2>
    <button onClick={this.increment}>更新</button>
    </>
    );
    }
    }
  • 可以借助 shouldComponentUpdate 钩子函数控制类组件是否重新渲染

    • 返回 false:跳过更新
    • 不返回或返回 true:进行更新
    • 不要进行深层比较或使用 JSON.stringify()
    /* ...... */
    shouldComponentUpdate(nextProps, nextState) {
    if (nextState.count === this.state.count) {
    return false;
    }
    }
  • 同时,React 内部已经内置了 PureComponent 组件

    • 直接继承即可实现对应的自动比较更新
    • 该组件内部实现的是浅比较 shallowEqual
    export default class Counter extends React.PureComponent {}

函数式组件

  • 对于函数式组件,直接使用 memo 高阶函数即可实现 props 的浅比较

    const App = memo(({ title }) => {
    return <h2>{title}</h2>;
    });

错误边界​

  • 错误边界是一种 React 组件,需要结合 getDerivedStateFromError 和 componentDidCatch 一起使用

    • 这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误
    • 同时展示降级 UI
    • 自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载
    class ErrorBoundary extends React.Component {
    constructor(props) {
    super(props);
    this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
    }

    render() {
    if (this.state.hasError) {
    // 你可以自定义降级后的 UI 并渲染
    return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
    }
    }
  • 错误边界可以捕获发生在整个子组件树的渲染期间、生命周期以及构造函数中的错误

    • 无法捕获事件处理、异步代码、服务端渲染、它自己抛出的错误
    • 函数组件要想实现错误边界组件,需要自己实现类似的功能(HOC),或者使用 react-error-boundary

样式方案​

内联样式​

  • 内联样式是官方推荐的一种 CSS 样式的写法

    • style 属性接收一个采用小驼峰命名属性的 JavaScript 对象
    • 并且可以引用 state 和 props 中的变量来设置相关的样式
    export default function App({ color }) {
    return (
    <div style={{ fontSize: 20, color }}>Hello React!</div>
    );
    }
  • 内联样式的优点

    • 样式之间不会有冲突
    • 可以动态获取 JavaScript 变量值
  • 内联样式的缺点

    • 写法上都需要使用驼峰标识
    • 大量的样式 , 代码混乱
    • 某些样式无法编写 比如伪类、伪元素

全局样式​

  • 全局样式:将 CSS 编写到一个单独的文件,之后再进行引入

    • 这样的编写方式和普通的网页开发中编写方式是一致的
    import React from 'react';
    // 引入全局样式
    import './App.css';

    export default function App() {
    return <div className="wrapper">全局样式</div>;
    }
  • 最大的缺点:不同 CSS 文件中的同名类名会导致样式冲突

模块化样式​

  • 相比于全局样式,CSS Module 解决了局部作用域的问题

    • 本质上是为每个组件的类名添加了唯一 hash
import React from 'react';
// 引入模块化样式
import { title } from './App.module.css';

export default function App() {
return <div className={title}>全局样式</div>;
}
  • 缺点:CSS 中无法读取 JavaScript 变量值,无法生成动态样式

CSS in JS​

认识 CSS in JS

  • 官方文档也有提到过 CSS in JS 这种方案
    • “CSS in JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义
    • 此功能并不是 React 的一部分,而是由第三方库提供
  • CSS in JS 的模式:将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 变量

使用 styled-components

  • styled-components 的本质是通过函数的调用,最终创建出一个组件

    • 这个组件会被自动添加上一个不重复的类名
    • 它支持类似于 CSS 预处理器一样的样式嵌套
import React from 'react';
import { AppWrapper } from './App_style';

export default function App() {
return <AppWrapper>全局样式</AppWrapper>;
}
  • 使用该样式组件时支持传递 props,动态设置样式,在样式文件中通过模板字符串获取 props
import React from 'react';
import { AppWrapper } from './App_style';

export default function App() {
return <AppWrapper color="skyblue">全局样式</AppWrapper>;
}
  • 支持组件样式的继承
import React from 'react';
import { PrimaryButton } from './App_style';

export default function App() {
return <PrimaryButton color="orange">按钮</PrimaryButton>;
}
  • 使用 ThemeProvider 支持全局主题的配置
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { PrimaryButton } from './App_style';

export default function App() {
return (
<ThemeProvider theme={{ primary: '#f6da76' }}>
<PrimaryButton>按钮</PrimaryButton>
</ThemeProvider>
);
}

React Hooks​

认识 Hook​

  • 类组件的缺点

    • 复杂组件变得难以理解、维护
    • 难以理解的 class
    • 组件复用状态很难(比如多层 Context 嵌套,让组件逻辑变得复杂)
  • Hook 是 React 16.8 的新增特性

    • 它可以让我们在不编写 class 的情况下使用 state 以及其他 React 特性
    • 完全可选,100% 向后兼容
    • 基本可以代替所有类组件
  • Hook 的约束规则

    • 只能在函数最外层调用,不能在循环、条件判断或子函数中调用
    • 只能在 React 的 函数组件或 自定义 Hook 中使用,不要在其他普通函数中调用

常用 Hook​

useState​

为函数组件提供状态

  • useState 可以向组件添加一个状态变量

    • 参数:传递一个初始化值,可以传递初始值或者传递一个计算函数(传递的计算函数只在初始化时调用一次)
    • 返回值:[state, setState],返回当前状态值和更新状态值的更新函数
    • setState 可以将 state 更新为不同的值并触发重新渲染
    function App() {
    const [count, setCount] = useState(0);
    // const [count, setCount] = useState(() => 0);

    return (
    <div>
    <h2>{count}</h2>
    <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
    );
    }
  • React 只在初次渲染时读取初始状态,后续渲染时将其忽略

set 函数分析

  • set 函数仅更新下一次渲染的状态变量,因此无法在本轮更新后获取到最新值

    function App() {
    const [count, setCount] = useState(0);

    const increment = () => {
    console.log(count); // 0

    setCount(count + 1); // 请求使用 1 重新渲染
    console.log(count); // 仍然是 0!

    setTimeout(() => {
    console.log(count); // 还是 0!
    }, 5000);
    }

    return <button onClick={increment}>+1</button>;
    }
  • set 函数可以直接传递新状态,也可以传递一个根据先前状态来计算新状态的函数

    • 传递的更新函数必须是一个纯函数

    • React 会将更新函数放入队列中并重新渲染组件,在下一次渲染期间,React 将通过把队列中所有更新函数应用于先前的状态来计算下一个状态

      function App() {
      const [count, setCount] = useState(0);

      const increment = () => {
      // 本轮更新后,count -> 2
      setCount((pre) => pre + 1);
      setCount((pre) => pre + 1);
      }

      return <button onClick={increment}>+1</button>;
      }
    • 如果提供的新值与当前 state 相同,React 将 跳过重新渲染该组件及其子组件(对于对象类型 state,需要生成一个新对象)

      function App() {
      const [info, setInfo] = useState({
      name: 'Avril',
      age: 17
      });

      const changeInfo = () => {
      // 生成一个原对象的浅拷贝
      setInfo({ ...info, age: info.age + 1 });
      }

      return (
      <div>
      <h2>{info.name} - {info.age}</h2>
      <button onClick={changeInfo}>更新</button>
      </div>
      );
      }
  • React 会批量处理状态更新,如果需要强制 React 更早地更新屏幕,可以使用 ReactDOM.flushSync(cb) (不推荐使用)

useEffect​

执行副作用

  • useEffect 用于执行组件运行期间需要的副作用

    • 参数一(setup):一个回调函数,在 React 执行完更新 DOM 操作之后,就会回调这个函数
      • 该回调函数可以选择性返回一个 清理(cleanup) 函数,用于移除事件监听、取消订阅等副作用
      • 在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数,然后使用新值运行 setup 函数
      • 当组件从页面卸载后,cleanup 代码 将运行最后一次
    • 参数二:setup 代码中引用的所有响应式值的列表
      • 响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数
      • 当响应值变化后(浅比较),setup 函数会自动执行
      • 如果不传递该参数,则组件每次重新渲染都会执行 setup 函数
    function App() {
    const [count, setCount] = useState(0);

    useEffect(() => {
    // 此处可以获取到更新后的 count 值
    }, [count]);

    return (
    <button onClick={() => setCount(count + 1)}>
    +1
    </button>
    );
    }
  • 无法“选择” Effect 的依赖项

    • Effect 代码中使用的每个响应式值,都必须声明为依赖项
    • 这种模式就是一种闭包

模拟组件生命周期

  • 模拟 componentDidMount 和 componentDidUpdate

    function App() {
    const [count, setCount] = useState(0);

    useEffect(() => {
    // 相当于 componentDidMount
    }, []);

    useEffect(() => {
    // 第一次执行时,相当于 componentDidMount
    // 后续 count 更新重新执行,相当于 componentDidUpdate
    }, [count]);

    return (
    <button onClick={() => setCount(count + 1)}>
    +1
    </button>
    );
    }
  • 模拟 componentWillUnmount

    function App() {

    useEffect(() => {
    return () => {
    // 组件卸载时执行,相当于 componentWillUnmount
    }
    }, []);

    return <h2>Hello useEffect</h2>;
    }

useRef​

  • useRef 可以引用一个不需要渲染的值

    • 参数:ref 对象的 current 属性的初始值,这个参数会首次渲染后被忽略
    • 返回值:返回一个 ref 对象,该对象在组件的整个生命周期保持不变,内部的 current 属性可以被读写
  • 应用场景

    • 保存一个不需要渲染的值,保存的值可以在 useEffect 中读取到最新的值,不存在异步的问题

      function Stopwatch() {
      const intervalRef = useRef(0);

      useEffect(() => {
      // 时刻可以读取到最新的值
      intervalRef.current++;
      console.log(intervalRef.current); // 1
      }, []);
      }
    • 引用一个 DOM 元素,操作原生 DOM

      export default function Stopwatch() {
      const inputRef = useRef(null);

      useEffect(() => {
      // 挂载后自动聚焦
      inputRef.current.focus();
      }, []);

      return <input type="text" ref={inputRef} />
      }

性能优化 Hook​

useMemo​

  • useMemo 在每次重新渲染的时候能够缓存计算的结果

    • 参数一:要缓存计算值的函数,是一个没有任何参数的纯函数
    • 参数二:响应式变量列表
    • 返回值:计算函数的返回结果,仅在初次渲染和依赖更新时重新计算
  • 应用场景

    • 跳过代价昂贵的重新计算

      function App({ numberList }) {
      const evenNumberList = useMemo(() =>
      numberList.filter(n => n % 2 === 0),
      [numberList]
      );
      }
    • 优化重新渲染子组件的性能

      • 默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件
      • 在组件树嵌套层级非常深时,顶级组件无意义的数据变化会导致整个组件树重新渲染,导致性能变差
      • 在响应式变量未发生变化时,结合 memo 可以跳过子组件的重新渲染
      import React, { memo, useMemo, useState } from 'react';

      const Header = memo(({ info }) => {
      console.log('re-render');
      return <h2>{info.name} - {info.age}</h2>;
      });

      const Counter = memo(({ count }) => <h2>{count}</h2>);

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

      const info = useMemo(() => ({
      /**
      * 每次执行 setCount 都会使组件重新渲染
      * 如果不使用 useMemo,相当于每次都创建了一个新对象
      * Header 组件每次都会重新渲染
      */
      name: 'Avril',
      age: 17
      }), []);

      return (
      <>
      <Header info={info} />
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>+1</button>
      </>
      )
      }

useCallback​

  • useCallback 可以在多次渲染中缓存函数

    • 参数一:要缓存的函数

    • 参数二:响应式变量列表

    • 返回值:一个函数,仅在初次渲染和依赖更新时重新计算

    • 简化实现

      // 在 React 内部的简化实现
      function useCallback(fn, dependencies) {
      return useMemo(() => fn, dependencies);
      }
  • 应用场景

    • 从记忆化回调中更新 state,通过传递更新函数来移除依赖

      function TodoList() {
      const [todos, setTodos] = useState([]);

      const handleAddTodo = useCallback((text) => {
      const newTodo = { id: nextId++, text };
      setTodos(todos => [...todos, newTodo]);
      }, []); // ✅ 不需要 todos 依赖项
      // ...
      }
    • 优化重新渲染子组件的性能

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

      const Header = memo(({ info, changeInfo }) => {
      console.log('re-render');
      return (
      <>
      <h2>{info.name} - {info.age}</h2>
      <button onClick={changeInfo}>更新</button>
      </>
      )
      });

      const Counter = memo(({ count }) => <h2>{count}</h2>);

      export default function App() {
      const [count, setCount] = useState(0);
      const [info, setInfo] = useState({ name: 'Avril', age: 18 });

      const changeInfo = useCallback(() => {
      /**
      * 每次执行 setCount 都会使组件重新渲染
      * 如果不使用 useCallback,相当于每次都创建了一个新函数
      * Header 组件每次都会重新渲染
      */
      setInfo({ ...info, age: info.age + 1 });
      }, [info])

      return (
      <>
      <Header info={info} changeInfo={changeInfo} />
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>+1</button>
      </>
      );
      }

React@18 Hook​

useId​

  • useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook
    • 主要用于 React 的同构应用开发
    • React 确保它能够与服务端渲染一起工作
  • 不能使用 useId 来生成列表中的 key

useTransition​

  • useTransition 可以在不阻塞 UI 的情况下来更新状态

    • 返回值:[isPending, startTransition]
      • isPending:是否存在待处理的转换
      • startTransition:必须接收一个同步函数,允许将状态更新标记为延迟状态
    import React, { useState, useTransition } from 'react';

    const list = Array(10000).fill(null).map((i, index) => index);

    const filterNumberList = (numberList, checked = true) => {
    if (checked) {
    return numberList.filter(n => n % 2 === 0);
    } else {
    return numberList.filter(n => n % 2 !== 0);
    }
    }

    export default function App() {
    const [isPending, startTransition] = useTransition();
    const [isEven, setIsEven] = useState(true);
    const [numberList, setNumberList] = useState(filterNumberList(list));

    const handleToggle = (e) => {
    const { checked } = e.target;
    setIsEven(checked);

    /**
    * 不使用 startTransition
    * 在处理列表更新时, 复选框的选中状态切换明显延迟
    */
    // const newNumberList = filterNumberList(list, checked);
    // setNumberList(newNumberList);

    /**
    * 使用 startTransition
    * 复选框的选中状态正常无阻塞更新
    * 列表更新被降低优先级处理
    */
    startTransition(() => {
    const newNumberList = filterNumberList(list, checked);
    setNumberList(newNumberList);
    });
    }

    return (
    <div>
    <label htmlFor="is-even">
    <input
    type="checkbox"
    id="is-even"
    checked={isEven}
    onChange={handleToggle}
    />
    显示偶数
    </label>

    { isPending ? 'Loading....' : numberList.map(n => <div key={n}>{n}</div>) }
    </div>
    );
    }
  • React 会立即执行传递给 startTransition 的函数

    • 但是在它运行的同时安排的任何状态更新都被标记为延迟更新

useDeferredValue​

  • useDeferredValue 接受一个值,并返回该值的新副本

    • 该副本的更新将会被延迟更新(相对于那些需要紧急更新的内容)
    • 组件件更新时,React 将会先尝试使用旧值进行重新渲染(因此它将返回旧值)
    • 然后再在后台使用新值进行另一个重新渲染
    const list = Array(10000).fill(null).map((i, index) => index);

    const filterNumberList = (numberList, checked = true) => {
    if (checked) {
    return numberList.filter(n => n % 2 === 0);
    } else {
    return numberList.filter(n => n % 2 !== 0);
    }
    }

    export default function App() {
    const [isEven, setIsEven] = useState(true);
    const [numberList, setNumberList] = useState(filterNumberList(list));
    const deferredNumberList = useDeferredValue(numberList);

    const handleToggle = (e) => {
    const { checked } = e.target;
    setIsEven(checked);
    /**
    * 使用 useDeferredValue
    * 复选框的选中状态正常无阻塞更新
    * 列表更新被降低优先级处理
    */
    setNumberList(filterNumberList(list, checked));
    }

    return (
    <div>
    <label htmlFor="is-even">
    <input
    type="checkbox"
    id="is-even"
    checked={isEven}
    onChange={handleToggle}
    />
    显示偶数
    </label>
    { deferredNumberList.map(n => <div key={n}>{n}</div>) }
    </div>
    );
    }
  • 由 useDeferredValue 引起的后台重新渲染在提交到屏幕之前不会触发 Effect

  • 结合 Suspense

    • 如果由于新值引起的后台更新导致 UI 暂停,用户将不会看到 fallback 效果
    • 将看到旧的延迟值,直到数据加载完成
    export default function App() {
    /* ...... */
    return (
    <Suspense fallback="处理中...">
    { deferredNumberList.map(n => <div key={n}>{n}</div>) }
    </Suspense>
    );
    }
  • React 延迟实现

    • 当 useDeferredValue 接收到与之前不同的值
    • 除了当前渲染,它还会安排一个后台重新渲染(可以被中断的)
    • 当触发的新的更新,React 会从头开始重新启动后台渲染

额外 Hook​

useContext​

  • useContext 用于读取和订阅组件中的 Context

    • 参数:一个 Context 对象,由 React.createContext 创建
    • 返回值:由 Provider 共享的 value 值
    • 如果 Context 发生变化,React 会自动重新渲染读取 Context 的组件
    import { useContext } from 'react';

    function Button() {
    const theme = useContext(ThemeContext);
    // ...
    }
  • 使用 Context 传递数据时,在传递对象和函数时可以借助 useMemo 和 useCallback 优化重新渲染

    import { useCallback, useMemo } from 'react';

    function MyApp() {
    const [currentUser, setCurrentUser] = useState(null);

    const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
    }, []);

    const contextValue = useMemo(() => ({
    currentUser,
    login
    }), [currentUser, login]);

    return (
    <AuthContext.Provider value={contextValue}>
    <Page />
    </AuthContext.Provider>
    );
    }

useReducer​

  • useReducer 用于处理复杂的 state,仅仅是 useState 的一种替代方案

    • 参数列表
      • 参数一(reducer):用于更新 state 的纯函数
      • 参数二:初始状态值
      • 参数三:用于计算初始值的函数
    • 返回值:[state, dispatch]
    import { useReducer } from 'react';

    function reducer(state, action) {
    // ...
    }

    function MyComponent() {
    const [state, dispatch] = useReducer(reducer, { age: 42 });
    // ...
    }
  • 调用 dispatch 函数 不会改变当前渲染的 state

useImperativeHandle​

  • useImperativeHandle 用于自定义由 ref 暴露出来的句柄,需要结合 forwardRef 使用

    • 参数一:从 forwardRef 渲染函数中获得的第二个参数
    • 参数二:一个包含想暴露的属性和方法的对象
    import { forwardRef, useRef, useImperativeHandle } from 'react';

    const MyInput = forwardRef(function MyInput(props, ref) {
    const inputRef = useRef(null);

    useImperativeHandle(ref, () => {
    return {
    focus() {
    inputRef.current.focus();
    },
    scrollIntoView() {
    inputRef.current.scrollIntoView();
    },
    };
    }, []);

    return <input {...props} ref={inputRef} />;
    });
  • 不要滥用 ref,如果可以通过 props 实现,那就不应该使用 ref

useLayoutEffect​

  • useLayoutEffect 和 useEffect 非常地相似,区别在于

    • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新
    • useLayoutEffect 会在渲染的内容更新到 DOM 上前执行,会阻塞 DOM 的更新
    • 可以用于在浏览器重新绘制屏幕前计算布局
    image-20211107234705550
    function Tooltip() {
    const ref = useRef(null);
    const [tooltipHeight, setTooltipHeight] = useState(0); // 你还不知道真正的高度

    useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // 现在重新渲染,你知道了真实的高度
    }, []);

    // ... 在下方的渲染逻辑中使用 tooltipHeight ...
    }
  • useLayoutEffect 可能会影响性能,尽可能使用 useEffect

自定义 Hook​

  • 自定义 Hook 本质上只是一种函数代码逻辑的抽取

    • 严格意义上来说,它本身并不算 React 的特性
    • 函数名必须以 use 开头
  • 应用实例

    • 获取元素的滚动位置

      import { useEffect, useState } from 'react';

      /**
      * 获取滚动位置
      * @param {HTMLElement} el 要监听滚动的元素
      */
      const useScrollPosition = (el) => {
      const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });

      useEffect(() => {
      const handleScroll = () => {
      const { scrollLeft, scrollTop } = el;

      setScrollPosition({ x: scrollLeft, y: scrollTop });
      }

      el.addEventListener('scroll', handleScroll);

      return () => {
      el.removeEventListener('scroll', handleScroll);
      }
      }, []);

      return scrollPosition;
      }
    • 本地数据存取

      function useLocalStorage(key) {
      const [data, setData] = useState(() => JSON.parse(localStorage.getItem(key)));

      useEffect(() => {
      localStorage.setItem(key, JSON.stringify(data))
      }, [data]);

      return [data, setData];
      }

扩展知识​

过渡动画​

  • 借助 react-transition-group 可以为 React 组件在切换时添加一些过渡动画,提供更好的交互
  • 该库主要包含四个组件
    • Transition:该组件是一个和平台无关的组件,在前端开发中,比较常用的是 CSSTransition
    • CSSTransition:添加过渡动画效果
    • SwitchTransition:用于两个组件显示和隐藏切换
    • TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画

CSSTransition​

  • CSSTransition 用于执行组件显示/隐藏切换的动画

    • 动画执行过程中,有三个状态:appear、enter、exit
    • 属性 classNames 指定类名前缀
    • 属性 timeout 指定动画时间,最好与 CSS 样式中的时间保持一致
    • 属性 appear 指定是否在初次进入添加动画
  • 需要指定 in 属性来触发进入或退出状态

    • 如果添加 unmountOnExit={true},组件会在执行退出动画结束后被移除掉
    • 当 in 为 true 时触发进入状态
      • 会添加 -enter、-enter-acitve 的类名开始执行动画
      • 当动画执行结束后,会移除两个这两个类名,并且添加 enter-done 的类名
    • 当 in 为 false 时触发退出状态
      • 会添加 -exit、-exit-active 的类名开始执行动画
      • 当动画执行结束后,会移除这两个类名,并且添加 -exit-done 的类名
    CSSTransition
import React, { PureComponent } from 'react';
import { CSSTransition } from 'react-transition-group';
import './App.css';

export default class App extends PureComponent {
state = {
show: true
}

toggleShow = () => {
this.setState({
show: !this.state.show
});
}

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

return (
<div>
<button onClick={this.toggleShow}>切换显示</button>

<CSSTransition
in={show}
timeout={2000}
unmountOnExit
classNames="react"
appear
>
<h2>Hello React</h2>
</CSSTransition>
</div>
);
}
}

SwitchTransition​

  • SwitchTransition 可以完成两个组件之间切换的炫酷动画

  • SwitchTransition 中主要有一个属性 mode

    • in-out:新组件先进入,旧组件再移除
    • out-in:旧组件先移除,新组件再进入
  • SwitchTransition 仍然需要借助 CSSTransition 来设置动画

    • 此时不再借助 in 属性,而是通过 key 属性来进行切换组件
SwitchTransition
import React, { PureComponent } from 'react';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
import './App.css';

export default class App extends PureComponent {
state = {
toggle: true
}

toggleShow = () => {
this.setState({
toggle: !this.state.toggle
});
}

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

return (
<div>
<button onClick={this.toggleShow}>切换显示</button>

<SwitchTransition>
<CSSTransition
key={toggle ? 'React' : 'Vue'}
timeout={2000}
unmountOnExit
appear
classNames="toggle"
>
<h2>{ toggle ? 'Hello React' : 'Hello Vue' }</h2>
</CSSTransition>
</SwitchTransition>
</div>
);
}
}

TransitionGroup​

TransitionGroup 用于对一组数据的变化添加动画

  • 需要将这些 CSSTransition 放入到一个 TransitionGroup 中来完成动画
TransitionGroup
export default class App extends PureComponent {
state = {
list: [0, 1, 2, 3]
}

update = () => {
const { list } = this.state;
this.setState({ list: [list.length, ...list] });
}

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

return (
<div>
<TransitionGroup>
{
list.map(i => (
<CSSTransition
key={i}
timeout={2000}
appear
classNames="group"
>
<span className="item">{i}</span>
</CSSTransition>
))
}
</TransitionGroup>

<div>
<button onClick={this.update}>更新</button>
</div>
</div>
)
}
}