React 之实战总结(近1w字,我踩过的坑大家就别再踩了😂)

5,583 阅读27分钟

写这篇文章初衷是整理一下自己这近几个月的心路历程,从刚开始(19年10月份) “入坑”react时的一脸懵,不知如何下手,到现在可以写简单的业务。讲述自己从完全不了解这个框架 ,然后又一步步上道儿,我这里就称之为“爬坑”,哈哈。所以想把自己的学习过程以及爬坑记录下来,给自己日后翻阅,如有也在写react ,想交流的小伙伴,文末有微信哦,哈哈。其实从很早就想研究下react了,只是时间上的不允许,现在终于可以腾出时间来(其实平时工作较饱和,也只能挤业余时间了)。

------文中的示例都是自己经过实践的,如理解有误,还请告知哦!😂------

看完React之后,推荐看这篇进阶版:# 为了提升代码质量我做了哪些努力?(近2万字,建议收藏),里面同样记录了更多的实战经验。

环境介绍:

项目使用umi脚手架配合dva搭建,ui组件是ant-design,模版使用的是antdesign的pro-layout现成的模版,较多使用aHooks来实现网络请求、节流等操作,是一个使用了都说真香的Hooks库,参考我的另一篇文章:aHooks方法库实操指南

具体版本号如下:

    "@ant-design/pro-layout": "4.7.0",
    "@antv/g2": "^3.5.11",
    "antd": "^3.25.2",
    "array-move": "^2.2.0",
    "umi": "2.12.3",
    "ahooks": "^2.0.1",
    "umi-plugin-react": "1.14.7",
    "uuid": "^3.3.3",
    "axios": "^0.19.0",
    "bizcharts": "^3.5.6",
    "classnames": "^2.2.6",
    "copy-to-clipboard": "^3.2.0",
    "dayjs": "^1.8.17",
    "immutable": "^4.0.0-rc.12",
    "lodash": "^4.17.15",
    "moment": "^2.24.0",
    "mz-modules": "^2.1.0",
    "parameter": "^3.6.0",
    "prop-types": "^15.7.2",
    "qrcode": "^1.4.4",
    "qs": "^6.9.1",
    "rc-form-hooks": "^0.0.1-alpha.22",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "swr": "^0.1.12",
    

关于react的生命周期

如同vue一样,react 也是有自己的生命周期,方便我们根据加载顺序来执行相应的操作。但由于Hooks的出现,似乎react 已经不在需要关心这些生命周期的问题,HooksReact 16.8 新增的特性,在你不需要写class 组件的情况下,就赋予了函数式组件 state 状态管理及生命周期函数的特性,当然下面也会详细介绍hooks将如何使用。

class 类组件的生命周期

常用的生命周期如下:

  1. 在渲染前调用:componentWillMount

  2. 在第一次渲染后调用:componentDidMount

  3. 在组件完成更新前调用:componentWillUpdate

  4. 在组件完成更新后立即调用:componentDidUpdate

  5. 在组件接收到一个新的 prop (更新后)时被调用:componentWillReceiveProps

  6. 在组件从 DOM 中移除之前立刻被调用:componentWillUnmount

react的父组件和子组件生命周期执行顺序:

加载渲染过程

父 componentWillMount => 父 render => 子 componentWillMount =>子 render => 子 componentDidMount => 父componentDidMount

子组件通过props取值并更新过程:

子 componentWillUpdate => 父 render => 子 componentWillReceiveProps => 父 componentWillUpdate => 子 render => 子 componentDidUpdate => 父 componentDidUpdate

单一父 / 子组件(不依赖props)更新过程:

componentWillUpdate => render => componentDidUpdate 

销毁过程:

componentWillUnmount

这里的介绍顺序是从单文件引用react 示例开始介绍,然后是 class 类组件,然后是hooks函数组件,循序渐进,利于理解和吸收。


一、 编译

let root =  document.getElementById('example');

  /* 
  * 类似vue的template ,第一个参数是插入的模版,第二个是插入的根元素
  * 在react中样式中的 class 要重命名为 className 
  * render 是必不可少的,用来渲染到页面上
  */

  ReactDOM.render(

    <h1 className="box">Hello, world!</h1>,

   root

  );

效果展示:


二、 定义变量

react中定义变量是很方便的,可以定义数组,数组中可以包含我们的html 标签,然后可以将变量直接带入到页面上。

<body>
    <div id="example"></div>
    <script type="text/babel">
      let root =  document.getElementById('example')
      let arr = [
        <h1 >Hello world!</h1>,
        <h2 >Hello React!</h2>,
      ];
      // 注意,react 的变量使用的是单花括号 {}
      ReactDOM.render(
        <div>{arr}</div>,
        root
      
      );
    </script>

效果展示:


三、组件

React里组件起初都是用class来写的(虽然现在都在用hooks,但这部分还是保留,就当是记录下它的发展史,hooks来写组件无疑是很爽的,下面会介绍到。)。

 <body>
    <div id="example"></div>
    <script type="text/babel">
    
      let root = document.getElementById('example');
      
      // class 的名字必须大写,并继承自 React.Component
      class HelloMessage extends React.Component {
        constructor(...args){
          super(...args);
          
          this.name=this.props.name
          this.job=this.props.job
          this.age = this.props.age
        }
        
        fn(){
          return "Aaa"
        }
        render() {
            // 变量还可以直接定义标签,style后面需跟一个{},而里面的内容需要是一个json,所以此处看起来是两个{{}}
            let div =<div style={{color:'red'}}>我是div</div>
          return (
            <div>
                // 花括号中的值还可以参与计算
                姓名: {this.name}<br/>
                工作: {this.job}<br/>
                年龄: {this.age+3}<br/>
                
                // 花括号不仅可以输出变量,还可以输出方法
                {this.fn()} <br/>
                
                // 将标签输出到页面
                {div}
            </div>
            );
        }
      }

      ReactDOM.render(
        <HelloMessage name="John" job="teacher" age="18"/>,
        root
      );
    </script>
  </body>


