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

16,511 阅读37分钟

大家好,我是小杜杜,俗话说的好,工欲善其事必先利其器,什么意思呢?就是说你想玩转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的使用方法,和场景,如有不足,还希望在评论区指出~

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

全面解读ReactApi.png

组件类

Component

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

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

image.png

可以看出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;

效果:

img1.gif

可以发现,当我们点击按钮的时候,数字并没有刷新,这是因为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会刷新吗?

直接看看效果:

img2.gif

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

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

效果:

img3.gif 可以看出,加上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;

效果:

img4.gif

当数字小于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传递会怎样:

企业微信截图_0bc3e73d-e6b4-45a9-bd66-c43cd8bbea11.png

接下来看看利用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;

效果:

企业微信截图_c7a4aa75-362d-4bba-8c83-ded0b4dce01b.png

如此以来就解决了不能在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实际上是没有节点的  企业微信截图_34d1a03b-acdd-4ed0-9392-86fb764731c7.png

另外,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;

效果:

img5.gif

Suspense

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

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

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

Profiler

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

此组件有两个参数:

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

举个栗子🌰:

import React, { Component, Profiler } from 'react';



export default Index;

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

image.png

依此的含义:

  • 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进行检查

主要有以下帮助:

工具类

crateElement

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

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

结构:

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

  • element:原生组件的话是标签的字符串,如“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来看看:

image.png

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

createContext

createContext:相信大家对这个Api很熟悉,用于传递上下文。createContext会创建一个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;

效果:

image.png

注意:如果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看到:

image.png

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

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

image.png

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

Children.map

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

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

效果:

image.png

Children.forEach

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

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

效果:

image.png

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
}

效果: 只有一个时:

image.png

多个时: image.png

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;

效果: image.png

这里需要注意的是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;

效果:

image.png

这个有点鸡肋,因为我们可以直接从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;

image.png

isValidElement

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

栗子🌰:

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

version

查看React的版本号:如:

console.log(React.version)

版本:

image.png

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

packages/shared/ReactVersion.js

image.png

生命周期

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

v16.0前

image.png

从图中,总共分为四大阶段: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

12b.png

与 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

效果:

img5.gif

注意点

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

img1.gif

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

效果:

img1.gif

根据依赖值改变

我们可以设置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

效果: img2.gif

useContext

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

useContext的参数一般是由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

效果: img3.gif

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

效果:

img4.gif

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 给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:

img2.gif

我们发现了一个问题,为什么点击切换按钮也会触发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;

img3.gif

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

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;

img2.gif

我们可以看到,当点击切换按钮的时候,没有经过 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;

效果: img1.gif

从上述可知,我们可以通过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

效果:

img5.gif

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

效果:

img6.gif

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

效果:

img1.gif

从上述代码可以看出,当点击按钮后,会触发 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

效果:

img3.gif

实际上,我们在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

效果:

img4.gif

和useTransition做对比

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

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

useInsertionEffect

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

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

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

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

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

image.png

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

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

模拟一下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

效果:

image.png

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

效果: image.png

自定义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:

image.png 我们传入的childrencreatePortal包裹后,children的节点位置会如何?

image.png 发现,我们处理的数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;

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

img6.gif

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

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

img1.gif

可以发现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:服务端渲染用hydrate与 render()相同,但它用于在 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;

效果:

img2.gif

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;

效果: img3.gif

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

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

image.png 此时的场景只会执行一次,并且渲染一次,渲染时为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>  

此时就会这样:

image.png

因为绕过了事件机制,此时就会渲染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> 

效果:

img4.gif

最后

参考文档

总结

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

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

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

其他react好文:

玩转 React Hooks 小册

小册链接:《玩转 React Hooks》

知其然,知其所以然。React Hooks 带来的全新机制让人耳目一新,因为它拓展了 React 的开发思路,为 React 开发者提供了一种更方便、更简洁的选择。

在引入 Hooks 的概念后,函数组件既保留了原本的简洁,也具备了状态管理、生命周期管理等能力,在原来 Class 组件所具备的能力基础上,还解决了 Class 组件存在的一些代码冗余、逻辑难以复用等问题。因此,在如今的 React 中,Hooks 已经逐渐取代了 Class 的地位,成了主导。

而且,Hooks 相对于 Class 而言,更容易上手,其简洁性、逻辑复用性等特性深受开发者喜爱,可谓是前端界的"流量明星",不止 React,Vue 3.0 、Preact、Solid.js 等框架也都选择加入 Hooks 的大家庭,前端的日常工作也在趋向于 Hooks 开发。

因此,掌握好 React Hooks 是非常有必要的一件事。本小册会通过基础篇、原码篇、实践篇 三大方向 探讨 Hooks,从原码的角度探寻 React 的奥秘。

除此之外,小册会以 React Hooks 为核心,同时穿插其他知识,如 TS、Jest、Fiber 等核心知识,并包含 React v18 的并发、数据撕裂等概念,最后结合 Hooks 写一个简易版 react-redux 和 Form 表单,通过其设计思想,助你在面试中脱颖而出。

小册整体设计如下思维导图所示:

玩转hooks.png

你会学到什么?

  1. 全面知悉 React 提供的 15 Hooks API 的使用和场景;
  2. 手写 30+ 自定义 Hooks 的实现,全面掌握设计思想;
  3. 了解 Hooks 源码,从根源上彻底解决现有的难点;
  4. 掌握函数式编程思想,用于工作,享受便利。

最后

感谢各位小伙伴的支持,如果在阅读过程中有什么问题欢迎大家加我微信,交个朋友,微信:domesyPro, 也可以关注笔者的公众号:杜杜的全栈之旅,一起来玩转 React Hooks 吧~