Hooks

1. Hooks介绍

1.1 Hooks是什么

  • Hook是React16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)
  • Hook指的类似于useState、useEffect这样的函数,Hooks是对这类函数的统称

1.2 class组件相对于函数式组件有什么优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等

1.3 class组件存在的问题

  • 随着业务的增多,class组件变得非常复杂
  • 学习难度较大
  • 组件复用状态难度大

1.4 Hooks使用场景

  • Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景)
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

2. useState

2.1 介绍

  • useState会定义一个与class里this.state功能完全相同的state变量,从而实现在函数式组件中应用state

2.2 参数

  • 初始化值,如果不设置为undefined

2.3 返回值:数组,共有两个元素

  • 元素1: 当前state的值
  • 元素2: 新函数,用于设置新的值

2.4 使用规则

  1. 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  2. 只能在React的函数组件中调用Hook。不要在其他JavaScript函数中调用

2.5 注意事项

  • 修改值时不可直接采用点击事件的方式修改,应展开原State再传入新元素<button onClick={e => setFriends([…friends, “tom”])}>添加朋友

2.6 求和案例:

import React, { useState } from 'react'

export default function ComplexHookState() {

  const [friends, setFriends] = useState(['xiaoming', 'xiaohong'])
  const [students, setStudents] = useState([
    { id: 110, name: 'why', age: 18 },
    { id: 111, name: 'xiaoming', age: 30 },
    { id: 112, name: 'xiaohong', age: 25 },
  ])

  function addFriend() {
    friends.push('hmm')
    setFriends(friends)
  }

  function incrementAgeWithIndex(index) {
    const newStudents = [...students]
    newStudents[index].age += 1
    setStudents(newStudents)
  }

  return (
    <div>
      <h2>好友列表:</h2>
      <ul>
        {
          friends.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
      <button onClick={e => setFriends([...friends, 'tom'])}>添加朋友</button>
      {/* 错误的做法 */}
      <button onClick={addFriend}>添加朋友</button>

      <h2>学生列表</h2>
      <ul>
        {
          students.map((item, index) => {
            return (
              <li key={item.id}>
                <span>名字: {item.name} 年龄: {item.age}</span>
                <button onClick={e => incrementAgeWithIndex(index)}>age+1</button>
              </li>
            )
          })
        }
      </ul>
    </div>
  )
}

3. useEffect

3.1 介绍

  • useEffect会在函数式组件中实现类似class生命周期的功能,通过useEffect的Hook可以告诉React需要在渲染后执行某些操作

3.2 参数

  • 参数一:传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
  • 参数二:传入一个数组,数组内传入useEffect执行所依赖的元素,即当该元素发生变化或页面刷新时才执行该useEffect。传入空数组则该useEffect只在页面刷新时执行。涉及到useEffect的性能优化

3.3 执行规则

  1. 在React执行完更新DOM操作之后,就会回调这个函数
  2. 默认情况下,无论是第一次渲染之后还是每次更新之后,都会执行这个回调函数
  3. return一个回调函数,在取消事件订阅时自动调用
  4. 定义多个useEffect时按其定义的先后顺序执行

3.4 用法

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

export default function Comp1(){
  const [count, setCount] = useState(0)
  const [isLogin, setIsLogin] = useState(true)

  useEffect(() => {
    console.log('修改DOM', count)
  }, [count]) //count会调

  useEffect(() => {
    console.log('订阅事件')
  }, []) //都不会调

  useEffect(() => {
    console.log('网络请求')
  })
  //都会调

  return (
    <div>
      <h2>MultiEffectHookDemo</h2>
      <h2>{count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <h2>{isLogin ? 'TaoLoading' : '未登录'}</h2>
      <button onClick={e => setIsLogin(!isLogin)}>登录/注销</button>
    </div>
  )
}

4.Context

在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,子组件给后代组件设置props,props在组件间自上向下(父传子)的逐层传递数据。但并不是所有的数据都适合这种传递方式,有些数据需要在多个组件中共同使用,如果还通过props一层一层传递,麻烦自不必多说。
Context为我们提供了一种在不同组件间共享数据的方式,它不再拘泥于props刻板的逐层传递,而是在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。换句话说,Context类似于JS中的全局作用域,可以将一些公共数据设置到一个同一个Context中,使得所有的组件都可以访问到这些数据。

2.1 何时使用 Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

class A extends React.Component {
  render() {
    return <B theme="dark" />;
  }
}

function B(props) {
  // B 组件接受一个额外的“theme”属性,然后传递给 C 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <C theme={props.theme} />
    </div>
  );
}

class C extends React.Component {
  render() {
    return h4>我从A组件接收到的主题模式:{this.props.theme}</h4>
  }
}

使用 context, 我们可以避免通过中间元素传递 props。

2.2 类式组件

当我们想要给子类的子类传递数据时,前面我们讲过了 redux 的做法,这里介绍的 Context 我觉得也类似于 Redux

// React.createContext
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
首先我们需要引入一个 ThemeContext 组件,我们需要引用ThemeContext 下的 Provider

const ThemeContext  = React.createContext();
const { Provider } = ThemeContext ;

Provider译为生产者,和Consumer消费者对应。Provider会设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有的Provider子组件中都可以访问。Context的搜索流程和JS中函数作用域类似,当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。

<Provider value={{ theme }}>
    <B />
</Provider>
/* 
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
*/

但是我们需要在使用数据的组件中引入 ThemeContext
static contextType = ThemeContext ;
在使用时,直接从 this.context 上取值即可
const {theme} = this.context

完整版的使用
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext  = React.createContext('light')

export default class A extends Component {

    state = {theme:'dark'}

    render() {
        const {theme} = this.state
        // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
        // 无论多深,任何组件都能读取这个值。
        // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
        return (
                <ThemeContext.Provider value={theme}>
                    <B/>
                </ThemeContext.Provider>
        )
    }
}

// 中间的组件再也不必指明往下传递 theme 了。
class B extends Component {
    render() {
        return (
                <>
                    <h3>我是B组件</h3>
                   <C/>
                 </>
        )
    }
}

class C extends Component {
    //声明接收context
    // 指定 contextType 读取当前的 theme context。
    // React 会往上找到最近的 theme Provider,然后使用它的值。
    // 在这个例子中,当前的 theme 值为 “dark”。
    static contextType = ThemeContext 
    
    render() {
        const {theme} = this.context
        return (
                <>
                    <h3>我是C组件</h3>
                    <h4>我从A组件接收到的主题模式:{theme}</h4>
                 </>
        )
    }
}

挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。

2.3 函数组件

函数组件和类式组件只有一点点小差别
Context对象中有一个属性叫做Consumer,直译过来为消费者,如果你了解生产消费者模式这里就比较好理解了,如果没接触过,你可以将Consumer理解为数据的获取工具。你可以将它理解为一个特殊的组件,所以你需要这样使用它

完整版的使用
import React, { useState, useEffect,Component } from 'react'
const ThemeContext  = React.createContext('light')
export default  class Comp1 extends Component{
  state = {theme:'dark'}
  render() {
      const theme = this.state
      // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
      // 无论多深,任何组件都能读取这个值。
      // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
      return (
              <ThemeContext.Provider value={theme}>
                  <B/>
              </ThemeContext.Provider>
      )
  }
}
// 中间的组件再也不必指明往下传递 theme 了。
class B extends Component {
  render() {
      return (
              <>
                  <h3>我是B组件</h3>
                 <C/>
               </>
      )
  }
}

function C(){
  return (
      <div>
          <h3>我是C组件</h3>
          <h4>我从A组件接收到的用户名:
          <ThemeContext.Consumer>
              {value => {
               let {theme}=value
                  return (<span>{theme}</span>)
              }}
          </ThemeContext.Consumer>
          </h4>
      </div>
  )
}

Consumer的标签体必须是一个函数,这个函数会在组件渲染时调用并且将Context中存储的数据作为参数传递进函数,该函数的返回值将会作为组件被最终渲染到页面中。这里我们将参数命名为了ctx,在回调函数中我们就可以通过ctx.xxx访问到Context中的数据。如果需要访问多个Context可以使用多个Consumer嵌套即可。

2.4 hook-useContext

通过Consumer使用Context实在是不够优雅,所以React还为我们提供了一个钩子函数useContext(),我们只需要将Context对象作为参数传递给钩子函数,它就会直接给我们返回Context对象中存储的数据。
因为我们平时的组件不会写的一个文件中,所以React.createContext要单独写在一个文件中
store/theme-context.js

import React from "react";

const ThemeContext  = React.createContext('light')

export default ThemeContext;
import React, {useContext} from 'react';
import ThemeContext from '../store/theme-context';

function C(){
    const ctx = useContext(TestContext);

    return (
        <div>
            <h3>我是C组件</h3>
            <h4>我从A组件接收到的用户名:
            <span>{ctx}</span>
            </h4>
        </div>
    )
}

5. useReducer

5.1 介绍

  • useReducer并非是Redux的替代品,而是类似useState的替代方案
  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
  • 数据是不会共享的,它们只是使用了相同的counterReducer的函数而已

5.2 参数

  • 参数一:reducer函数
  • 参数二:初始值,类型不限

5.3 使用规则

  1. 创建reducer纯函数
  2. 在组件内引入useReducer并传入reducer和初始值从而创建stateconst [state, dispatch] = useReducer(reducer, { counter: 0 })
  3. 分发对应事件
<button onClick={e => dispatch({ type: "increment" })}>+1</button>
<button onClick={e => dispatch({ type: "decrement" })}>-1</button>

6. useCallback

6.1 介绍

  • useCallback实际目的是为了进行性能的优化
  • 将回调函数传入useCallback中,当所依赖值发生变化时才渲染该函数

6.2 传参(参数形式类似useEffect)

  • 参数一:回调函数
  • 参数二:所依赖的值

6.3 应用场景

  • 在将一个组件中的函数, 传递给子组件进行回调使用时, 使用useCallback对函数进行处理

6.4 注意点

  • 子组件为函数式组件时需要用memo()进行包裹,见06-02文件
  • 子组件为类组件时需要继承PureComponent

7. useMemo

7.1 介绍

  • useMemo实际目的也是为了进行性能的优化

7.2 传参(参数形式类似useEffect)

  • 参数一:回调函数
  • 参数二:所依赖的值

8. useRef

8.1 介绍

  • useRef返回一个Ref对象,返回的Ref对象再组件的整个生命周期保持不变
  • 即当组件重新渲染时,无论传入的值是否变化,useRef返回的值总是最初值

8.2 使用规则

  1. 用法一:
    1. 通过useRef()创建一个Refconst titleRef = useRef()
    2. 在组件中绑定到对应的ref

      RefHookDemo01

    3. 获取相对的元素titleRef.current.innerHTML = “Hello World”
  2. 用法二:
    1. 在组件内直接使用useRef()创建一个Refconst numRef = useRef(0)
    2. 读取Ref中的值

      count上一次的值: {numRef.current}

    3. 在组件整个生命周期内返回的Ref对象总是不变的

8.3 使用场景

  • 引用DOM
  • 使用ref保存上一次的某一个值

8.4 注意点

  • 不能直接对函数式组件使用useRef(),需要在其外侧包裹forwardRef(),将函数式组件作为参数传递给,需要在其外侧包裹forwardRef(),并在函数内再进行一次ref的传值。见09-01文件

8.5 案例

import React, { useEffect, useRef } from 'react'

class TestCpn extends React.Component {
  render() {
    return <h2>TestCpn</h2>
  }
}

function TestCpn2(props) {
  return <h2>TestCpn2</h2>
}

export default function RefHookDemo01() {

  const titleRef = useRef()
  const inputRef = useRef()
  const testRef = useRef()
  const testRef2 = useRef()

  function changeDOM() {
    titleRef.current.innerHTML = 'Hello World'
    inputRef.current.focus()
    console.log(testRef.current)
    console.log(testRef2.current)
  }

  return (
    <div>
      <h2 ref={titleRef}>RefHookDemo01</h2>
      <input ref={inputRef} type="text" />
      <TestCpn ref={testRef} />
      {/* <TestCpn2 ref={testRef2} /> */}

      <button onClick={e => changeDOM()}>修改DOM</button>
    </div>
  )
}

9. useImperativeHandle

9.1 介绍

  • 在使用ref时是将整个组件进行暴露,这就导致其他组件拿到该组件后可以进行任意修改
  • useImperativeHandle()可以对暴露的进行限制

9.2 使用规则

  • 在函数式组件内对返回的值使用useImperativeHandle()进行修改。见09-02文件
  • 其中:
  1. 父组件中将inputRef传递给子组件
  2. 子组件经过useImperativeHandle()将useImperativeHandle()返回的对象绑定到ref返回的current中
  3. 最后父组件使用的inputRef是经过处理后的只有focus的元素

10. useLayoutEffect

10.1 与useEffect的区别

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新

10.2 10-02文件执行顺序

  • useEffect
    1. 点击修改数字,将10修改为0
    2. 渲染完毕界面,执行useEffect()
    3. 符合if语句条件,又将0修改为随机数,重新
  • useLayoutEffect
    1. 点击修改数字,将10修改为0
    2. 此时页面为进行渲染,页面还是10,而state中是0
    3. 在渲染完毕页面之前执行useLayoutEffect()
    4. 符合if语句条件,又将0修改为随机数,渲染页面

11.Fragment(代替div)

我们编写组件的时候每次都需要采用一个 div 标签包裹,才能让它正常的编译,但是这样会引发什么问题呢?我们打开控制台看看它的层级
image.png
它包裹了几层无意义的 div 标签,我们可以采用 Fragment 来解决这个问题
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
首先,我们需要从 react 中暴露出 Fragment ,将我们所写的内容采用 Fragment 标签进行包裹,当它解析到 Fragment 标签的时候,就会把它去掉
这样我们的内容就直接挂在了 root 标签下

render() {
  return (
    <React.Fragment 可选 key={xxx.id}>
    <ChildA />
    <ChildB />
    <ChildC />
    </React.Fragment>
  );
}

在React中为我们提供了一种更加便捷的方式,直接使用<></>代替Fragment更加简单:
同时采用空标签,也能实现,但是它不能接收任何值,而 Fragment 能够接收 1 个值key

render() {
  return (
    <>
    <ChildA />
    <ChildB />
    <ChildC />
    </>
  );
}

12.PureComponent (组件优化)

1.1 shouldComponentUpdate 优化

在我们之前一直写的代码中,我们一直使用的Component 是有问题存在的

  1. 只要执行 setState ,即使不改变状态数据,组件也会调用 render
  2. 当前组件状态更新,也会引起子组件 render

而我们想要的是只有组件的 state 或者 props 数据发生改变的时候,再调用 render
我们可以采用重写 shouldComponentUpdate 的方法,但是这个方法不能根治这个问题,当状态很多时,我们没有办法增加判断
看个案例来了解下原理:
如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在这段代码中,shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。

1.2 PureComponent 优化

这段代码可以改成以下这种更简洁的形式:

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。
PureComponent 会对比当前对象和下一个状态的 prop 和 state ,而这个比较属于浅比较,比较基本数据类型是否相同,而对于引用数据类型,比较的是它的引用地址是否相同,这个比较与内容无关

state = {stus:['小张','小李','小王']}

addStu = ()=>{
    /* const {stus} = this.state
    stus.unshift('小刘')
    this.setState({stus}) */

    const {stus} = this.state
    this.setState({stus:['小刘',...stus]})
}

注释掉的那部分,我们是用unshift方法为stus数组添加了一项,它本身的地址是不变的,这样的话会被当做没有产生变化(因为引用数据类型比较的是地址),所以我们平时都是采用合并数组的方式去更新数组。

1.3 案例

import React, { PureComponent } from 'react'
import "./index.css";

export default class A extends PureComponent {
  state = {
    username:"张三"
  }

  handleClick = () => {
    this.setState({})
  }

  render() {
    console.log("A:enter render()")
    const {username} = this.state;
    const {handleClick} = this;

    return (
      <div className="a">
        <div>我是组件A</div>
        <span>我的username是{username}</span>&nbsp;&nbsp;
        <button onClick={handleClick}>执行setState且不改变状态数据</button>
        <B/>
      </div>
    )
  }
}

class B extends PureComponent{
  render(){
    console.log("B:enter render()")
    return (
      <div className="b">
        <div>我是组件B</div>
      </div>
    )
  }
}

点击按钮后不会有任何变化,render函数也没有调用
image.png
修改代码

handleClick = () => {
    this.setState({
      username: '李四',
    })
}

点击按钮后只有A组件的render函数会调用
image.png
修改代码

handleClick = () => {
    const { state } = this
    state.username = '李四'
    this.setState(state)
}

image.png
点击后不会有任何变化,render函数没有调用,这个时候其实是shouldComponentUpdate返回的false。

13.Render Props

如何向组件内部动态传入带内容的结构(标签)?

Vue中: 
    使用slot技术, 也就是通过组件标签体传入结构  <AA><BB/></AA>
React中:
    使用children props: 通过组件标签体传入结构
    使用render props: 通过组件标签属性传入结构, 一般用render函数属性

children props

render() {
    return (
            <A>
              <B>xxxx</B>
            </A>
    )
}


问题: 如果B组件需要A组件内的数据, ==> 做不到

术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
采用 render props 技术,我们可以像组件内部动态传入带有内容的结构
当我们在一个组件标签中填写内容时,这个内容会被定义为 children props,我们可以通过 this.props.children 来获取
例如:
hello
这个 hello 我们就可以通过 children 来获取
而我们所说的 render props 就是在组件标签中传入一个 render 方法(名字可以自己定义,这个名字更语义化),又因为属于 props ,因而被叫做了 render props

<A render={(name) => <B name={name} />} />
A组件: {this.props.render(内部state数据)}
B组件: 读取A组件传入的数据显示 {this.props.data}

你可以把 render 看作是 props,只是它有特殊作用,当然它也可以用其他名字来命名
在上面的代码中,我们需要在 A 组件中预留出 B 组件渲染的位置 在需要的位置上加上{this.props.render(name)}
那我们在 B 组件中,如何接收 A 组件传递的 name 值呢?通过 this.props.name 的方式

export default class Parent extends Component {
	render() {
		return (
			<div className="parent">
				<h3>我是Parent组件</h3>
				<A render={ name => (<B name={name}/>) }/>
			</div>
		)
	}
}

class A extends Component {
	state = {name:'tom'}
	render() {
		console.log(this.props);
		const {name} = this.state
		return (
			<div className="a">
				<h3>我是A组件</h3>
				{this.props.render(name)}
			</div>
		)
	}
}

class B extends Component {
	render() {
		console.log('B--render');
		return (
			<div className="b">
				<h3>我是B组件,{this.props.name}</h3>
			</div>
		)
	}
}

14.Portal

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
李立超老师的博客
这篇博客对于Portal的引出我觉得写的很好
portal – 李立超 | lilichao.com

3.1 问题的引出

在React中,父组件引入子组件后,子组件会直接在父组件内部渲染。换句话说,React元素中的子组件,在DOM中,也会是其父组件对应DOM的后代元素。
但是,在有些场景下如果将子组件直接渲染为父组件的后代,在网页显示时会出现一些问题。比如,需要在React中添加一个会盖住其他元素的Backdrop组件,Backdrop显示后,页面中所有的元素都会被遮盖。很显然这里需要用到定位,但是如果将遮罩层直接在当前组件中渲染的话,遮罩层会成为当前组件的后代元素。如果此时,当前元素后边的兄弟元素中有开启定位的情况出现,且层级不低于当前元素时,便会出现盖住遮罩层的情况。

const Backdrop = () => {
    
  return <div
           style={
                  {
                    position:'fixed',
                    top:0,
                    bottom:0,
                    left:0,
                    right:0,
                    background:'rgba(0,0,0,.3)',
                    zIndex:9999
                  }
                }
           >
  </div>
};

const Box = props => {
    
  return <div
          style={
              {
                width:100,
                height:100,
                background:props.bgColor
              }
            }
           >
             {props.children}
           </div>
};

const App = () => {
    
  return (
  		<div>
            <Box bgColor='yellowgreen'>
            <Backdrop/>
            </Box>
            <Box bgColor='orange' />
  		</div>;
  )
};

上例代码中,App组件中引入了两个Box组件,一个绿色,一个橙色。绿色组件中引入了Backdrop组件,Backdrop组件是一个遮罩层,可以在覆盖住整个网页。
现在三个组件的关系是,绿色Box是橙色Box的兄弟元素,Backdrop是绿色Box的子元素。如果Box组件没有开启定位,遮罩层可以正常显示覆盖整个页面。
image.png
Backdrop能够盖住页面
但是如果为Box开启定位,并设置层级会出现什么情况呢?

const Box = props => {
    
  return <div
           style={
                  {
                    width:100,
                    height:100,
                    background:props.bgColor,
                    position:'relative',
                    zIndex:1
                  }
                }
            >
             {props.children}
           </div>
};

现在修改Box组件,开启相对定位,并设置了z-index为1,结果页面变成了这个样子:
image.png
和上图对比,显然橙色的box没有被盖住,这是为什么呢?首先我们来看看他们的结构:

<App>
    <绿色Box>
            <遮罩/>
    </绿色Box>
    <橙色Box/>
</App>

绿色Box和橙色Box都开启了定位,且z-index相同都为1,但是由于橙色在后边,所以实际层级是高于绿色的。由于绿色是遮罩层的父元素,所以即使遮罩的层级是9999也依然盖不住橙色。
问题出在了哪?遮罩层的作用,是用来盖住其他元素的,它本就不该作为Box的子元素出现,作为子元素了,就难免会出现类似问题。所以我们需要在Box中使用遮罩,但是又不能使他成为Box的子元素。怎么办呢?React为我们提供了一个“传送门”可以将元素传送到指定的位置上。
通过ReactDOM中的createPortal()方法,可以在渲染元素时将元素渲染到网页中的指定位置。这个方法就和他的名字一样,给React元素开启了一个传送门,让它可以去到它应该去的地方。

3.2 Portal的用法

  1. 在index.html中添加一个新的元素
  2. 在组件中中通过ReactDOM.createPortal()将元素渲染到新建的元素中

在index.html中添加新元素:

修改Backdrop组件: ``` const backdropDOM = document.getElementById('backdrop');

const Backdrop = () => {
return ReactDOM.createPortal(
<div
style={
{
position:‘fixed’,
top:0,
bottom:0,
left:0,
right:0,
zIndex:9999,
background:‘rgba(0,0,0,.3)’
}
}
>
,
backdropDOM
);
};

如此一来,我们虽然是在Box中引入了Backdrop,但是由于在Backdrop中开启了“传送门”,Backdrop就会直接渲染到网页中id为backdrop的div中,这样一来上边的问题就解决了
### 3.3 通过 Portal 进行事件冒泡
尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 _React 树_, 且与 _DOM 树_ 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。
这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 _React 树_的祖先,即便这些元素并不是 _DOM 树_ 中的祖先。假设存在如下 HTML 结构:
``` 在 #app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。 ``` // 在 DOM 中有两个容器是兄弟级 (siblings) const appRoot = document.getElementById('app-root'); const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement(‘div’);
}