四、循环

JSX语法允许我们把htmljs 穿插来写,我们来看看最常用的循环怎么写。

 <body>
    <div id="example"></div>
    <script type="text/babel">
      let root = document.getElementById('example');

      
      class HelloMessage extends React.Component {
        constructor(...args){
          super(...args)
        }
        
        
        render() {
        let root =  document.getElementById('example')
        let names = ['Alice', 'Emily', 'Kate'];
          return (
            <div>
               { 
                names.map( (item)=> {
                // 循环中需要添加key值,用来保证唯一性
                return <div key={item}>Hello, {item}!</div>
                })
                 }
            </div>
          
            );
        }
      }

      ReactDOM.render(
       <div>
          <HelloMessage />
        </div>,
        root
      );
    </script>
  </body>


效果展示:


五、组件嵌套

我们平常的开发中,有时候需要用到些公共组件,那我们就应对其进行封装提取出来,以下是粗略版的父子组件嵌套写法

<body>
        <div id="example"></div>
        <script type="text/babel">
        
     // 父组件   
     class Parent extends React.Component{
       constructor(...args){
           super(...args)
       }
       render(){
           return(
            <ul>
            // 将写好的子组件嵌套进来即可
                    <Child/>
                    <Child/>
                    <Child/>
                    <Child/>
            </ul>
           )
       }
     }

    // 子组件
     class Child extends React.Component{
        constructor(...args){
            super(...args)
        }
        render(){
            return(
               <li>111</li> 
            )
        }
     }

      ReactDOM.render(
       
        <Parent />,
        document.getElementById('example')
      );
    </script>
    </body>

一般情况下,render 里面只会有一个最大的标签包含,如果你有两个标签,请在外面添加一个包裹标签,正确写法:

      ReactDOM.render(
       <div>
            <Parent></Parent>
            <Child></Child>
       </div>,
        document.getElementById('example')
      );

错误写法

 ReactDOM.render(
 
       <Child></Child>
       
        <Parent></Parent>
       ,
        document.getElementById('example')
      );

在脚手架中还可用 react 中的空标签<></> 去充当我们最外层的包裹层,它的好处是不会多生成一个div

import { Component } from 'react'
class ConfigContent extends Component {
    constructor(...args){
        super(...args)
    }
    render(){
        return(
            <>
                <Parent></Parent>
                <Child></Child>
            </>
        )
    }
}

export default ConfigContent

组件可以写成单标签或者双标签两种形式。如下:

// 双标签
  <Parent></Parent>
  
 // 单标签
 
 <Parent/>

六、 父子组件传参 props

父组件给子组件传递参数,子组件接收,并渲染子组件: 父组件=>子组件

父组件

import { PureComponent } from "react";
import Child from "./child";

class Parent extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { id: 1 };
  }

  render() {
    return (
        <Child id={this.state.id} />
    );
  }
}

export default Parent;

子组件:

import { PureComponent } from "react";

class Child extends PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <h1>child-page</h1>
        <p>{this.props.id}</p>
      </div>
    );
  }
}

export default Child;

效果展示:

子组件通过事件将子组件的值传到父组件: 子组件 => 父组件

子组件:

import { Button } from "antd";
import { PureComponent } from "react";

class Child extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }

  action = {
    handleChange: () => {
      this.props.changeEvent(`子组件定义的值:${this.state.a}`);
    }
  };

  render() {
    return (
      <div>
        <h1>child-page</h1>
        <Button type="primary" onClick={this.action.handleChange}>
          改变父组件
        </Button>
      </div>
    );
  }
}

export default Child;

父组件:

import { PureComponent } from "react";
import { Button } from "antd";
import Child from "./child";

class Parent extends PureComponent {
  constructor(props) {
    super(props);
  }

  action = {
    changeEvent: mode => {
      console.log("父组件收到的值:", mode);
    }
  };

  render() {
    return (
      <div>
        <Child changeEvent={mode => this.action.changeEvent(mode)} />
      </div>
    );
  }
}

export default Parent;

点击后的效果展示:

七、 添加事件

事件是我们在交互过程中必不可少的,那么我们试试看,在react中如何添加事件。

(1)我们原生的添加事件的方式,采用的是小写onclick

<button onclick="activateLasers()">
  Activate Lasers
</button>

(2)react的添加事件,采用驼峰的方式定义onClick

<button onClick={activateLasers}>
  Activate Lasers
</button>

下面介绍几种在项目中添加事件的方式,大家可根据情况选择:

1. 在标签中添加方法及函数体,也是和初始的事件最接近的方式:

<body>
        <div id="example"></div>
        <script type="text/babel">
 
           class Child extends React.Component {
            constructor(...args){
              super(...args)
              this.a=[123]
            }
           // 直接在标签上添加
            render() {
              return (
                <div onClick={function(){
                  console.log("eee")
                }}>{this.a}</div>
              )
            }
            
           }
           ReactDOM.render(
           <Child/>,
             document.getElementById('example')
           )
    </script>
    </body>

