「React 深入」一文吃透React v18全部Api(1.3w+)

大家好,我是小杜杜,俗话说的好,工欲善其事必先利其器,什么意思呢?就是说你想玩转React就必须知道React有什么,无论是否运用到,首先都要知道,提升思维广度~

其实React官方提供了很多Api,只是这些Api我们并不常用,所以往往会忽略它们,但在一些特定的场景下,这些Api也会起到关键性的作用,所以今天就逐个盘点一下,说说它们的使用方法和使用场景。

当然这些Api并不需要全部掌握,只需要知道有这个知识点就好了~

本文将会全面总结所有的ReactApi,包含组件类工具类生命周期react-hooksreact-dom五大模块,并配带示例,帮助大家更好的掌握,大家可以边嗑瓜子边阅读,如有不全、不对的地方欢迎在评论区指出~

由于本文过长,建议点赞 +收藏, 在正式开始前,我抽取了一些问题,一起来看看:

  • 1.React v18中对react-dom做了那些改动,增加了那些新的hooks?
  • 2.useRef除了获取元素的节点信息,还能做什么?
  • 3.为什么会有Children.map?它与不同的遍历有和不同
  • 4.类组件的生命周期在不同的版本是怎样变化的?
  • 5.子元素如何渲染到父元素上面的?

其实问题很多,看完这篇文章后,相信一定能帮你解答的非常清楚,还请各位小伙伴多多支持一下

前言

在之前的几篇文章中,有自定义hooksHOC虚拟DOMdiff算法,多多少少都有React官方Api做为基础条件,我的这个专栏的目的就是对React的深入,就是希望对React有一个全面的提升

写这篇文章的主要目的有:

  • 提升知识广度,要想深入React就必须全面了解React,首先要学会用,要知道,如果连知道都不知道,谈何深入?
  • React v18react-dom的改动还是比较大的,并且新增了五个hooks,逐一盘点一下,看看做了那些改动
  • 这个专栏实际上是循序渐进的,相互之间都有一定的关联,同时要想看懂,也需要有一定的React基础,对刚开始学习React的小伙伴可能并不是太友好,所以特地总结一番,用最简单的示例,帮你掌握这些Api
  • 对之后的源码有帮助,所以本篇文章将会全面解读React Api的使用方法,和场景,如有不足,还希望在评论区指出~

附上一张今天的学习图谱~

「React 深入」一文吃透React v18全部Api(1.3w+)

组件类

Component

在React中提供两种形式,一种是类组件,另一种是函数式组件,而在类组件组件中需要使用Component继承,这个组件没有什么好讲的,我们可以看看源码:

文件位置packages/react/src/ReactBaseClasses.js

「React 深入」一文吃透React v18全部Api(1.3w+)

可以看出Component进行一些初始化的工作,updater保存着更新组件的方法

PureComponent

PureComponent:会对propsstate进行浅比较,跳过不必要的更新,提高组件性能。

可以说PureComponentComponent基本完全一致,但PureComponent会浅比较,也就是较少render渲染的次数,所以PureComponent一般用于性能优化

那么什么是浅比较,举个:

import { PureComponent } from 'react';
import { Button } from 'antd-mobile';
class Index extends PureComponent{
  constructor(props){
      super(props)
      this.state={
         data:{
            number:0
         }
      }
  }
  render(){
      const { data } = this.state
      return <div style={{padding: 20}}>
          <div> 数字: { data.number  }</div>
          <Button
            color="primary" 
            onClick={() => {
                const { data } = this.state
                data.number++
                this.setState({ data })
            }}
          >数字加1</Button>
      </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

可以发现,当我们点击按钮的时候,数字并没有刷新,这是因为PureComponent会比较两次的data对象,它会认为这种写法并没有改变原先的data,所以不会改变

我们只需要:

this.setState({ data: {...data} })

这样就可以解决这个问题了

与shouldComponentUpdate的关系如何

在生命周期中有一个shouldComponentUpdate()函数,那么它能改变PureComponent吗?

其实是可以的,shouldComponentUpdate()如果被定义,就会对新旧 propsstate 进行 shallowEqual 比较,新旧一旦不一致,便会触发 update

也可以这么理解:PureComponent通过自带的propsstate的浅比较实现了shouldComponentUpdate(),这点Component并不具备

PureComponent可能会因深层的数据不一致而产生错误的否定判断,从而导致shouldComponentUpdate结果返回false,界面得不到更新,要谨慎使用

memo

memo:结合了pureComponent纯组件componentShouldUpdate功能,会对传入的props进行一次对比,然后根据第二个函数返回值来进一步判断哪些props需要更新

要注意memo是一个高阶组件函数式组件类组件都可以使用。

memo接收两个参数:

  • 第一个参数:组件本身,也就是要优化的组件
  • 第二个参数:(pre, next) => boolean, pre:之前的数据, next:现在的数据,返回一个布尔值,若为 true则不更新,为false更新

性能优化

接下来,我们先看这样一个:

import React, { Component } from 'react';
import { Button } from 'antd-mobile';
const Child = () => {
    return <div>
        {console.log('子组件渲染')}
        大家好,我是小杜杜~
    </div>
}
class Index extends Component{
  constructor(props){
      super(props)
      this.state={
        flag: true
      }
  }
  render(){
      const { flag } = this.state
      return <div style={{padding: 20}}>
          <Child/>
          <Button
            color="primary" 
            onClick={() => this.setState({ flag: !flag })}
          >状态切换{JSON.stringify(flag)}</Button>
      </div>
  }
}
export default Index;

在上述代码中,我们设置一个子组件,也就是Child和一个按钮,按钮的效果是切换 flag 的状态,可以看出flagChild之间没有任何关系,那么在切换状态的时候,Child会刷新吗?

直接看看效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

可以看出,在我们切换状态的时候,Child实际上也会刷新,我们肯定不希望组件做无关的刷新,那么我们加上memo来看看的效果:

const HOCChild = memo(Child, (pre, next) => {
    return true
})

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)
可以看出,加上memo后,Child不会再做无关的渲染,从而达到性能优化的作用

第二个参数的作用

栗子:

import React, { Component, memo } from 'react';
import { Button } from 'antd-mobile';
const Child = ({ number }) => {
    return <div>
        {console.log('子组件渲染')}
        大家好,我是小杜杜~
        <p>传递的数字:{number}</p>
    </div>
}
const HOCChild = memo(Child, (pre, next) => {
    if(pre.number === next.number) return true
    if(next.number < 7) return false
    return true
})
class Index extends Component{
  constructor(props){
      super(props)
      this.state={
        flag: true,
        number: 1
      }
  }
  render(){
      const { flag, number } = this.state
      return <div style={{padding: 20}}>
          <HOCChild number={number} />
          <Button
            color="primary" 
            onClick={() => this.setState({ flag: !flag})}
          >状态切换{JSON.stringify(flag)}</Button>
        <Button
            color="primary"
            style={{marginLeft: 8}} 
            onClick={() => this.setState({ number: number + 1})}
        >数字加一:{number}</Button>
      </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

当数字小于7,才会出发Child的更新,通过返回的布尔值来控制

memo的注意事项

React.memoPureComponent的区别:

  • 服务对象不同:PureComponent 服务与类组件,React.memo既可以服务于类组件,也可以服务与函数式组件,useMemo服务于函数式组件(后续讲到)
  • 针对的对象不同:PureComponent针对的是propsstateReact.memo只能针对props来决定是否渲染

这里还有个小的注意点:memo的第二个参数的返回值与shouldComponentUpdate的返回值是相反的,经常会弄混,还要多多注意

  • memo:返回true组件不渲染 , 返回false组件重新渲染。
  • shouldComponentUpdate: 返回true组件渲染 , 返回false组件不渲染。

forwardRef

forwardRef:引用传递,是一种通过组件向子组件自动传递引用ref的技术。对于应用者的大多数组件来说没什么作用,但对于一些重复使用的组件,可能有用。

听完介绍是不是感觉云里雾里的,官方对forwardRef的介绍也很少,我们来看看转发的问题

React中,React不允许ref通过props传递,因为ref是组件中固定存在的,在组件调和的过程中,会被特殊处理,而forwardRef就是为了解决这件事而诞生的,让ref可以通过props传递

举个栗子:父组件想要获取孙组件上的信息,我们直接用ref传递会怎样:

「React 深入」一文吃透React v18全部Api(1.3w+)

接下来看看利用forwardRef来转发下ref,就可以解决这个问题了:

import React, { Component, forwardRef } from 'react';
const Son = ({sonRef}) => {
    return <div>
        <p>孙组件</p>
        <p ref={sonRef}>大家好,我是小杜杜~</p>
    </div>
}
const Child = ({ childRef }) => {
    return <div>
       <div>子组件</div>
        <Son sonRef={childRef} />
    </div>
}
const ForwardChild = forwardRef((props, ref) => <Child childRef={ref} {...props} />)
class Index extends Component{
  constructor(props){
    super(props)
  }
  node = null
  componentDidMount(){
      console.log(this.node)
  }
  render(){
    return <div style={{padding: 20}}>
        <div>父组件</div>
        <ForwardChild ref={(node) => this.node = node} />
    </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

如此以来就解决了不能在react组件中传递ref的问题,至于复用的组件可能会用到,目前也没思路用forwardRef干嘛,就当熟悉吧~

Fragment

React中,组件是不允许返回多个节点的,如:

    return <p>我是小杜杜</p>
           <p>React</p>
           <p>Vue</p>

我们想要解决这种情况需要给为此套一个容器元素,如<div></div>

    return <div>
       <p>我是小杜杜</p>
       <p>React</p>
       <p>Vue</p>
    </div>

但这样做,无疑会多增加一个节点,所以在16.0后,官方推出了Fragment碎片概念,能够让一个组件返回多个元素,React.Fragment 等价于<></>

    return <React.Fragment>
       <p>我是小杜杜</p>
       <p>React</p>
       <p>Vue</p>
    </React.Fragment>

可以看到React.Fragment实际上是没有节点的

「React 深入」一文吃透React v18全部Api(1.3w+)

另外,react中支持数组的返回,像这样:

    return [
        <p key='1'>我是小杜杜</p>,
       <p key='2'>React</p>,
       <p key='3'>Vue</p>
    ]

还有我们在进行数组遍历的时候,React都会在底层处理,在外部嵌套一个<React.Fragment />

Fragment 与 <></>的不同

我们都知道<></><Fragment></Fragment>的简写,从原则上来说是一致的,那么你知道他们又什么不同吗?

实际上,Fragment 这个组件可以赋值 key,也就是索引,<></>不能赋值,应用在遍历数组上,有感兴趣的同学可以试一试~

lazy

lazy:允许你定义一个动态加载组件,这样有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件,也就是懒加载组件(高阶组件)

lazy接收一个函数,这个函数需要动态调用import(),如:

const LazyChild = lazy(() => import('./child'));

那么import('./child')是一个怎样的类型呢?

实际上lazy必须接受一个函数,并且需要返回一个Promise, 并且需要resolve一个default一个React组件,除此之外,lazy必须要配合Suspense一起使用

举个例子:我加入了setTimeout方便看到更好的效果:

import React, { Component, Suspense, lazy } from 'react';
import Child from './child'
import { Button, DotLoading } from 'antd-mobile';
const LazyChild = lazy(() => new Promise((res) => {
    setTimeout(() => {
        res({
            default: () => <Child />
        })
    }, 1000)
}))
class Index extends Component{
  constructor(props){
    super(props)
    this.state={
        show: false
    }
  }
  render(){
    const { show } = this.state
    return <div style={{padding: 20}}>
        <Button color='primary' onClick={() => this.setState({ show: true })} >
            渲染
        </Button>
        {
            show && <Suspense fallback={<div><DotLoading color='primary' />加载中</div>}>
            <LazyChild />
        </Suspense>
        }
    </div>
  }
}
export default Index;

Child 文件:

import React, { useEffect } from 'react';
import img from './img.jpeg'
const Index = () => {
  useEffect(() => {
    console.log('照片渲染')
  }, [])
  return <div>
  <img src={img} width={200} height={160} />
</div>
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

Suspense

Suspense:让组件”等待”某个异步组件操作,直到该异步操作结束即可渲染。

与上面lazy中的案例一样,两者需要配合使用,其中fallback为等待时渲染的样式

Suspenselazy可以用于等待照片、脚本和一些异步的情况。

Profiler

Profiler:这个组件用于性能检测,可以检测一次react组件渲染时的性能开销

此组件有两个参数:

  • id:标识Profiler的唯一性
  • onRender:回调函数,用于渲染完成,参数在下面讲解

举个栗子:

import React, { Component, Profiler } from 'react';
export default Index;

让我们来看看打印的是什么:

「React 深入」一文吃透React v18全部Api(1.3w+)

依此的含义:

  • idProfiler树的id
  • phasemount挂载,update渲染
  • actualDuration:更新committed花费的渲染时间
  • baseDuration:渲染整颗子树需要的时间
  • startTime:更新开始渲染的时间
  • commitTime:更新 committed 的时间
  • interactions:本次更新的interactions的集合

需要注意的是,这个组件应该在需要的时候去使用,虽然Profiler是一个轻量级的,但也会带来负担

StrictMode

StrictMode:严格模式,是一种用于突出显示应用程序中潜在问题的工具

Fragment一样,StrictMode也不会出现在UI层面,只是会检查和警告

可以看一下官方的示例:

import React from 'react';
function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>      <Footer />
    </div>
  );
}

上述代码中只会对ComponentOneComponentTwo进行检查

主要有以下帮助:

  • 识别具有不安全生命周期的组件
  • 关于旧版字符串引用 API 使用的警告
  • 关于不推荐使用 findDOMNode 的警告
  • 检测意外的副作用
  • 检测遗留上下文 API
  • 确保可重用状态

工具类

crateElement

在之前讲的虚拟DOM的时候已经详细的讲解过,我们在这里在在复习一遍

JSX会被编译为React.createElement的形式,然后被babel编译

结构:

React.createElement(type, [props], [...children])共有三个参数:

  • type:原生组件的话是标签的字符串,如“div”,如果是React自定义组件,则会传入组件
  • [props]:对象,dom类中的属性组件中的props
  • [...children]:其他的参数,会依此排序

举个例子: 举个栗子:

    class Info extends React.Component {
        render(){
            return(
                <div>
                    Hi!我是小杜杜
                    <p>欢迎</p>
                    <Children>我是子组件</Children>
                </div>
            )
        }
    }

上述代码会被翻译为:

    class Info extends React.Component {
        render(){
            return React.createElement(
                'div', 
                null, 
                "Hi!我是小杜杜",
                React.createElement('p', null, '欢迎'), // 原生标签
                React.createElement( 
                    Children, //自定义组件
                    null, // 属性
                    '我是子组件'  //child文本内容
                )
            )
        }
    }

注意点

  • JSX的结构实际上和React.createElement写法一致,只是用JSX更加简单、方便
  • 经过React.createElement的包裹,最终会形成$$typeof = Symbol(react.element)对象,对象保存了react.element的信息。

cloneElement

cloneElement:克隆并返回一个新的React元素,

结构: React.createElement(type, [props], [...children])

React.cloneElement()几乎等同于:

<element.type {...element.props} {...props}>
    {children}
</element.type>

举个例子:

import React from 'react';
const Child = () => {
  const children = React.cloneElement(<div>大家好,我是小杜杜</div>, {name: '小杜杜'})
  console.log(children)
  return <div>{children}</div>
}
const Index = () => {
  return <div style={{padding: 20}}>
    <Child />
  </div>
}
export default Index;

打印下children来看看:

「React 深入」一文吃透React v18全部Api(1.3w+)

其实是可以看到传递的name的,也就是说可以通过React.cloneElement方法去对组件进行一些赋能

creatContent

creatContent:相信大家对这个Api很熟悉,用于传递上下文。creatContent会创建一个Context对象,用Providervalue来传递值,用Consumer接受value

我们实现一个父传孙的小栗子:

import React, { useState } from 'react';
const Content = React.createContext()
const Child = () => {
  return <Content.Consumer>
    {(value) => <Son {...value} />}
  </Content.Consumer>
}
const Son = (props) => {
  return <>
    <div>大家好,我是{props.name}</div>
    <div>幸运数字是:{props.number}</div>
  </>
}
const Index = () => {
  const [data, _] = useState({
    name: '小杜杜',
    number: 7
  })
  return <div style={{padding: 20}}>
    <Content.Provider value={data}>
      <Child />
    </Content.Provider>
  </div>
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

注意:如果Consumer上一级一直没有Provider,则会应用defaultValue作为value

只有当组件所处的树中没有匹配到Provider时,其defaultValue参数才会生效。

Children

Children: 提供处理this.props.children不透明数据结构的实用程序.

那么什么是不透明的数据呢?

先来看看下面的栗子:

import React, { useEffect } from 'react';
const Child = ({children}) => {
  console.log(children)
  return children
}
const Index = () => {
  return <div style={{padding: 20}}>
    <Child>
      <p>大家好,我是小杜杜</p>
      <p>大家好,我是小杜杜</p>
      <p>大家好,我是小杜杜</p>
      <p>Hello~</p>
    </Child>
  </div>
}
export default Index;

打印下children看到:

「React 深入」一文吃透React v18全部Api(1.3w+)

我们可以看到每个节点都打印出来了,这种情况属于透明的,但我们要是便利看看:

<Child>
      {
        [1,2,3].map((item) => <p key={item}>大家好,我是小杜杜</p>)
      }
  <p>Hello~</p>
</Child>

「React 深入」一文吃透React v18全部Api(1.3w+)

却发现我们便利的三个元素被包了一层,像这种数据被称为不透明,我们想要处理这种数据,就要以来React.Chilren 来解决

Children.map

Children.map:遍历,并返回一个数组,针对上面的情况,我们可以通过这个方法将数据便会原先的

const Child = ({children}) => {
  const res = React.Children.map(children, (item) => item)
  console.log(res)
  return res
}

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

Children.forEach

Children.forEach:与Children.map类似,不同的是Children.forEach并不会返回值,而是停留在遍历阶段

const Child = ({children}) => {
  React.Children.forEach(children, (item) => console.log(item))
  return children
}

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

Children.count

Children.count:返回Child内的总个数,等于回调传递给mapforEach将被调用的次数。如:

const Child = ({children}) => {
  const res =  React.Children.count(children)
  console.log(res) // 4
  return children
}

Children.only

Children.only:验证Child是否只有一个元素,如果是,则正常返回,如果不是,则会报错。‍♂不知道这个有啥用~

const Child = ({children}) => {
  console.log(React.Children.only(children))
  return children
}

效果: 只有一个时:

「React 深入」一文吃透React v18全部Api(1.3w+)

多个时:

「React 深入」一文吃透React v18全部Api(1.3w+)

Children.toArray

Children.toArray:以平面数组的形式返回children不透明数据结构,每个子元素都分配有键。

如果你想在你的渲染方法中操作子元素的集合,特别是如果你想this.props.children在传递它之前重新排序或切片,这很有用。

我们在原先的例子上在加一次来看看:

import React from 'react';
const Child = ({children}) => {
  console.log(`原来数据:`, children)
  const res = React.Children.toArray(children)
  console.log(`扁平后的数据:`, res)
  return res
}
const Index = () => {
  return <div style={{padding: 20}}>
    <Child>
      {
        [1,2,3].map((item) => [5, 6].map((ele) => <p key={`${item}-${ele}`}>大家好,我是小杜杜</p>))
      }
      <p>Hello~</p>
    </Child>
  </div>
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

这里需要注意的是key,经过Children.toArray处理后,会给原本的key添加前缀,以使得每个元素key的范围都限定在此函数入参数组的对象内。

createRef

createRef:创建一个ref对象,获取节点信息,直接举个例子:

import React, { Component } from 'react';
class Index extends Component{
  constructor(props){
      super(props)
  }
  node = React.createRef()
  componentDidMount(){
    console.log(this.node)
  }
  render(){
    return <div ref={this.node} > 节点信息 </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

这个有点鸡肋,因为我们可以直接从ref上获取到值,没有必要通过createRef去获取,像这样

import React, { Component } from 'react';
class Index extends Component{
  constructor(props){
      super(props)
  }
  node = null
  componentDidMount(){
    console.log(this.node)
  }
  render(){
    return <div ref={(node) => this.node = node} > 节点信息 </div>
  }
}
export default Index;

createFactory

createFactory:返回一个生成给定类型的 React 元素的函数。

接受一个参数type,这个typecreateElementtype一样,原生组件的话是标签的字符串,如“div”,如果是React自定义组件,则会传入组件

效果与createElement一样,但这个说是遗留的,官方建议直接使用createElement,并且在使用上也会给出警告

栗子:

import React, { useEffect } from 'react';
const Child = React.createFactory(()=><div>createFactory</div>) 
const Index = () => {
  return <div style={{padding: 20}}>
    大家好,我是小杜杜
    <Child />
  </div>
}
export default Index;

「React 深入」一文吃透React v18全部Api(1.3w+)

isValidElement

isValidElement:用于验证是否是React元素,是的话就返回true,否则返回false,感觉这个Api也不是特别有用,因为我们肯定知道是否是

栗子:

    console.log(React.isValidElement(<div>大家好,我是小杜杜</div>)) // true
    console.log(React.isValidElement('大家好,我是小杜杜')) //false

version

查看React的版本号:如:

console.log(React.version)

版本:

「React 深入」一文吃透React v18全部Api(1.3w+)

我们可以看下在React中的文件位置,在react中有一个单独处理版本信息的位置:

packages/shared/ReactVersion.js

「React 深入」一文吃透React v18全部Api(1.3w+)

生命周期

React 的 生命周期主要有两个比较大的版本,分别是v16.0前v16.4两个版本的生命周期,我们分别说下旧的和新的生命周期,做下对比~

v16.0前

「React 深入」一文吃透React v18全部Api(1.3w+)

从图中,总共分为四大阶段:Intialization(初始化)Mounting(挂载)Update(更新)Unmounting(卸载)

Intialization(初始化)

在初始化阶段,我们会用到 constructor() 这个构造函数,如:

constructor(props) {
  super(props);
}
  • super的作用“ 用来调用基类的构造方法( constructor() ), 也将父组件的props注入给子组件,供子组件读取 (组件中props只读不可变``,state可变)
  • 初始化操作,定义this.state的初始内容
  • 只会执行一次

Mounting(挂载)

componentWillMount:在组件挂载到DOM前调用

  • 这里面的调用的this.setState不会引起组件的重新渲染,也可以把写在这边的内容提到constructor(),所以在项目中很少。
  • 只会调用一次

render: 渲染

  • 只要propsstate发生改变(无两者的重传递和重赋值,论值是否有变化,都可以引起组件重新render),都会重新渲染render。
  • return:是必须的,是一个React元素(UI,描述组件),不负责组件实际渲染工作,由React自身根据此元素去渲染出DOM。
  • render纯函数(Pure function:返回的结果只依赖与参数,执行过程中没有副作用),不能执行this.setState。

componentDidMount:组件挂载到DOM后调用

  • 调用一次

Update(更新)

componentWillReceiveProps(nextProps):调用于props引起的组件更新过程中

  • nextProps:父组件传给当前组件新的props
  • 可以用nextPropsthis.props来查明重传props是否发生改变(原因:不能保证父组件重传的props有变化)
  • 只要props发生变化就会,引起调用

shouldComponentUpdate(nextProps, nextState):性能优化组件

  • nextProps:当前组件的this.props

  • nextState:当前组件的this.state

  • 通过比较nextPropsnextState,来判断当前组件是否有必要继续执行更新过程。

    返回false:表示停止更新,用于减少组件的不必要渲染,优化性能

    返回true:继续执行更新

  • componentWillReceiveProps()中执行了this.setState,更新了state,但在render前(如shouldComponentUpdate,componentWillUpdate),this.state依然指向更新前的state,不然nextState及当前组件的this.state的对比就一直是true了

componentWillUpdate(nextProps, nextState):组件更新前调用

  • 在render方法前执行
  • 由于组件更新就会调用,所以一般很少使用
  • render:重新渲染

componentDidUpdate(prevProps, prevState):组件更新后被调用

  • prevProps:组件更新前的props
  • prevState:组件更新前的state
  • 可以操作组件更新的DOM

Unmounting(卸载)

componentWillUnmount:组件被卸载前调用

  • 可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏

React v16.4

「React 深入」一文吃透React v18全部Api(1.3w+)

与 v16.0的生命周期相比

新增了 getDerivedStateFromPropsgetSnapshotBeforeUpdate

取消了 componentWillMountcomponentWillReceivePropscomponentWillUpdate

getDerivedStateFromProps

getDerivedStateFromProps(prevProps, prevState):组件创建和更新时调用的方法

  • prevProps:组件更新前的props
  • prevState:组件更新前的state

注意:在React v16.3中,在创建和更新时,只能是由父组件引发才会调用这个函数,在React v16.4改为无论是Mounting还是Updating,也无论是什么引起的Updating,全部都会调用。

有点类似于componentWillReceiveProps,不同的是getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,当然也不推荐使用

如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾。

在组件创建时和更新时的render方法之前调用,它应该返回一个对象来更新状态,或者返回null来不更新任何内容。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps,prevState):Updating时的函数,在render之后调用

  • prevProps:组件更新前的props
  • prevState:组件更新前的state

可以读取,但无法使用DOM的时候,在组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)

返回的任何指都将作为参数传递给componentDidUpdate()

注意

在17.0的版本,官方彻底废除 componentWillMountcomponentWillReceivePropscomponentWillUpdate

如果还想使用的话可以使用:UNSAFE_componentWillMount()UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate()

对了,如果在面试的时候可能会问道有关生命周期的问题,建议各位小伙伴,将以上的生命周期都可说一说,然后做个对比,这样的话,效果肯定不错~

react-hooks

react-hooksReact 16.8的产物,给函数式组件赋上了生命周期,再经过三年多的时间,函数式组件已经逐渐取代了类组件,可以说是React开发者必备的技术

同时在React v18中又出现了一些hooks,今天我们将一起详细的看看,确保你能迅速掌握~

React v16.8中的hooks

useState

useState:定义变量,可以理解为他是类组件中的this.state

使用:

const [state, setState] = useState(initialState);
  • state:目的是提供给 UI,作为渲染视图的数据源
  • setState:改变 state 的函数,可以理解为this.setState
  • initialState:初始默认值

在这里我介绍两种写法,直接看栗子:

import React, { useState } from 'react';
import { Button } from 'antd-mobile';
const Index = () => {
  const [ number, setNumber ] = useState(0)
  return <div style={{padding: 20}}>
    <div>数字:{number}</div>
    <Button
      color='primary'
      onClick={() => {
        setNumber(number + 1) //第一种
      }}
    >
      点击加1
    </Button>
    <Button
      color='primary'
      style={{marginLeft: 8}}
      onClick={() => {
        setNumber((value) => value + 2) //第二种
      }}
    >
      点击加2
    </Button>
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

注意点

useState有点类似于PureComponent,会进行一个比较浅的比较,如果是对象的时候直接传入并不会更新,这点一定要切记,如:

import React, { useState } from 'react';
import { Button } from 'antd-mobile';
const Index = () => {
  const [ state, setState ] = useState({number: 0})
  return <div style={{padding: 20}}>
    <div>数字:{state.number}</div>
    <Button
      color='primary'
      onClick={() => {
        state.number++
        setState(state)
      }}
    >
      点击
    </Button>
  </div>
}
export default Index

「React 深入」一文吃透React v18全部Api(1.3w+)

useEffect

useEffect:副作用,你可以理解为是类组件的生命周期,也是我们最常用的钩子

那么什么是副作用呢? 副作用(Side Effect:是指 function 做了和本身运算返回值无关的事,如请求数据、修改全局变量,打印、数据获取、设置订阅以及手动更改 React 组件中的 DOM 都属于副作用操作都算是副作用

我们直接演示下它的用法栗子:

不断执行

useEffect不设立第二个参数时,无论什么情况,都会执行

模拟初始化和卸载

我们可以利用useEffect挂载卸载阶段,通常我们用于监听addEventListenerremoveEventListener的使用

import React, { useState, useEffect } from 'react';
import { Button } from 'antd-mobile';
const Child = () => {
  useEffect(() => {
    console.log('挂载')
    return () => {
      console.log('卸载')
    }
  }, [])
  return <div>大家好,我是小杜杜</div>
}
const Index = () => {
  const [ flag, setFlag ] = useState(false)
  return <div style={{padding: 20}}>
    <Button
      color='primary'
      onClick={() => {
        setFlag(v => !v)
      }}
    >
     {flag ? '卸载' : '挂载'}
    </Button>
    {flag && <Child />}
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

根据依赖值改变

我们可以设置useEffect的第二个值来改变

import React, { useState, useEffect } from 'react';
import { Button } from 'antd-mobile';
const Index = () => {
  const [ number, setNumber ] = useState(0)
  const [ count, setCount ] = useState(0)
  useEffect(() => {
    console.log('count改变才会执行')
  }, [count])
  return <div style={{padding: 20}}>
    <div>number: {number}   count: {count}</div>
    <Button
      color='primary'
      onClick={() => setNumber(v => v + 1)}
    >
      number点击加1
    </Button>
    <Button
      color='primary'
      style={{marginLeft: 8}}
      onClick={() => setCount(v => v + 1)}
    >
      count点击加1
    </Button>
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useContent

useContent:上下文,类似于Context:其本意就是设置全局共享数据,使所有组件可跨层级实现共享

useContent的参数一般是由createContext的创建,通过 CountContext.Provider 包裹的组件,才能通过 useContext 获取对应的值

举个例子:

import React, { useState, createContext, useContext } from 'react';
import { Button } from 'antd-mobile';
const CountContext = createContext(-1)
const Child = () => {
  const count = useContext(CountContext)
  return <div style={{marginTop: 20}}>
    子组件获取到的count: {count}
    <Son />
  </div>
}
const Son = () => {
  const count = useContext(CountContext)
  return <div style={{marginTop: 20}}>
    孙组件获取到的count: {count}
  </div>
}
const Index = () => {
  const [ count, setCount ] = useState(0)
  return <div style={{padding: 20}}>
    <div>父组件:{count}</div>
    <Button
      color='primary'
      onClick={() => setCount(v => v + 1)}
    >
      点击加1
    </Button>
    <CountContext.Provider value={count}>
      <Child />
    </CountContext.Provider>
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useReducer

useReducer:它类似于redux功能的api

结构:

const [state, dispatch] = useReducer(reducer, initialArg, init);
  • state:更新后的state
  • dispatch:可以理解为和useStatesetState一样的效果
  • reducer:可以理解为reduxreducer
  • initialArg:初始值
  • init:惰性初始化

直接来看看栗子:

import React, { useReducer } from 'react';
import { Button } from 'antd-mobile';
const Index = () => {
  const [count, dispatch] = useReducer((state, action)=> {
    switch(action?.type){
      case 'add':
        return state + action?.payload;
      case 'sub':
        return state - action?.payload;
      default:
        return state;
    }
  }, 0);
  return <div style={{padding: 20}}>
    <div>count:{count}</div>
    <Button
      color='primary'
      onClick={() => dispatch({type: 'add', payload: 1})}
    >
      加1
    </Button>
    <Button
      color='primary'
      style={{marginLeft: 8}}
      onClick={() => dispatch({type: 'sub', payload: 1})}
    >
      减1
    </Button>
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useMemo

useMemo:与memo的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行callback函数,而useMemo的第二个参数是一个数组,通过这个数组来判定是否更新回掉函数

当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。

简单的理解下,当一个页面内容非常复杂,模块非常多的时候,函数式组件会从头更新到尾,只要一处改变,所有的模块都会进行刷新,这种情况显然是没有必要的。

我们理想的状态是各个模块只进行自己的更新,不要相互去影响,那么此时用useMemo是最佳的解决方案。

这里要尤其注意一点,只要父组件的状态更新,无论有没有对自组件进行操作,子组件都会进行更新useMemo就是为了防止这点而出现的

为了更好的理解useMemo,我们来看下面一个小栗子:

// usePow.ts
const Index = (list: number[]) => {
  return list.map((item:number) => {
    console.log(1)
    return Math.pow(item, 2)
  })
}
export default Index;
// index.tsx
import { Button } from 'antd-mobile';
import React,{ useState } from 'react';
import { usePow } from '@/components';
const Index:React.FC<any> = (props)=> {
  const [flag, setFlag] = useState<boolean>(true)
  const data = usePow([1, 2, 3])
  return (
    <div>
      <div>数字:{JSON.stringify(data)}</div>
      <Button color='primary' onClick={() => {setFlag(v => !v)}}>切换</Button>
       <div>切换状态:{JSON.stringify(flag)}</div>
    </div>
  );
}
export default Index;

我们简单的写了个 usePow,我们通过 usePow 给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

我们发现了一个问题,为什么点击切换按钮也会触发console.log(1)呢?

这样明显增加了性能开销,我们的理想状态肯定不希望做无关的渲染,所以我们做自定义 hooks的时候一定要注意,需要减少性能开销,我们为组件加入 useMemo试试:

    import { useMemo } from 'react';
    const Index = (list: number[]) => {
      return useMemo(() => list.map((item:number) => {
        console.log(1)
        return Math.pow(item, 2)
      }), []) 
    }
    export default Index;

「React 深入」一文吃透React v18全部Api(1.3w+)

发现此时就已经解决了这个问题,不会在做相关的渲染了

useCallback

useCallbackuseMemo极其类似,可以说是一模一样,唯一不同的是useMemo返回的是函数运行的结果,而useCallback返回的是函数

注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,其次,这个组件必须配合memo,否则不但不会提升性能,还有可能降低性能

      import React, { useState, useCallback } from 'react';
      import { Button } from 'antd-mobile';
      const MockMemo: React.FC<any> = () => {
        const [count,setCount] = useState(0)
        const [show,setShow] = useState(true)
        const  add = useCallback(()=>{
          setCount(count + 1)
        },[count])
        return (
          <div>
            <div style={{display: 'flex', justifyContent: 'flex-start'}}>
              <TestButton title="普通点击" onClick={() => setCount(count + 1) }/>
              <TestButton title="useCallback点击" onClick={add}/>
            </div>
            <div style={{marginTop: 20}}>count: {count}</div>
            <Button onClick={() => {setShow(!show)}}> 切换</Button>
          </div>
        )
      }
      const TestButton = React.memo((props:any)=>{
        console.log(props.title)
        return <Button color='primary' onClick={props.onClick} style={props.title === 'useCallback点击' ? {
        marginLeft: 20
        } : undefined}>{props.title}</Button>
      })
      export default MockMemo;

「React 深入」一文吃透React v18全部Api(1.3w+)

我们可以看到,当点击切换按钮的时候,没有经过 useCallback封装的函数会再次刷新,而经过 useCallback包裹的函数不会被再次刷新

有很多小伙伴有个误区,就是useCallback不能单独使用,必须要配合memo吗?

其实是这样的,你可以单独使用useCallback,但只用useCallback起不到优化的作用,反而会增加性能消耗

想之前讲的,React.memo会通过浅比较里面的props,如果没有memo,那么使用的useCallback也就毫无意义

因为useCallback本身是需要开销的,所以反而会增加性能的消耗

useRef

useRef: 可以获取当前元素的所有属性,并且返回一个可变的ref对象,并且这个对象只有current属性,可设置initialValue

结构:

const refContainer = useRef(initialValue);

有许多小伙伴只知道useRef可以获取对应元素的属性,但useRef还具备一个功能,就是缓存数据,接下来一起看看:

通过useRef获取对应的属性值

栗子:

import React, { useState, useRef } from 'react';
const Index:React.FC<any> = () => {
  const scrollRef = useRef<any>(null);
  const [clientHeight, setClientHeight ] = useState<number>(0)
  const [scrollTop, setScrollTop ] = useState<number>(0)
  const [scrollHeight, setScrollHeight ] = useState<number>(0)
  const onScroll = () => {
    if(scrollRef?.current){
      let clientHeight = scrollRef?.current.clientHeight; //可视区域高度
      let scrollTop  = scrollRef?.current.scrollTop;  //滚动条滚动高度
      let scrollHeight = scrollRef?.current.scrollHeight; //滚动内容高度
      setClientHeight(clientHeight)
      setScrollTop(scrollTop)
      setScrollHeight(scrollHeight)
    }
  }
  return (
    <div >
      <div >
        <p>可视区域高度:{clientHeight}</p>
        <p>滚动条滚动高度:{scrollTop}</p>
        <p>滚动内容高度:{scrollHeight}</p>
      </div>
      <div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
        <div style={{height: 2000}}></div>
      </div>
    </div>
  );
};
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

从上述可知,我们可以通过useRef来获取对应元素的相关属性,以此来做一些操作

缓存数据

react-redux的源码中,在hooks推出后,react-redux用大量的useMemo重做了Provide等核心模块,其中就是运用useRef来缓存数据,并且所运用的 useRef() 没有一个是绑定在dom元素上的,都是做数据缓存用的

可以简单的来看一下:

    // 缓存数据
    /* react-redux 用userRef 来缓存 merge之后的 props */ 
    const lastChildProps = useRef() 
    // lastWrapperProps 用 useRef 来存放组件真正的 props信息 
    const lastWrapperProps = useRef(wrapperProps) 
    //是否储存props是否处于正在更新状态 
    const renderIsScheduled = useRef(false)
    //更新数据
    function captureWrapperProps( 
        lastWrapperProps, 
        lastChildProps, 
        renderIsScheduled, 
        wrapperProps, 
        actualChildProps, 
        childPropsFromStoreUpdate, 
        notifyNestedSubs 
    ) { 
        lastWrapperProps.current = wrapperProps 
        lastChildProps.current = actualChildProps 
        renderIsScheduled.current = false 
   }

我们看到 react-redux 用重新赋值的方法,改变了缓存的数据源,减少了不必要的更新,如过采取useState势必会重新渲染。

有的时候我们需要使用useMemouseCallbackApi,我们控制变量的值用useState 有可能会导致拿到的是旧值,并且如果他们更新会带来整个组件重新执行,这种情况下,我们使用useRef将会是一个非常不错的选择

useImperativeHandle

useImperativeHandle:可以让你在使用 ref 时自定义暴露给父组件的实例值

这个Api我觉得是十分有用的,建议掌握哦,来看看使用的场景:

在一个页面很复杂的时候,我们会将这个页面进行模块化,这样会分成很多个模块,有的时候我们需要在最外层的组件上控制其他组件的方法,希望最外层的点击事件,同时执行子组件的事件,这时就需要 useImperativeHandle 的帮助

结构:

useImperativeHandle(ref, createHandle, [deps])
  • refuseRef所创建的ref
  • createHandle:处理的函数,返回值作为暴露给父组件的 ref 对象。
  • deps:依赖项,依赖项更改形成新的 ref 对象。

举个栗子:

import React, { useState, useImperativeHandle, useRef } from 'react';
import { Button } from 'antd-mobile';
const Child = ({cRef}) => {
  const [count, setCount] = useState(0)
  useImperativeHandle(cRef, () => ({
    add
  }))
  const add = () => {
    setCount((v) => v + 1)
  }
  return <div style={{marginBottom: 20}}>
    <p>点击次数:{count}</p>
    <Button color='primary' onClick={() => add()}>加1</Button>
  </div>
}
const Index = () => {
  const ref = useRef(null)
  return <div style={{padding: 20}}>
    <div>大家好,我是小杜杜</div>
    <div>注意:是在父组件上的按钮,控制子组件的加1哦~</div>
    <Button
      color='primary'
      onClick={() =>  ref.current.add()}
    >
      父节点上的加1
    </Button>
    <Child cRef={ref} />
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useLayoutEffect

useLayoutEffect: 与useEffect基本一致,不同的地方时,useLayoutEffect同步

要注意的是useLayoutEffect在 DOM 更新之后,浏览器绘制之前,这样做的好处是可以更加方便的修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,所以useLayoutEffectuseEffect之前执行

如果是useEffect的话 ,useEffect 执行在浏览器绘制视图之后,如果在此时改变DOM,有可能会导致浏览器再次回流重绘

除此之外useLayoutEffectcallback 中代码执行会阻塞浏览器绘制

举个例子:

import React, { useState, useLayoutEffect, useEffect, useRef } from 'react';
import { Button } from 'antd-mobile';
const Index = () => {
  const [count, setCount] = useState(0)
  const time = useRef(null)
  useEffect(()=>{
    if(time.current){
      console.log("useEffect:", performance.now() - time.current)
    }
  })
  useLayoutEffect(()=>{
    if(time.current){
      console.log("useLayoutEffect:", performance.now() - time.current)
    }
  })
  return <div style={{padding: 20}}>
    <div>count: {count}</div>
    <Button
      color='primary'
      onClick={() => {
        setCount(v => v + 1)
        time.current = performance.now()
      }}  
    >
      加1
    </Button>
  </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useDebugValue

useDebugValue:可用于在 React 开发者工具中显示自定义 hook 的标签

官方并不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在开发者工具中的这个 Hook 旁边显示标签  
  // e.g. "FriendStatus: Online"  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

React v18中的hooks

useSyncExternalStore

useSyncExternalStore:是一个推荐用于读取订阅外部数据源hook,其方式与选择性的 hydration 和时间切片等并发渲染功能兼容

结构:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot])
  • subscribe: 订阅函数,用于注册一个回调函数,当存储值发生更改时被调用。此外, useSyncExternalStore 会通过带有记忆性的 getSnapshot 来判别数据是否发生变化,如果发生变化,那么会强制更新数据。
  • getSnapshot: 返回当前存储值的函数。必须返回缓存的值。如果 getSnapshot 连续多次调用,则必须返回相同的确切值,除非中间有存储值更新。
  • getServerSnapshot:返回服务端(hydration模式下)渲染期间使用的存储值的函数

举个栗子:

import React, {useSyncExternalStore} from 'react';
import { combineReducers , createStore  } from 'redux'
const reducer = (state=1,action) => {
  switch (action.type){
    case 'ADD':
      return state + 1
    case 'DEL':
      return state - 1
    default:
      return state
  }
}
/* 注册reducer,并创建store */
const rootReducer = combineReducers({ count: reducer  })
const store = createStore(rootReducer,{ count: 1  })
const Index = () => {
    // 订阅
    const state = useSyncExternalStore(store.subscribe,() => store.getState().count)
    return <div>
        <div>{state}</div>
        <div>
          <button onClick={() => store.dispatch({ type:'ADD' })} >加1</button>
          <button style={{marginLeft: 8}} onClick={() => store.dispatch({ type:'DEL' })} >减1</button>
        </div>
    </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

从上述代码可以看出,当点击按钮后,会触发 store.subscribe(订阅函数),执行getSnapshot后得到新的count,如果count发生变化,则会触发更新

useTransition

useTransition:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

那么什么是过渡任务?

在一些场景中,如:输入框、tab切换、按钮等,这些任务需要视图上立刻做出响应,这些任务可以称之为立即更新的任务

但有的时候,更新任务并不是那么紧急,或者来说要去请求数据等,导致新的状态不能立更新,需要用一个loading...的等待状态,这类任务就是过度任务

结构:

const [isPending, startTransition] = useTransition();
  • isPending:过渡状态的标志,为true时是等待状态
  • startTransition:可以将里面的任务变成过渡任务

大家可能对上面的描述存在着一些疑问,我们直接举个例子来说明:

import React, { useState, useTransition } from 'react';
const Index = () => {
  const [isPending, startTransition] = useTransition();
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
  return  <div>
      <div>大家好:我是小杜杜~</div>
      输入框: <input
        value={input}
        onChange={(e) => {
          setInput(e.target.value);
          startTransition(() => {
            const res = [];
            for (let i = 0; i < 2000; i++) {
              res.push(e.target.value);
            }
            setList(res);
          });
        }} />
      {isPending ? (
        <div>加载中...</div>
      ) : (
        list.map((item, index) => <div key={index}>{item}</div>)
      )}
    </div>
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

实际上,我们在Input输入内容是,会进行增加,假设我们在startTransition中请求一个接口,在接口请求的时候,isPending会为true,就会有一个loading的状态,请求完之后,isPending变为false渲染列表

useDeferredValue

useDeferredValue:接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。

如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。

也就是说useDeferredValue可以让状态滞后派生

结构:

const deferredValue = useDeferredValue(value);
  • value:可变的值,如useState创建的值
  • deferredValue: 延时状态

这个感觉和useTransition有点相似,还是以输入框的模式,举个栗子:

import React, { useState, useDeferredValue } from 'react';
const getList = (key) => {
  const arr = [];
  for (let i = 0; i < 10000; i++) {
    if (String(i).includes(key)) {
      arr.push(<li key={i}>{i}</li>);
    }
  }
  return arr;
};
const Index = () => {
  const [value, setValue] = useState("");
  const deferredValue = useDeferredValue(value);
  console.log('value:', value);
  console.log('deferredValue:',deferredValue);
  return (
    <div >
      <div>
        <div>大家好,我是小杜杜</div>
        输入框:<input onChange={(e) => setValue(e.target.value)} />
      </div>
      <div>
        <ul>{deferredValue ? getList(deferredValue) : null}</ul>
      </div>
    </div>
  );
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

和useTransition做对比

根据上面两个示例我们看看useTransitionuseDeferredValue做个对比看看

  • 相同点:useDeferredValueuseTransition一样,都是过渡更新任务
  • 不同点:useTransition给的是一个状态,而useDeferredValue给的是一个值

useInsertionEffect

useInsertionEffect:与 useEffect一样,但它在所有 DOM 突变之前同步触发。

我们来看看useInsertionEffect对比于useEffectuseLayoutEffect在执行顺序上有什么区别,栗子:

  useEffect(()=>{
    console.log('useEffect')
  },[])
  useLayoutEffect(()=>{
    console.log('useLayoutEffect')
  },[])
  useInsertionEffect(()=>{
    console.log('useInsertionEffect')
  },[])

「React 深入」一文吃透React v18全部Api(1.3w+)

可以看到在执行顺序上 useInsertionEffect > useLayoutEffect > useEffect

特别注意一点:seInsertionEffect应仅限于 css-in-js 库作者使用。优先考虑使用useEffectuseLayoutEffect来替代。

模拟一下seInsertionEffect的使用:

import React, { useInsertionEffect } from 'react';
const Index = () => {
  useInsertionEffect(()=>{
    const style = document.createElement('style')
    style.innerHTML = `
      .css-in-js{
        color: blue;
      }
    `
    document.head.appendChild(style)
 },[])
  return (
    <div>
        <div className='css-in-js'>大家好,我是小杜杜</div>
    </div>
  );
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

useId

useId :是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免hydration 不匹配的 hook

这里牵扯到SSR的问题,我打算之后在单独写一章,来详细讲讲,所以在这里就介绍一下使用即可

    const id = useId();

例子:

import React, { useId } from 'react';
const Index = () => {
  const id = useId()
  return (
    <div>
        <div id={id} >
          大家好,我是小杜杜
        </div>
    </div>
  );
}
export default Index

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

自定义hooks

自定义hooks是在react-hooks基础上的一个扩展,可以根据业务、需求去制定相应的hooks,将常用的逻辑进行封装,从而具备复用性

关于自定义hooks的内容可以看看我之前的文章:搞懂这12个Hooks,保证让你玩转React

里面通过分析ahooks源码,讲解了很多不错的自定义hooks,如:useCreationuseReactiveuseEventListener等的实现,相信一定能够帮助到各位,感兴趣的可以支持下~

react-dom

react-dom:这个包提供了用户DOM的特定方法。这个包在React v18中还是做了很大的改动,接下来我们逐个看看

createPortal

createPortal:在Portal中提供了一种将子节点渲染到已 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。

也就是说createPortal可以把当前组件或element元素的子节点,渲染到组件之外的其他地方。

来看看createPortal(child, container)的入参:

  • child:任何可渲染的子元素
  • container:是一个DOM元素

看着概念可能并不是很好理解,我们来举个栗子:

import React, { useState, useEffect, useRef } from 'react';
import ReactDom from 'react-dom'
const Child = ({children}) => {
  const ref = useRef()
  const [newDom, setNewDom] = useState()
  useEffect(() => {
    setNewDom(ReactDom.createPortal(children, ref.current))
  }, [])
  return <div>
    <div ref={ref}>同级的节点</div>
    <div>
      这层的节点
      {newDom}
    </div>
  </div>
}
const Index = () => {
  return <div style={{padding: 20}}>
    <Child>
      <div>大家好,我是小杜杜</div>
    </Child>
  </div>
}
export default Index;

要注意下Child:

「React 深入」一文吃透React v18全部Api(1.3w+)
我们传入的childrencreatePortal包裹后,children的节点位置会如何?

「React 深入」一文吃透React v18全部Api(1.3w+)
发现,我们处理的数newDom的数据到了同级的节点处,那么这个Api该如何应用呢?

我们可以处理一些顶层元素,如:Modal弹框组件,Modal组件在内部中书写,挂载到外层的容器(如body),此时这个Api就非常有用

flushSync

flushSync:可以将回调函数中的更新任务,放到一个较高级的优先级中,适用于强制刷新,同时确保了DOM会被立即更新

为了更好的理解,我们举个栗子:

import { Button } from 'antd-mobile';
import React, { Component} from 'react';
import ReactDOM from 'react-dom'
class Index extends Component{
  constructor(props){
    super(props)
    this.state={
      number: 0
    }
  }
  render(){
    const { number } = this.state
    console.log(number)
    return <div style={{padding: 20}}>
      <div>数字: {number}</div> 
      <Button
        color='primary'
        onClick={() => {
          this.setState({ number: 1  })
          this.setState({ number: 2  })
          this.setState({ number: 3  })
        }}
      >
        点击 
      </Button>    
    </div>
  }
}
export default Index;

我们看看点击按钮会打印出什么?

「React 深入」一文吃透React v18全部Api(1.3w+)

这个不难理解,因为this.setState会进行批量更新,所以打印出的是3 接下来,我们用flushSync处理下number: 2 来看看是什么效果:

    onClick={() => {
      this.setState({ number: 1  })
      ReactDOM.flushSync(()=>{
        this.setState({ number: 2  })
      })
      this.setState({ number: 3  })
    }}

「React 深入」一文吃透React v18全部Api(1.3w+)

可以发现flushSync会优先执行,并且强制刷新,所以会改变number值为2,然后13在被批量刷新,更新为3

render

render:这个是我们在react-dom中最常用的Api,用于渲染一个react元素

我们通常使用在根部,如:

ReactDOM.render(
    < App / >, 
    document.getElementById('app')
)

createRoot

React v18中,render函数已经被createRoot所替代

createRoot会控制你传入的容器节点的内容。当调用 render 时,里面的任何现有 DOM 元素都会被替换。后面的调用使用 React 的 DOM diffing 算法进行有效更新。

并且createRoot不修改容器节点(只修改容器的子节点)。可以在不覆盖现有子节点的情况下将组件插入现有 DOM 节点。

如:

import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <Main />
  </StrictMode>
);

hydrate

hydrate:服务端渲染用hydraterender()相同,但它用于在ReactDOMServer渲染的容器中对 HTML 的内容进行 hydrate 操作。

hydrate(element, container[, callback])

hydrateRoot()

hydrateReact v18也被替代为hydrateRoot()

hydrateRoot(container, element[, options])

unmountComponentAtNode

unmountComponentAtNode:从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回true,如果没有组件可被移除将会返回false

举个栗子:

import { Button } from 'antd-mobile';
import React, { Component} from 'react';
import ReactDOM from 'react-dom'
const Child = () => {
  return <div>大家好,我是小杜杜</div>
}
class Index extends Component{
  constructor(props){
    super(props)
    this.state={
      number: 0
    }
  }
  node = null
  componentDidMount(){
    ReactDOM.render(<Child/>, this.node) // 创建一个容器
  }
  render(){
    const { number } = this.state
    console.log(number)
    return <div style={{padding: 20}}>
      <div ref={(node) => this.node = node}></div> 
      <Button
        color='primary'
        onClick={() => {
          const res = ReactDOM.unmountComponentAtNode(this.node)
          console.log(res)
        }}
      >
        卸载 
      </Button>    
    </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

root.unmount()

unmountComponentAtNode 同样在React 18中被替代了,替换成了createRoot中的unmount()方法

const root = createRoot(container);
root.render(element);
root.unmount()

findDOMNode

findDOMNode:用于访问组件DOM元素节点(应急方案),官方推荐使用ref

需要注意的是:

  • findDOMNode只能用到挂载的组件上
  • findDOMNode只能用于类组件,不能用于函数式组件
  • 如果组件渲染为null或者为false,那么findDOMNode返回的值也是null
  • 如果是多个子节点Fragment的情况,findDOMNode会返回第一个非空子节点对应的 DOM 节点。
  • 在严格模式下这个方法已经被弃用

举个例子:

import { Button } from 'antd-mobile';
import React, { Component} from 'react';
import ReactDOM from 'react-dom'
class Index extends Component{
  render(){
    return <div style={{padding: 20}}>
      <div>大家好,我是小杜杜</div> 
      <Button
        color='primary'
        onClick={() => {
          console.log(ReactDOM.findDOMNode(this))
        }}
      >
        获取容器
      </Button>    
    </div>
  }
}
export default Index;

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

unstable_batchedUpdates

unstable_batchedUpdates :可用于手动批量更新state,可以指定多个setState合并为一个更新请求

那么这块手动合并,用在什么情况下呢?来看看下面的场景:

import { Button } from 'antd-mobile';
import React, { Component} from 'react';
import ReactDOM from 'react-dom'
class Index extends Component{
  constructor(props){
    super(props)
    this.state={
      number: 0
    }
  }
  render(){
    const { number } = this.state
    return <div style={{padding: 20}}>
      <div>数字: {number}</div> 
      <Button
        color='primary'
        onClick={() => {
          this.setState({ number: this.state.number + 1 })
          console.log(this.state.number)
          this.setState({ number: this.state.number + 1  })
          console.log(this.state.number)
          this.setState({ number: this.state.number + 1 })
          console.log(this.state.number)
        }}
      >
        点击 
      </Button>    
    </div>
  }
}
export default Index

当我们点击按钮后,三个打印会打印出什么?

「React 深入」一文吃透React v18全部Api(1.3w+)
此时的场景只会执行一次,并且渲染一次,渲染时为1

那么我们打破React的机制,比如说使用setTimeout绕过,再来看看会打印出什么:

      <Button
        color='primary'
        onClick={() => {
          setTimeout(() => {
            this.setState({ number: this.state.number + 1 })
            console.log(this.state.number)
            this.setState({ number: this.state.number + 1  })
            console.log(this.state.number)
            this.setState({ number: this.state.number + 1 })
            console.log(this.state.number)
          }, 100)
        }}
      >
        点击 
      </Button>  

此时就会这样:

「React 深入」一文吃透React v18全部Api(1.3w+)

因为绕过了事件机制,此时就会渲染3次,并且渲染的结果为3

那么我们现在想在setTimeout实现React的事件机制该怎么办?就需要用到unstable_batchedUpdates来解决这类问题

      <Button
        color='primary'
        onClick={() => {
          setTimeout(() => {
            ReactDOM.unstable_batchedUpdates(() => {
              this.setState({ number: this.state.number + 1 })
              console.log(this.state.number)
              this.setState({ number: this.state.number + 1  })
              console.log(this.state.number)
              this.setState({ number: this.state.number + 1 })
              console.log(this.state.number)
            })
          }, 100)
        }}
      >
        点击 
      </Button> 

效果:

「React 深入」一文吃透React v18全部Api(1.3w+)

最后

参考文档

  • react 官方文档
  • 「React 进阶」 React 全部 Hooks 使用大全 (包含 React v18 版本 )

总结

本文基本总结了React的所有Api,如果有没写到的,或者是Api用法没写全的,请在下方评论区留言,尽量把这篇文章打造成最全的~

主要包扩组件类工具类生命周期react-hooksreact-dom五大模块的内容,如果你能耐心的看完,相信你对React一定有了更深的理解,同时建议初学者亲自尝试一遍,看看这些Api怎么用,如何用~

至此,签约计划的三篇文章就写完了,说实话,太累了,写出硬文确实比较难,还是希望大家多多关注这个专栏,后续将会带来更好,更全,更容易理解的React文章,还请各位小伙伴多多支持,点赞 + 收藏 哦~

其他react好文:

  • 搞懂这12个Hooks,保证让你玩转React
  • 作为一名React,我是这样理解HOC的!
  • 「React深入」一文吃透虚拟DOM和diff算法
  • 花三个小时,完全掌握分片渲染和虚拟列表~

我正在参与技术社区创作者签约计划招募活动,点击链接报名投稿。

发表评论

提供最优质的资源集合

立即查看 了解详情