componentDidMount() {
// 在 Modal 的所有子元素被挂载后,
// 这个 portal 元素会被嵌入到 DOM 树中,
// 这意味着子元素将被挂载到一个分离的 DOM 节点中。
// 如果要求子组件在挂载时可以立刻接入 DOM 树,
// 例如衡量一个 DOM 节点,
// 或者在后代节点中使用 ‘autoFocus’,
// 则需添加 state 到 Modal 中,
// 仅当 Modal 被插入 DOM 树中才能渲染子元素。
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el
);
}
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// 当子元素里的按钮被点击时,
// 这个将会被触发更新父元素的 state,
// 即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
clicks: state.clicks + 1
}));
}

render() {
return (


Number of clicks: {this.state.clicks}



Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.






);
}
}

function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 ‘onClick’ 属性
return (


Click

);
}

const root = ReactDOM.createRoot(appRoot);
root.render();

![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1683247763863-5d77356e-d2f8-4118-9238-1b33c908b41f.png#averageHue=%237f7f7f&clientId=ue74b1059-5eee-4&from=paste&id=u9c30d868&originHeight=312&originWidth=1918&originalType=url&ratio=2&rotation=0&showTitle=false&size=80429&status=done&style=none&taskId=u280bc37a-cf0b-4562-b5e8-d7b31179e29&title=)
点击click后,可以发现数字从0变成1了
![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1683247764115-72b52793-b9d5-428c-b03b-b2a6b44b1db5.png#averageHue=%237c7c7c&clientId=ue74b1059-5eee-4&from=paste&id=u47834510&originHeight=243&originWidth=1430&originalType=url&ratio=2&rotation=0&showTitle=false&size=54152&status=done&style=none&taskId=u014888f2-063b-4a54-a027-e9027db37b8&title=)
子组件Child的点击事件能冒泡到父组件Parent,触发父元素的点击事件
[在 CodePen 上尝试](https://codepen.io/gaearon/pen/jGBWpE)
在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 <Modal /> 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。

 
## 14.错误边界
### 3.1 基本使用
当不可控因素导致数据不正常时,我们不能直接将报错页面呈现在用户的面前,由于我们没有办法给每一个组件、每一个文件添加判断,来确保正常运行,这样很不现实,因此我们要用到**错误边界**技术
错误边界是一种 React 组件,这种组件**可以捕获发生在其子组件树任何位置的JavaScript错误,并打印这些错误,同时展示降级UI**,而并不会渲染那些发生崩溃的子组件树。**错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误**
**错误边界就是让这块组件报错的影响降到最小,不要影响到其他组件或者全局的正常运行**
例如 A 组件报错了,我们可以在 A 组件内添加一小段的提示,并把错误控制在 A 组件内,不影响其他组件

- 我们要对容易出错的组件的父组件做手脚,而不是组件本身

我们在父组件中通过 getDerivedStateFromError 来配置**子组件**出错时的处理函数
### **编写生命周期函数 getDerivedStateFromError**

1. 静态函数
2. 运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
3. **注意:只有子组件发生错误,才会运行该函数**
4. 该函数返回一个对象,React会将该对象的属性覆盖掉当前组件的state(必须返回 **null** 或者**状态对象**(State Obect))
5. 参数:错误对象
6. 通常,该函数用于改变状态

state={
hasError:false,
}

static getDerivedStateFromError(error) {
console.log(error);
return { hasError: error }
}

我们可以将 hasError 配置到状态当中,当 hasError 状态改变成 error 时,表明有错误发生,我们需要在组件中通过判断 hasError 值,来指定是否显示子组件
{this.state.hasError ? <h2>Child出错啦</h2> : <Child />}
但是我们会发现这个效果过了几秒之后自动又出现报错页面了,那是因为**开发环境还是会报错**,**生产环境不会报错** 直接显示 要显示的文字,白话一些就是这个适用于生产环境,为了生产环境不报错。开发中我们可以将Child出错啦这种错误提示换成一个错误组件。
### 3.2 综合案例
按照React官方的约定,一个类组件定义了**static getDerivedStateFromError()** 或**componentDidCatch()** 这两个生命周期函数中的任意一个(或两个),即可被称作ErrorBoundary组件,实现错误边界的功能。
其中,getDerivedStateFromError方法被约定为渲染备用UI,componentDidCatch方法被约定为捕获打印错误信息。
### **编写生命周期函数 componentDidCatch**

1. 实例方法
2. 运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
3. 通常,该函数用于记录错误消息

export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
Error: null,
ErrorInfo: null
};
}
//控制渲染降级UI
static getDerivedStateFromError(error,info) {
return {hasError: error};
}
//捕获抛出异常
componentDidCatch(error, errorInfo) {
// 1、错误信息(error)
// 2、错误堆栈(errorInfo)
//传递异常信息
this.setState((preState) =>
({hasError: preState.hasError, Error: error, ErrorInfo: errorInfo})
);
//可以将异常信息抛出给日志系统等等
//do something…
}
render() {
//如果捕获到异常,渲染降级UI
if (this.state.hasError) {
return


{Error:${this.state.Error?.message}}


{this.state.ErrorInfo?.componentStack}
;
}
return this.props.children;
}
}