2. 在class组件中添加方法(需要重新绑定this):

  <body>
        <div id="example"></div>
        <script type="text/babel">
 

     class Cmp1 extends React.Component{
        constructor(...args){
            super(...args)
        }
        fn(){
          // props只读的,这里的值是不可改的,是从外面传进来的
          console.log(this.props.a)  // 0 
        }
        // onClick 类似原生事件,此处bind就是把我们的fn的this牢牢绑在组件上,此时的内部也能拿到我们组件的this
        render(){
            return(
               <div>
                {this.props.a}
               <input type="button" value="+1" onClick={this.fn.bind(this)}/>
               </div>
            )
        }
    }

      ReactDOM.render(
       <div>
        
        <Cmp1 a={0}/>
       
       </div>,
        document.getElementById('example')
      );
    </script>
    </body>

3. 定义属性,在其内部添加方法(简单,this的绑定不会变):

<body>
        <div id="example"></div>
        <script type="text/babel">
 
           class Child extends React.Component {

            constructor(...args){
              super(...args)
              this.a=[123]
            }
            // 定义变量
            action ={
              fn(){
                console.log(23)
              }
            }
            // 在这里直接调用,就不用绑定this了
            render() {
              return (
                <div onClick={this.action.fn}>{this.a}</div>
              )
            }
            

           }
           ReactDOM.render(
           <Child/>,
             document.getElementById('example')
           )
    </script>
    </body>

4. 第四种添加事件方式

<body>
        <div id="example"></div>
        <script type="text/babel">
 
           class Child extends React.Component {

            constructor(...args){
              super(...args)
              this.a=[123]
            }
         
              fn=()=>{
                console.log(23)
              }
    
            render() {
              return (
                <div onClick={this.fn}>{this.a}</div>
              )
            }
            
           }
           ReactDOM.render(
           <Child/>,
             document.getElementById('example')
           )
    </script>
    </body>

5. React Hooks 中的事件添加

当我们了解到React Hooks的事件添加后,我们终于松了口气,终于不再考虑this绑定的问题了

import React from 'react';
import { Button } from 'antd';

const App = () => {
  const handleClick = () => {
    console.log('按钮点击');
  };

  return <Button onClick={handleClick}>按钮</Button>;
};

export default App;

备注一下,关于事件传参的写法

import { PureComponent } from "react";
import { Button } from "antd";

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { arr: [1, 2, 3, 4] };
  }

  action = {
    handleClick: i => {
      console.log(i.target.innerHTML);
    }
  };

  render() {
    return (
      <div>
        {this.state.arr.map((item, index) => {
          return (
            <Button
              key={index}
              onClick={index => this.action.handleClick(index)}
            >
              {item}
            </Button>
          );
        })}
      </div>
    );
  }
}

export default App;

效果展示:

八、利用state制作一个 input ++ 功能

state 构造函数是唯一能够初始化 this.state 的地方,接收一个对象,是可变的,可以是内部加的,也可以从外部传进来,this.setSate是唯一改变state的方式。

// 制作一个input ++ 功能
<body>
        <div id="example"></div>
        <script type="text/babel">
 

     class Cmp1 extends React.Component{
        constructor(...args){
            super(...args)
           
            this.state={a:0}
        }
       
        fn(){
          // this.setSate是唯一改变state的方式
        this.setState({a:this.state.a+1})
        }
        render(){
            return(
               <div>
               {this.state.a}
               <input type="button" value="+1" onClick={this.fn.bind(this)}/>
               </div>
            )
        }
    }

      
      ReactDOM.render(
       <div>
            <Cmp1/>
       </div>,
        document.getElementById('example')
      );
    </script>
    </body>

页面效果:


九、路由跳转 && 路由传参 && 获取参数

我们在跳转路由时,有时需要给跳转到到页面携带一些参数来定位页面的显示或回显数据,此小节我们就来看看如何传参,以及如何取参。

首先我们先拿一下props,看看都有哪些参数:

console.log(this.props);

参数如下:

我们来具体解析一下:

history:包含了路由pushreplacegoBack 等方法,以及可拿到querystate 等参数

history:{
   
    location:{
        pathname:'/dashboard/workplace', // url地址
        search:'?name='xiaoxiao',  // 拿到的是完整的参数字符串
        hash:'',
        query:{name:'xiaoxiao'},  // 拿到参数的对象格式
        state:undefined  // 拿到通过state传入的参数
    }, 
    push:function push(path,state){} ,  // 跳转到指定路径
    replace:function replace(path,state){} , // 跳转到指定路径,不会保留history
    goBack:function goBack(path,state){} , // 返回上一个路由地址
}

这个location 同上面的location,可拿到query参数 以及state 参数

location:{
    pathname:'/dashboard/workplace',
    search:'?name='xiaoxiao',
    query:{name:'xiaoxiao'},
    state:undefined
}

包含了具体的 url 信息,并可以拿到params的参数的值。

match:{
    path:'/dashboard/workplace',
    url:'/dashboard/workplace',
    params:{}
}

接下来我们就使用:this.props.history.push 来模拟跳转,并携带参数:

1. query

通过url来进行传参,地址栏是可见的

⚠️注意️:刷新页面后,参数不会丢失,可传对象

A 页面:

 this.props.history.push({
    pathname: "/dashboard/workplace",
    query: { name: "xiaoqiu" }
});

B页面 (参数在url里问号后显示)

http://localhost:8000/#/dashboard/workplace?name=xiaoqiu

打印一下我们接收到的参数:

2. state

state传参,同query差不多,只是属性不一样,而且state传的参数是加密的,不会在地址栏显示

注意️:刷新页面后,参数就会丢失,可传对象

A页面:

this.props.history.push({
    pathname: "/dashboard/workplace",
    state: { name: "xiaoqiu" }
});

B页面: (参数并没有出现在地址栏哦!)

http://localhost:8000/#/dashboard/workplace

打印一下我们接收到的参数:


此时我们刷新一下页面,看看是否还可以拿到state的值:

这时state的值已经丢失,评论中有位掘友反应,刷新后在history.state 里面有state的值,我想说,history没有直接的state属性,state是在historylocation的属性中,不知道是否能解释那位掘友的问题🤔,欢迎大家一起讨论哦。

追加:得到了掘友新的反馈:刷新页面state的值会存在浏览器的history.state中,接下来我们做一下验证: 首先在接到参数页面打印:

history.state

经过上面的查询,拿到的结果是null,我去查阅了相关api

history.pushState({page: 1}, "title 1", "?page=1")


使用pushState赋值的可以拿到history.state:

3. search

searchquery参数一样是通过url进行传输

⚠️注意️:刷新页面后,参数不会丢失,但只可传字符串,不能传输对象

A页面:

this.props.history.push({
    pathname: "/dashboard/workplace",
    search: "a=1&b=2"
});

B页面

http://localhost:8000/#/dashboard/workplace?a=1&b=2

打印一下我们接收到的参数:


总结一下:

此时我们注意到无论是通过query或是search 传参,返回参数时两者也同时都能取到,只是展现方式不同。query是以对象形式展现,search是以字符串展现,如果你想在地址栏显示,那就用query,如果想加密传输,那就用state,但要注意使用state传递参数刷新后就会丢失哦!

十、ref获取组件实例

通过给组件绑定ref 可以获取到整个子组件的实例,进而可传参数并调用其方法,也能拿到表单输入值

1. 传统的ref,获取当前组件的参数以及事件

import { Button } from "antd";
import { PureComponent } from "react";

class Child extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }

  action = {
    handleChange: () => {
      console.log(this.refs["button"]);
    }
  };

  render() {
    return (
      <div>
        <h1>child-page</h1>
        <Button type="primary" onClick={this.action.handleChange} ref="button">
          改变父组件按钮
        </Button>
      </div>
    );
  }
}

export default Child;

控制台打印ref拿到的组件参数以及方法:

2. reactcreateRef,拿到子组件的实例

import { PureComponent, createRef } from "react";
import { Button } from "antd";
import Child from "./child";

class Parent extends PureComponent {
  constructor(props) {
    super(props);
    this.children = createRef();
    this.state = { id: 1, arr: [1, 2, 3, 4] };
  }

  action = {
    handleClick: () => {
        console.log(this.children);
    }
  };

  render() {
    return (
      <div>
        <Button onClick={() => this.action.handleClick()}>
            按钮
        </Button>
        子组件:
        <Child ref={this.children} />
      </div>
    );
  }
}

export default Parent;

控制台输出子组件的值:

3. useRef 获取元素 / 子组件实例

(1)获取元素的ref对象 ,可以返回一个元素的实例
const refVideo = useRef<HTMLElement | any>(null);

const handleChange = () => {

    if (refVideo && refVideo.current) {

    console.log(refVideo.current)

    }
};

绑定元素:

<video

    ref={refVideo}

    src='http://www.abc.com'

    preload="auto"

    autoPlay={true}

/>

<Switch defaultChecked onChange={handleChange} />
(2)获取子组件-class组件的实例

⚠️ 注意: ️如果想拿到子组件的实例,子组件必须是一个class类组件参考react官网

截屏2022-12-16 17.23.53.png

父组件-函数组件:

import { useRef, useEffect } from 'react'
import Child from './testChild'

const Parent = () => {
  const divRef = useRef()

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

  return (
    <div>
      hello hooks
      <Child ref={divRef} text={'子组件'} />
    </div>
  )
}
export default Parent
 

子组件-class组件

import { PureComponent } from 'react'

class Child extends PureComponent {
  constructor(props) {
    super(props)
    this.state = { count: 12 }
  }
  render() {
    return <div>子组件</div>
  }
}
export default Child

控制台输出:

(3) 父组件和子组件都是函数组件,如何获取子组件实例?

首先将ref传递到子组件中,然后需要使用forwardRef对子组件进行包装

父组件:

import ForwardChild from './ForwardChild'

const Parent = () => {
  const parentRef = useRef();
  function focusHander() {
    console.log(parentRef);
    parentRef.current.focus();
    parentRef.current.value = '哈哈';
  }
  return (
    <>
      <ForwardChild ref={parentRef} />
      <button onClick={focusHander}>获取焦点</button>
    </>
  )
}

export default Parent;

子组件:

const Child=(props, parentRef)=> {
  console.log(props);
  return (
    <>
      <input type="text" ref={parentRef} />
    </>
  )
}
 
  // 使用forwardRef将ref直接传递进去
 
let ForwardChild = forwardRef(Child);


十一、createContext: 跨越子组件,实现祖孙数据传输

我们在平常的开发中,如果遇到孙组件需要使用到爷爷组件的数据,我们一般会经过中间的父组件,一层一层向下传播,但有时中间的父组件又不需要用到这些给孙子组件的值,再或者中间父组件层级过多,或者逻辑业务变动,那就需要我们从头到尾进行一系列的修改,有点得不偿失,今天我们就来实现一个从爷爷组件,跨越父组件,直通孙子组件的方法:

Provider 顾名思义,参数的提供者,将需要传递的参数通过它来暴露出来 Consumer 参数的接收者,用在需要接收参数的组件