虽然函数式组件无法定义 Error Boundary,但 Error Boundary 可以捕获函数式组件的异常错误
实现ErrorBoundary组件后,我们只需要将其当作常规组件使用,将其需要捕获的组件放入其中即可。
使用方式如下:

//main.js
import ReactDOM from ‘react-dom/client’;
import {ErrorBoundary} from ‘./ErrorBoundary.jsx’;
ReactDOM.createRoot(document.getElementById(‘root’)).render(



);

//app.js
import React from ‘react’;
function App() {
const [count, setCount] = useState(0);
if (count>0){
throw new Error(‘count>0!’);
}
return (


<button onClick={() => setCount((count) => count + 1)}>
count is {count}


);
}
export default App;

点击按钮后即可展示抛出异常时,应该渲染的降级UI:
![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1683249842665-92105795-8a48-42b6-a50b-28f347a2619d.png#averageHue=%23fafafa&clientId=ue74b1059-5eee-4&from=paste&id=uf06745d4&originHeight=226&originWidth=828&originalType=url&ratio=2&rotation=0&showTitle=false&size=116533&status=done&style=none&taskId=u0ef50d6d-c294-4ba1-8b61-8dcc93fa19b&title=)
### 3.3 让子组件不影响父组件正常显示案例
假设B组件(子组件)的出错:users不是一个数组,却是一个字符串。此时,会触发调用getDerivedStateFromError,并返回状态数据{hasError:error}。A组件(父组件)将根据hasError值判断是渲染备用的错误页面还是B组件。

import React, { Component } from ‘react’

export default class A extends Component {
state = { hasError: ‘’ }
static getDerivedStateFromError(error) {
return {
hasError: error,
}
}

componentDidCatch(error, info) {
console.log(‘error:’, error)
console.log(‘info:’, info)
console.log(‘用于统计错误信息并反馈给后台,将通知开发人员进行bug修复’)
}

render() {
const { hasError } = this.state
return (


我是组件A

{hasError ? ‘当前网络不稳定,请稍候再试!’ : }

)
}
}

class B extends Component {
state = {
users: ‘’,
}
render() {
const { users } = this.state
return (


我是组件B

{users.map(userObj => (

  • {userObj.name},{userObj.age}

  • ))}

)
}
}

![image.png](https://img-blog.csdnimg.cn/img_convert/be2671b2e2fe9f15fee73f0158bd4176.png#averageHue=#4e513f&clientId=ue74b1059-5eee-4&from=paste&id=u8d7aad89&originHeight=887&originWidth=1920&originalType=url&ratio=2&rotation=0&showTitle=false&size=674674&status=done&style=none&taskId=u3fc150dc-3c61-4485-a227-27852ec9c63&title=)
### 3.4 使用错误边界需要注意什么
没有什么技术栈或者技术思维是银弹,错误边界看起来用得很爽,但是需要注意以下几点:

- 错误边界实际上是用来捕获render阶段时抛出的异常,而React事件处理器中的错误并不会渲染过程中被触发,所以**错误边界捕获不到事件处理器中的错误**。
- React官方推荐使用try/catch来自行处理事件处理器中的异常。
- 错误边界无法捕获异步代码中的错误(例如 setTimeout或 requestAnimationFrame回调函数),这两个函数中的代码通常不在当前任务队列内执行。
- 目前错误边界只能在类组件中实现,也只能捕获**其子组件树**的错误信息。错误边界无法捕获自身的错误,如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,类似于JavaScript中的cantch的工作机制。
- 错误边界无法在服务端渲染中生效,因为根本的渲染方法已经ReactDOM.createRoot().render()修改为了ReactDOM.hydrateRoot(), 而上面也提到了,错误边界捕获的是render阶段时抛出的异常。

**总结:仅处理渲染子组件期间的同步错误**
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿泽不会飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值