首先将引入以及创建context,并抛出 Provider、Consumer

/**
* 创建 context 并导出Provider、Consumer
* Provider:一般用于提供参数的组件
* Consumer:一般用在接受参数的组件
*  */

import React from 'react'
let { Provider, Consumer } = React.createContext()
export { Provider, Consumer }

导入Provider:父/祖父组件,要位于Consumer的上层组件:

import { Provider } from '@@/utils/context'

class Parent extends PureComponent {
  constructor(props) {
    super(props)
    this.state = { name: 'zhangsan' }
  }
  render() {
    return (
    <Provider value={this.state.name}>
        <div>
            <p>父组件定义的值:{name}</p>
                 <Son></Son>
        </div>
    </Provider>
)
  }
}
export default Parent

抛出:Consumer 子/孙组件,要位于Provider的下层的组件,首先测试第一层的儿子组件:

import { Consumer } from '@@/utils/context'

class Son extends PureComponent {
  constructor(props) {
    super(props)
  }
  render() {
    return (
    <Consumer>
        {
            name=>(
                <div>
                    <p>子组件接收的值:{name}</p>
                     <Grandson></Grandson>
                </div>
            )
        }
    </Consumer>
)
  }
}
export default Son

接下来测试第N层的孙子组件,写法同上:

import { Consumer } from '@@/utils/context'

class Grandson extends PureComponent {
  constructor(props) {
    super(props)
  }
  render() {
    return (
    <Consumer>
        {
            name=>(
                <div>
                    <p>孙子组件接收的值:{name}</p>
                </div>
            )
        }
    </Consumer>
)
  }
}
export default Grandson

当多个参数需要传递时,就直接放在一个对象({})里就可以,子组件收到的也时一个对象

父组件:

import React from 'react';
import { Provider } from '@xb/utils/context';
import Child from './Child1';

const App = () => {
  const state = false;
  const params = {
    name: 'zhangsan',
    age: 18,
    job: 'teacher',
  };
 
  return (
    <Provider value={{ params, state }}>
         
        子组件:<Child />
          
    </Provider>
  );
};

export default App;

Child:

import React from 'react';
import { Consumer } from '@xb/utils/context';

const Child1 = () => {
  return (
    <Consumer>
      {data => (
        <div>
          <p>
            子组件接收的值:{data.params.age}--{data.state}
          </p>
        </div>
      )}
    </Consumer>
  );
};

export default Child1;

总结:从上层往下传值,刚刚我们实验的是字符串,对象,其实还可传输数组。基本满足上层无需层层传递,下层也可拿到上层的数据。当我们的项目需要在多处使用context,那也时没有问题的,每个Consumer只会去找它的上级传来的值,而不会去兄弟组件寻找。

十二、React.Hooks函数式组件实践

上面剧透了很多次,这一讲来讲一下令人心动💓 的hooks。那么什么是 hooks ?

借用官网的解释:

HookReact 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

1. useState : 存储state,更新state

我们都知道hooks的一大亮点就是在不编写 class 的情况下使用 state,那么hooksstateclassstate的区别是什么呢?该怎么使用呢?

定义state

const [state, setState] = useState(initialState); 
  • 接收一个默认参数
  • 返回一个 state,以及更新 state 的函数

更新state

setState(newState);

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

我们还记得我们在之前学习的时候学过一个数字 ++ 的例子吗? 现在我们就用hooks的方式来实现:

import React, { useState } from 'react';
import { Button, Card } from 'antd';

const App = () => {
  const [state, setState] = useState<number>(0);

  return (
    <Card>
      <p>count: {state}</p>

      <Button
        type="primary"
        onClick={() => {
          setState(state + 1);
        }}
      >
        +
      </Button>
    </Card>
  );
};

export default App;

我们知道this.setState 是有第二个参数的,在第二个参数接收一个函数,在函数里可以拿到上一次的值,那么在useState 里可以传函数吗?

...

const [testState, setTestState] = useState('2');

// 我有一个点击事件testFn,点击会改变state

const testFn = () => {
    setTestState((prv) => {
      console.log('里面的:prv', prv);  // 1
      return prv + 100;
    });
    console.log('外面的prv:', testState);  // 2
};

大家来猜一下,首次点击,1 和 2 的位置哪个会先打印?

WeChatc88614db90485aa5c9559aef3b1055b9.png

结果是setState里面的console先被触发了,然后再向外,但state并没有进行计算。由此得出结论,setState 是异步的,如果是同步,那么第二个打印应该会得到里面计算的结果。

(😖 至于为啥会先执行里面的打印,这个我也没理解,万能的掘友们,知道的话评论区留言吧。)


那么我们再来猜一下,我再次点击,1 和 2 哪个会先执行?

WeChat306022595c5d1021dec8e422b0bc07c3.png

结论:先执行的外层打印,后执行的里层的打印,我理解是react 遇到setState之后会将它放到异步的队列中,等外面同步任务执行完后,再执行异步的任务。


关于一个讨论比较热烈的问题:setState 是同步还是异步的?


2. flushSync 提高state的优先级

在上面我们得知state是异步的,那么我们有没有提升更新优先级的方法呢? 当然,是有的:React-dom 提供了 flushSyncflushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。 示例:

...

 const handleTest = () => {
     
    setTimeout(() => {
      setCount(1);
    }, 0);
    
    setCount(2);
    
    ReactDOM.flushSync(() => {
      setCount(3);
    });
    
    setCount(4);
    
  };
  
  ...
  
  return (
  
    <>
      {console.log(count)}
    </>
    
  )

打印结果为: 3 、4 、1

  • 首先 flushSync setCount(3)设定了一个高优先级的更新,所以 23 被批量更新到 3 ,所以 3 先被打印。
  • 按照顺序更新为 4
  • 最后执行 setTimeout 中的 count = 1

flushSync补充说明

无论是在同步还是异步的情况下,如果发现了 flushSync ,就会立即执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,23 被批量更新到 3 ,所以 3 先被打印。

综上所述, React 同一级别更新优先级关系是:

flushSync 中的 setState  ->  正常执行上下文中 setState  ->  setTimeout / Promise 中的 setState。

3. useEffect : 监听参数变化,执行的函数

useEffect无论是否有参数的监听,React 都会等待浏览器完成画面渲染再延迟调用 。如果你不想让它首次就执行,可以考虑使用:useUpdateEffect -- 它是一个只在依赖更新时执行的 useEffect hook

  1. 默认情况下,useEffect 会在每轮组件渲染完成后执行。这样的话,一旦 useEffect 的依赖发生变化,它就会被重新创建。
 useEffect(() => {
    handleSubmit();
  }, []);
  1. 当有依赖的参数时,如下:tabkey发生变化后,会自动执行函数体内的handleSubmit方法
  useEffect(() => {
    handleSubmit();
  }, [tabKey]);
  1. 可以监听多个参数的变化,如下:当tabKey或者state任意一个发生改变,都会执行函数体内的handleSubmit方法:
  useEffect(() => {
    handleSubmit();
  }, [tabKey,state]);
  1. 当组件销毁时执行的一些内容,可以在useEffectreturn一个函数,这样return的函数体内的内容会在销毁时执行:
 useEffect(() => {
    handelColumns(tabVal || 'intention');
    // 需要是一个函数哦!!!
    return () => {
    // 以下内容只会在销毁的时候执行
      console.log(1);
    };
  }, []);
  1. 特殊场景:useEffect 的依赖项只可识别简单的数据类型,如果你的依赖是一个对象,可以使用useDeepCompareEffect
import useDeepCompareEffect from 'use-deep-compare-effect';

let defaultKeys = {a:{b:2}};

useDeepCompareEffect (()=>{

    console.log('defaultKeys');
},[defaultKeys])

未来可期:依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

4. useReducer : useState的替代方案,存储state,更新state

useReducer作为useState的替代方案。在某些场景下,useReducer 会比 useState 更适用,例如: state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。

接收参数:

  • 第一个参数是一个形如 (state, action) => newStatereducer函数;
  • 第二个参数是state的初始值;
  • 第三个参数是可选参数,值为一个函数,可以用来惰性提供初始状态。

返回值:

  • useReducer 返回一个数组,数组中包含一个 statedispathstate 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的函数。

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法,下面我们使用【 input ++、--】的例子来看一下它如何使用:

第一种:指定初始化,将初始 state 作为第二个参数传入

import React, { useReducer } from 'react';
import { Button } from 'antd';
import CardLayout from '@xb/layouts/CardLayout';

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};
const IntendedList = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <CardLayout>
      <p>count: {state.count}</p>

      <Button
        type="primary"
        onClick={() => {
          dispatch({ type: 'increment' });
        }}
        style={{ marginRight: 30 }}
      >
        +
      </Button>

      <Button
        type="primary"
        onClick={() => {
          dispatch({ type: 'decrement' });
        }}
      >
        -
      </Button>
    </CardLayout>
  );
};

export default IntendedList;

第二种:惰性初始化,你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

import React, { useReducer } from 'react';
import { Button } from 'antd';
import CardLayout from '@xb/layouts/CardLayout';

const initialCount = 0;

const init = initialCount => {
  return { count: initialCount };
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
};
const IntendedList = () => {
  const [state, dispatch] = useReducer(reducer, initialCount, init);

  return (
    <CardLayout>
      <p>count: {state.count}</p>

      <Button
        type="primary"
        onClick={() => {
          dispatch({ type: 'reset', payload: initialCount });
        }}
        style={{ marginRight: 30 }}
      >
        reset
      </Button>

      <Button
        type="primary"
        onClick={() => {
          dispatch({ type: 'increment' });
        }}
        style={{ marginRight: 30 }}
      >
        +
      </Button>

      <Button
        type="primary"
        onClick={() => {
          dispatch({ type: 'decrement' });
        }}
      >
        -
      </Button>
    </CardLayout>
  );
};

export default IntendedList;

页面的展现(gif图没做成,先看这个把😄 ):

5.useCallback 缓存回调函数,仅在某个依赖项改变时才会更新

类似useEffect,把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

参数:

  • 第一个参数是一个回调函数
  • 第二个参数是一个数组,存放回调函数的依赖,当这些依赖变化时,重新渲染
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

6. useMemo : 缓存变量,仅在某个依赖项改变时才会更新

把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

  const compute = useMemo(() => {
    const count = 222;
    // 这里使用 a或b 针对 count 做一些很复杂的计算
    // 只有当依赖的 a 或者 b 的值发生改变时才会重新计算,否则返回的是之前缓存的值
    return count * a * b;
  }, [a, b]);

7. 自定义hook

有些知识真的是需要一定的积累后,才能理解,并且要多多学而时习之,就比如这个自定义hook,现在终于能模仿着写出一些小例子了,分享给大家。


首先介绍一下,自定义 Hooks 允许创建自定义 Hook,只要函数名遵循以 use 开头,且返回非 JSX 元素,就是 Hooks 啦!自定义 Hooks 内还可以调用包括内置 Hooks 在内的所有自定义 Hooks

这是一个通过监听参数值来判断当前的登录状态的hook,先使用 手动的 + 、-来模拟一个动态监听状态,当数值大于0为在线状态,反之为离线状态,下面是代码:

hook页面:

import { useState, useEffect } from 'react';

/**
 * @param value {number}
 * @return {boolean}
 */
const useOnlineState = (value: number): boolean => {
  const [state, setState] = useState(false);

  useEffect(() => {
    if (value > 0 && Boolean(value)) {
      setState(true);
      return;
    }
    setState(false);
  }, [value]);

  return state;
};

export default useOnlineState;

调用页面:

import React, { useState } from 'react';
import { Button, Card } from 'antd';
import useOnlineState from './test';

const App = () => {
  const [count, setCount] = useState(0);
  const onlineState = useOnlineState(count);

  return (
    <Card>
      <p> {count}</p>
    
      <Button
        type="primary"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </Button>
      <Button
        style={{ margin: '0px 0px 20px 30px' }}
        type="primary"
        onClick={() => {
          setCount(count - 1);
        }}
      >
        -
      </Button>

      <p>当前状态{onlineState ? '在线' : '离线'}( 数值大于0为在线状态,反之为离线状态 )</p>
    </Card>
  );
};

export default App;

页面展现:

  • 离线:

  • 在线:

上面我们使用自定义组件导出了一个online状态,我们还可以使用导出函数方法,如下导出状态和显示隐藏的方法:

Hook:

import { useCallback, useState } from 'react';

export type IHook = [
  boolean,
  {
    hide: () => void;
    show: () => void;
  },
];

/**
 * 分享
 * @param {string} value
 * @return {IHook}
 */

const useShowinfo = (value: boolean): IHook => {
  const [state, setState] = useState<boolean>(value);

  const hide = useCallback(() => {
    setState(false);
  }, []);

  const show = useCallback(() => {
    setState(true);
  }, []);

  return [state, { hide, show }];
};

export default useShowinfo;


调用页面:

import React from 'react';
import { Button, Card } from 'antd';
import useShowinfo from './test';

const App = () => {
  const [state, { show, hide }] = useShowinfo(false);

  return (
    <Card>
      <Button
        type="primary"
        onClick={() => {
          show();
        }}
      >
        show
      </Button>
      <Button
        style={{ margin: '0px 0px 20px 30px' }}
        type="primary"
        onClick={() => {
          hide();
        }}
      >
        hide
      </Button>
      <p>{state ? 'show' : 'hide'}</p>
    </Card>
  );
};

export default App;

页面展现:

  • show

  • hide

十三、React.memo当子组件依赖的props未发生改变,缓存子组件,不进行渲染

把组件做了一份缓存props,改变的时候会对新旧props进行浅对比。我们平常开发项目时,一般情况下都本着一个模块负责一块儿业务,获取数据和逻辑一般放在父组件,渲染放在子组件。那么当我们的父组件页面数据发生变化后,无论是否传给子组件props,子组件就会重新渲染,当然这并不是我们想要的结果,我们期待的结果:只有当子组件依赖父组件当props发生变化后,再次渲染子组件,下面就开始我们的改造:

非必要不用,当代码或者组件没有任何问题,使用它会使我们组件性能更好,但使用它会增加很多计算过程,所以谨慎使用。useMemo和useCallback 逻辑接近,思路相同,对依赖项进行浅对比


未改造前

父组件:

import React, { useState } from 'react';
import useForm from 'rc-form-hooks';
import { Form, Input, Radio } from 'antd';
import Child from './Child';

const Parent = () => {
  const form = useForm();
  const { getFieldDecorator } = form;
  const [sourceType, setSourceType] = useState<number>(0);

  // 切换资源分类
  const onChange = e => {
    const { value } = e.target;
    setSourceType(value);
  };

  return (
    <Form>
      <Form.Item label="资源名称">
        {getFieldDecorator('title', {
          rules: [{ required: true, message: '请输入资源名称' }],
        })(<Input placeholder="请输入资源名称" maxLength={45} />)}
      </Form.Item>

      <Form.Item label="资源分类">
        {getFieldDecorator('type', {
          rules: [{ required: true }],
          initialValue: 0,
        })(
          <Radio.Group onChange={onChange}>
            <Radio value={0}>A类</Radio>
            <Radio value={1}>B类</Radio>
          </Radio.Group>,
        )}
      </Form.Item>
      <Child sourceType={sourceType} />
    </Form>
  );
};

export default Parent;

子组件:

import React from 'react';

interface IProps {
  sourceType: number;
}

const Child: React.FC<IProps> = props => {
  console.log('子组件渲染', props.sourceType);
  return <></>;
};

export default Child;

页面展示:

初次进入父组件页面:

当更改了非子组件的props后:

当更改了子组件的props后:


改造后

父组件同上,子组件如下改变:

import React from 'react';

interface IProps {
  sourceType: number;
}
const Child: React.FC<IProps> = React.memo(props => {
  console.log('子组件渲染', props.sourceType);
  return <></>;
});
export default Child;

改造后重新刷新父组件,查看子组件渲染情况,此时已经没有其他多余的渲染:


props升级到多层嵌套参数

上面我们试的都是单层的数据,React.memo默认做到浅比较,如果我们的参数是类似对象的多层嵌套,那就需要使用到它的第二个参数了

父组件只改动一下这里:

 <Child sourceType={{ a: { b: sourceType } }} />

重点在子组件,React.memo的第二个参数,我当前只想到了JSON.stringify,各位掘友有其他更优雅的方式可以告诉我哦😄:

import React from 'react';

interface IProps {
  sourceType: {
    a: {
      b: number;
    };
  };
}
const Child: React.FC<IProps> = React.memo(
  props => {
    console.log('子组件渲染', props.sourceType.a.b);
    return <></>;
  },

  (precProps, nextProps) => JSON.stringify(precProps) === JSON.stringify(nextProps),
);
export default Child;

当切换与子组件无关当数据后,子组件不再渲染:

只有当子组件依赖的props参数改变后,才会执行重新渲染

哈哈,是不是很神奇,我上面的例子是用在函数组件hooks中,那class组件,推荐使用PureComponent,相同的效果

import { PureComponent, Fragment,  } from 'react'

@Form.create()
class Search extends PureComponent {
  constructor(props) {
    super(props)
   
  }

  render() {
    return (
      <Fragment>内容</Fragment>
    )
  }
}

export default Search

十四、React.lazy 动态加载子组件,组件的懒加载

今天在逛掘金时,发现自己竟然漏了这个lazy的方法,说明自己看的还是不够多,还需要需要需要努力啊!!💪


今天就来说说这个懒加载,我们开发的过程中,父子组件嵌套的情况一般比较频繁,但有时我们的子组件是需要在一定场景才会显示的,那么我们就尽量不让它渲染,减少父组件页面的渲染的负载。

React.lazy必须通过调用动态的import()加载一个函数,此时会返回一个Promise, 并解析(resolve)为一个带有包含React组件的默认导出的模块。 ---- Reactjs官网

这里还需要说明两点:

  1. 需要使用标识符Suspense 来包裹子组件,以此让react 得知哪些内容是需要懒加载的;
  2. Suspense的属性fallback不能为空,fallback属性存放等待子组件加载时呈现出的元素,如:loading 图标;

我先给大家上一下代码:

父组件:

import React, { useState, lazy, Suspense } from 'react';
import { Button } from 'antd';
const Test = lazy(() => import('./test'));

const App = () => {
  const [visible, setVisible] = useState<boolean>(false);

  return (
    <>
      <Button
        type="primary"
        onClick={() => {
          setVisible(true);
        }}
      >
        切换
      </Button>
      {visible && (
        <Suspense fallback={<div>Loading...</div>}>
          <Test />
        </Suspense>
      )}
      
    </>
  );
};

export default App;

子组件(一个普通的子组件):

import React, { useEffect } from 'react';

const TestView = () => {
  useEffect(() => {
    console.log('子组件的渲染');
  }, []);

  return <div>我是测试的页面</div>;
};

export default TestView;

正常的父组件加载完,可以看到我们的子组件并没有渲染:

当我们点击“切换”,子组件异步加载出来了:

参考文章:

  1. 如何使用React.lazy和Suspense进行组件延迟加载
  2. react代码分割

十五、react 18 更新了哪些内容?

react18版本使用的是 createRoot 来创建react,这个版本又叫: Concurrent 模式

1.flushSync 同步更新state

我们都知道setState是异步更新的,那么如果我们想用到最新的值,在18之前我们是可以使用setTimout 来包裹一层,可是在18之后react 将所有的任务都统一为异步任务,所以也就是说在setTimout 中也是没办法再拿到最新的值了,为了处理这个问题,react 新增了一个叫flushSync的api,支持同步更新state这个在上面的示例也有介绍如何使用,这里就不在赘述了

2. createRoot开启react 18的新特性

通过设置createRoot 开启react 18Concurrent 模式,然后就可以使用react18新特性了

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
class Test extends React.Component {
  render() {
    return (
    <div>hellow</div>
    );
  }
}
ReactDOM.createRoot(
  document.getElementById('root')
).render(<Test/>);

3. useTransition降低渲染优先级

可以用来降低渲染优先级。分别用来包裹计算量大的 functionvalue,降低优先级,减少重复渲染次数。

举个例子:

搜索引擎的关键词联想。一般来说,对于用户在输入框中输入都希望是实时更新的,如果此时联想词比较多同时也要实时更新的话,这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差,这并不是我们想要的结果。

我们将这个场景的状态更新提取出来:一个是用户输入的更新;一个是联想词的更新。这个两个更新紧急程度显然前者大于后者。

以前我们可以使用防抖的操作来过滤不必要的更新,但防抖有一个弊端,当我们长时间的持续输入(时间间隔小于防抖设置的时间),页面就会长时间都不到响应。

startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新。即使用户长时间输入最迟 5s 也会更新一次,官方还提供了 hook 版本的 useTransition,接收传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的pending 状态和startTransition函数。

WeChate41973e7f5a2ea49d511f8b9d528a3e4.png


4. useId 解决服务端渲染唯一ID问题

用于解决SSR时客户端与服务端难以生成统一的ID 的问题

const id = useId();

当一个组件,同时会被服务端和客户端渲染时,我们就可以使用 useId 来创建当前组件的唯一身份。

function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react"/>
    </>
  );
};

参考链接


5. 异步更新

Concurrent 模式下,无论是写在setTimeout 里面的 setState 还是 写在外面的 setState 都是异步去更新的。结论:不论是在合成事件中,还是在宏任务中,都是会合并更新。


写到此处,并没有结束哦!接下来我还会持续追加,看文章的小伙伴们可以添加一下关注哦!

如果你对我对文章感兴趣或者有些建议想说给我听👂,可以留言或者给我发邮件

邮箱:christine_lxq@sina.com

如果亲感觉我的文章还不错的话,那就添加一下关注吧!