【学习前端第七十七课】函数式组件

函数式组件

函数式组件其实一开始就与类式组件被React共同推出了,但是因为函数式组件没有state也没有生命周期函数,所以导致实际的应用价值不如类式组件,所以一直都无法被很好的推广开来,但是自从React16.8版本开始推出了hook函数之后,其应用价值得到了很大的提升,逐渐开始被更多开发者青睐

函数式组件与类式组件的主要区别点:

  1. 没有state状态,但是提供了很多的hook函数,让函数式组件可以使用state,并且数据渲染更简洁
  2. 没有this,因为函数内的this并不会指向函数自身,所以不需要向类式组件一样通过this指向自身来调用自身的方法和状态
  3. 没有生命周期函数,但是可以依靠hook函数模拟出来

新建一个Func.js,通过我们之前安装的VScode插件的快捷指令rfc,快速创建一个函数式组件的基本代码结构

import React from 'react'

export default function Func() {
  return (
    <div>
      
    </div>
  )
}
提问:

我们在组件内并没有调用React对象,那么为什么要导入React对象呢?(对之前内容的复习)

原因:

因为return中的div标签是react的虚拟标签,是需要通过React.createElement方法创建出来的

useState函数

**功能:**useState函数可以在函数式组件内创建state状态数据进行使用

**参数:**传入的实参会作为当前组件的内部状态的值使用

**返回值:**该方法会返回一个数组,这个数组中包含两个数组元素,第一个是状态值,第二个是修改状态值的方法

import React, { useState } from 'react'  //从react中导入useState函数

export default function Func() {
    //通过数组解构取值的方式,分别取出useState返回的数组中的数组元素
    //num就是状态值
    //setNum就是修改状态的方法
    const [num,setNum] = useState(10)
    //制作一个修改状态值的方法
    const changeNum = () => {
        setNum(num+1)
    }
    return (
        <div>
            <h1>{num}</h1>
            <button onClick={changeNum}>按钮</button>
        </div>
    )
}

代码分析:

这里我们主要看解构取值的这句话,num里面装的就是创建的状态值,setNum里面装的就是修改num的方法,因为是数组解构,所以没有属性只有下标,上面代码中的num和setNum可以我们自己自定义名称

在虚拟标签中调用的时候,因为没有this所以如果是渲染数据就直接调用num即可,如果是调用方法,直接写方法名即可

补充:

如果我们不想专门声明一个方法来调用setNum的话也可以直接在onClick事件上绑定一个匿名函数

<button onClick={() => setNum(num + 1)}>按钮</button>

注意事项:

useState函数只能在组件内的最外层使用,比如我们如果想做一个if判断决定是否执行useState,或者将useState写在函数式组件的某一个方法体内的话是会报错的(不要在组件内部的{ }内调用useState)

函数式组件父子传值

在Func.js中再创建一个函数式组件Child,实现父传子

import React, { useState } from 'react'
//创建子组件Child设置形参props接收父组件传入的数据与方法
function Child(props){
    return (
        <>
            <h1>{props.num}</h1>
            <button onClick={props.changeNum}>按钮</button>
        </>        
    )
}

export default function Func() {
    const [num,setNum] = useState(10)
    const changeNum = () => {
        setNum(num+1)
    }
    return (
        <div>
            <Child num={num} changeNum={changeNum}></Child>
        </div>
    )
}

代码分析:

父子传值的方式与之前的类式组件的逻辑基本一致,所有传入子组件的数据与方法都可以在子组件中的props中调出使用,只不过现在必须要在函数式组件的参数中设置形参props,不然无法接收父组件传入的数据方法

使用带参的修改数据方法

在上面我们通过执行useState解构获取了一个setNum方法来修改num数据,然后我们又自定义了一个changeNum方法来调用setNum实现了一个对num数据进行递增运算的效果,

现在我们想要实现通过调用changeNum传入一个实参,然后根据实参值来决定num 的修改结果,大致的代码示意如下

注意:以下写法是错误的,浏览器会报错,只是进行一个示意
const changeNum = val => {
	setNum(num + val)
}
//当我调用changeNum的时候,根据传入的实参来决定num加多少

报错信息:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

这里主要说我们重复渲染的次数太多了成了一个无限循环

原因:
<button onClick={changeNum(10)}>按钮</button>

原因就出在调用changeNum的时候写在后面的小括号,首先,我们先明确两个点

  1. 在React组件的标签结构中使用的{ }是用来执行js语句的
  2. 我们在调用函数的时候后面写的小括号可以理解成是一个立即执行符

基于以上两点当浏览器开始渲染组件的时候,会对大括号内的js语句进行执行,而安装上面的写法,我需要在调用函数的时候通过()给函数传实参,而也就因为这个()导致现在实际上并不是把changeNum函数的函数体赋值给click事件,等待点击之后再执行,而是还没等到点击,就已经开始直接执行 setNum(num + val)这句话了,而这里执行的setNum方法,其内部其实是依靠setState方法来修改的数据,而setState是个异步方法,就有可能出现一种情况,因为要修改的数据还没有出现,找不到需要修改的数据,setState就会反复找组件要数据,导致组件被反复渲染,而react对组件重复渲染的次数有限制,导致了报错

解决方案:

方案1:采用闭包函数

//第一种闭包写法
import React,{useState} from 'react'

export default function Effect() {
    const [num,setNum] = useState(10)
    const changeNum = (val) => setNum(num + val)
    return (
        <div>
            <h1>{num}</h1>
            <button onClick={() => changeNum(10)}>按钮</button>
        </div>
    )
}

setNum也可以传入一个回调 setNum(prev => prev + val) ,prev代表上一次的数据(旧值)

//第二种闭包的写法
import React,{useState} from 'react'

export default function Effect() {
    const [num,setNum] = useState(10)
    const changeNum = (val) => {
        return () => setNum(prev => prev + val)
    }
    return (
        <div>
            <h1>{num}</h1>
            <button onClick={changeNum(10)}>按钮</button>
        </div>
    )
}

两种闭包的写法区别就在于需要包在setNum外层的函数声明是写在标签结构的{ } 里,还是直接在组件内先写好

方案2:通过bind方法

import React,{useState} from 'react'

export default function Effect() {
    const [num,setNum] = useState(10)
    const changeNum = (val) => setNum(num + val)
    return (
        <div>
            <h1>{num}</h1>
            <button onClick={changeNum.bind(this,10)}>按钮</button>
        </div>
    )
}

第二种方案本质上也是一种闭包的表现,因为bind会给原函数外再包装一层函数

useEffect函数

**功能:**useEffect函数的主要功能就两个

  1. 用来表示部分声明周期函数效果
  2. 监听某一个数据的变化

**参数:**useEffect接收两个参数

  1. 回调函数:作为监听器使用时,监听某个状态值变化时执行
  2. 数组:可以在数组元素中设置希望监听的状态

注意:

该方法有一定的特殊性,它会根据传入的实参的一些变化形成一些不同的情况

新建一个Effect.js创建函数式组件来表示情况

useEffect函数的几种情况:

1、作为监听器使用时,传入的第二个参数的数组中设置了具体要监听的状态,该监听会被立即执行

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

export default function Effect() {
    const [num,setNum] = useState(10)
    const changeNum = () => {
        setNum(num+1)
    }
    useEffect(() => {
        console.log("useEffect被触发了")
    },[num])
    return (
        <div>
            <h1>{num}</h1>
            <button onClick={changeNum}>按钮</button>
        </div>
    )
}

情况分析:

这种情况下会在组件加载完之后立即执行监听,相当于vue3中的watchEffect,然后我们通过调用修改num的方法可以反复触发监听,但是如果我们有其他数据因为没有在数组元素中设置,所以无法监听

2、传入的第二个参数是一个空数组,表示没有监听任何状态,但是监听还是会立即执行一次,所以,这种情况下我们可以理解成就相当于componentDidMount

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

export default function Effect() {
	//......
    useEffect(() => {
        console.log("useEffect被触发了")
    },[])
    //......
}

情况分析:

因为情况1中我们也说了useEffect作为监听器使用就和vue3中的watchEffect一样,无论是否监听到数据变化都会执行一遍回调函数,所以当我们什么都不监听的时候所立即执行的这次回调函数就可以当成是挂载阶段的componentDidMount声明周期函数来看待

3、不写第二个参数的数组,表示只要是useState生成的数据发生变化时就会执行,相当于监听组件中所有数据的变化,这种情况类似于声明周期中的更新阶段update

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

export default function Effect() {
	//......
    useEffect(() => {
        console.log("useEffect被触发了")
    })
    //......
}

情况分析:

数据发生了变化我们就可以理解成数据进行更新,所以在这种情况下触发的回调函数我们就可以当作更新阶段执行的componentDidUpdate生命周期函数

4、在回调函数中再返回一个函数,这个返回的函数就相当于组件卸载时执行的生命周期函数componentWillUnmount

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

export default function Effect() {
	//......
    useEffect(() => {
        console.log("useEffect被触发了");
        return () => {
        	console.log("我被卸载了")
        }
    })
    //......
}

情况分析:

卸载阶段就只有一个生命周期函数,所以我们可以很明确的直接说明回调函数中return的函数就是componentWillUnmount生命周期函数,我们可以通过之前测试componentWillUnmount的方式,在入口index.js文件中设置一个setTimeout定时3秒后重新渲染一个新的组件到root上来查看效果

useContext函数

之前我们在类式组件中可以通过context上下文环境进行跨级传值,在函数式组件中,需要通过以下两个方法来实现

  1. createContext() 创建一个上下文环境空间
  2. useContext() 从创建好的上下文空间中获取数据值

举例:

import React,{createContext,useContext,useState} from 'react' //导入需要使用的hook函数

//第一步:创建一个上下文环境空间,这个函数的执行会返回一个组件,所以我们的常量名会作为组件名使用,首字母大写
const ContextMsg = createContext();
const Son = () => {
    //第三步:在后代组件中通过useContext从执行的上下文环境空间ContextMsg中调出值使用
    const obj = useContext(ContextMsg)
    return (
        <div>
            <h1>{obj.userName} | {obj.age}</h1>
        </div>
    )
}

export default function Context() {
    const [str,setStr] = useState({
        userName:"zhangsan",
        age:18
    })
    return (
        //第二步:在父组件中调用上下文环境组件中的Provider提供器,通过value属性向上下文环境中的提供器传入数据
        <ContextMsg.Provider value={str}>
            <div>
                <Son></Son>
            </div>
        </ContextMsg.Provider>       
    )
}

代码分析:

上面实现Context跨级传值的逻辑大致可以分为以下几步操作

  1. 通过createContext创建一个上下文环境空间,这个空间本质上是一个组件,这个组件内部也有一个和react-redux一样的一个提供器,我们通过向这个提供器注入数据,从而让整个上下文环境中的内层子组件都可以调用这个数据,这个创建过程不能在组件内部
  2. 在父组件的标签结构的最外层使用组件中的提供器,并通过该组件的value属性向上下文环境中传入数据
  3. 在子组件中通过useContext来获取在父组件中传入到上下文环境中的数据来进行调用
获取上下文环境中数据的另外一种方式

除了上面例子中通过useContext函数获取之外,我们也可以直接通过createContext创建的上下文环境组件来获取

举例:

//子组件Son
const Son = () => {
    return (
        <ContextMsg.Consumer>
            {
                obj => 
                <div>
                    <h1>{obj.userName} | {obj.age}</h1>
                </div>
            }
        </ContextMsg.Consumer>
    )
}

代码分析:

  • 在需要调用上下文数据的子组件的标签结构最外层套上 <ContextMsg.Consumer></ContextMsg.Consumer> ,然后在其内部使用 { } 执行一个函数,函数的形参会自动注入传入到上下文环境中的数据
  • 子组件内部的标签结构全部都写在上下文环境组件内部执行的函数的返回值上,在这里我们就可以调用上下文环境中的数据进行渲染
将方法传入上下文进行传递

再上面的例子中我们将一个对象传入上下文环境,然后调用,即然能传对象,那么我们自然也可以在对象内设置方法,让方法随着对象一并传入到上下文环境中进行调用

举例:我现在在父组件中通过useState创建状态并解构获取状态值和修改状态的方法,现在我把状态值和修改状态的方法传入到上下文环境中,在不同的子组件中分别调用状态值和修改状态的方法

import React,{createContext,useContext,useState} from 'react'
//创建上下文环境
const ContextMsg = createContext();
export default function Context() {
    //父组件中创建状态,分别是str状态值(对象),setStr修改状态的方法
    const [str,setStr] = useState({
        userName:"zhangsan",
        age:18
    })
    return (
        //将状态和方法通过ES6的语法包打成一个匿名对象,传入到上下文环境中
        <ContextMsg.Provider value={{str,setStr}}>
            <div>
                <Son></Son>
                <Btn></Btn>
            </div>
        </ContextMsg.Provider>       
    )
}
 //在Son组件中只调出数据进行渲染
const Son = props => {
    return (
        <ContextMsg.Consumer>
            {
                ({str}) => 
                <div>
                    <h1>{str.userName} | {str.age}</h1>
                </div>
            }
        </ContextMsg.Consumer>
    )
}
//在Btn组件中调出方法并修改原数据
const Btn = props => {
    return (
        <ContextMsg.Consumer>
            {
                //在子组件中通过解构取值的方式将上下文环境中的数据和方法调出
                ({str,setStr}) => <button onClick={() => setStr({
                    ...str,
                    userName:"lisi"
                })}>按钮</button>
            }
        </ContextMsg.Consumer>
    )
}

useReducer函数

这里的这个函数名中带有reducer,可能会让大家认为是之前redux中的reducer函数,但是这里的useReducer并不是redux中的函数,所以与redux无关,但是他们的功能是和用法都是非常相似的

useReducer 是一个数据管理器,与redux的功能基本一样,它可以接收两个参数

参数1:一个回调函数,这个传入的回调函数其内部语法结构与作用,与我们之前在redux中学习的reducer函数一样

参数2:传入一个对象,这个对象就是一个state状态对象,也就是需要管理的数据

举例:

import React, { useReducer } from 'react'

export default function Reducer() {
    //通过useReducer创建一个数据管理器,并把修改方法和需要管理的数据作为参数传入
    //改方法会返回一个数组,我们通过解构取值获取到管理器中的数据state和派发修改行为的dispatch方法
    let [state,dispatch] = useReducer(stateReducer,{num:10});
    //创建一个修改num的方法,在内部创建一个行为action,并将该行为派发给数据管理器
    const changeNum = () => {
        let action = {
            type:"num",
            value:10
        }
        dispatch(action)
    }
    return (
        <div>
            <h1>{state.num}</h1>
            <button onClick={changeNum}>按钮</button>
        </div>
    )
}

//创建一个与redux中一样的根据dispatch派发的行为来决定如何修改数据的reducer方法
//内部的语法结构与redux中的reducer导出的方法一样
const stateReducer = (state,action) => {
    let newState = JSON.parse(JSON.stringify(state));
    switch (action.type) {
        case "num":
            newState.num += action.value;
            break;
    
        default:
            break;
    }
    return newState
}

以上我们可以看到,基本上整个过程就是在完全模仿redux的写法,所以作用用法都基本上是一样的

练习:

使用useContext 将被useRedcuer所管理的数据和派送修改行为的方法分别传递给两个不同的子组件,来完成数据的修改

import React, { useReducer,createContext } from 'react'
//创建上下文环境
let NumContext = createContext();
//子组件head
const Head = () => {
 return (
     //在子组件中调用上下文环境中的num
     <NumContext.Consumer>
         {
             ({state}) => <h1>{state.num}</h1>
         }
     </NumContext.Consumer>
 )
}
//子组件Btn
const Btn = () => {
 return (
     //在子组件中调用修改方法
     <NumContext.Consumer>
         {
             ({changeNum}) => <button onClick={changeNum}>按钮</button> 
         }
     </NumContext.Consumer>
 )
}

export default function Reducer() {
 let [state,dispatch] = useReducer(stateReducer,{num:10});
 const changeNum = () => {
     let action = {
         type:"num",
         value:20
     }
     dispatch(action)
 }
 return (
     //将数据和修改方法传入到上下文环境中
     <NumContext.Provider value={{state,changeNum}}>
         <Head></Head>
         <Btn></Btn>
     </NumContext.Provider>
 )
}

const stateReducer = (state,action) => {
 let newState = JSON.parse(JSON.stringify(state));
 switch (action.type) {
     case "num":
         newState.num += action.value;
         break;

     default:
         break;
 }
 return newState
}

useRef函数

useRef可以获取函数组件中的DOM,其功能与之前在类组件中使用React.createRef() 方法一致

使用方式如下

import React, { useRef } from 'react'

const Hook = () => {
    let inputDOM = useRef(null);
    const changeVal = () => {
        console.log(inputDOM.current.value);
    }
    return (
        <div>
            <input type="text" ref={inputDOM} />
            <button onClick={changeVal}>按钮</button>
        </div>
    )
}

export default Hook

代码分析:

useRef的用法和vue3中的Ref方法使用方式类似,先通过useRef创建一个空的ref对象,然后通过React组件的提供的API,ref进行赋值操作,将对应的DOM获取

注意:

原生的DOM在Ref对象的current属性中

useMemo函数

该函数主要使用解决函数组件中的重复渲染的性能损耗问题

原因:

  • 函数组件是没有 shouldCompnentUpdate 这个生命周期函数的,所以也就无法实现之前我们在类组件中讲过的关于使用shouldComponentUpdate来实现性能优化的操作
  • 函数组件中是没有所谓的mount和update两个阶段状态的,这就意味着函数组件在每次调用时都会把内部所有的逻辑都执行一遍,这样就带来了巨大的性能损耗

举例:以下是没有使用useMemo的情况

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

const Memo = () => {
    const [obj,setObj] = useState({
        filterStr:"",  //筛选条件用字符串
        inputStr:"",
        list:[
            {
                id:1,
                userName:"zhangsan1"
            },
            {
                id:2,
                userName:"zhangsan2"
            },
            {
                id:3,
                userName:"zhangsan3"
            }
        ]
    });
    //这是只修改obj中str属性值的方法
    const changeStr = (e) => {
        setObj({
            ...obj,
            inputStr:e.target.value
        })
    }
    //这个方法是把obj中list数组做一个筛选获取一个经过筛选之后的新数组
    //我们实际渲染到页面上的是这个经过筛选的新数组
    const changeList = (str) => {
        return obj.list.filter((item) => {
            if(item.userName.includes(str)){
                return item
            }
        })
    }
    let newList = changeList(obj.filterStr);
    return (
        <div>
            {/* 在input中修改obj的str值 */}
            <input type="text" onChange={(e) => changeStr(e)} value={obj.str} />
            {
                //渲染经过筛选的新数组
                newList.map(item => <div key={item.id}>{item.userName}</div>)
            }
        </div>
    )
}

export default Memo

代码分析:

在上面的代码中,会出现一个情况,我们实际只是修改了obj中的inputStr,但是因为这是一个函数组件,基于上面我们说过的函数组件没有刷新和挂载阶段的区别,所以每一次函数组件中的数据更新都会导致整个函数组件被重新渲染一遍,其中就会把 changeList 这个用于筛选obj中list数组的方法再重新执行一遍,但是我们的list数组并没有改变,所以就导致了此次changeList的执行成了一个无用执行,筛选出来的新数组与之前的无异,但是这也会被算作一次list的数据更新,所以又会导致下面重新渲染一次

基于上面的情况,我们在类数组上可以通过 shouldCompnentUpdate 这个生命周期函数来做数据更新的前后对比来决定是否执行数据更新,但是函数组件中又没有,所以就造成了重复渲染的性能损耗问题

这个时候,我们可以把changeList的调用放在useMemo当中,并将obj中的filterStr作为依赖项,只有当依赖项发生变化的时候,才会执行changeList重新开始筛选,而filterStr我们是作为筛选条件传入到changeList中的,只要筛选条件变了,自然筛选结果也要随之发生变化,这样重新筛选的执行才会是有用的

以上代码做如下修改

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

const Memo = () => {
    //。.....
    //这个方法是把obj中list数组做一个筛选获取一个经过筛选之后的新数组
    //我们实际渲染到页面上的是这个经过筛选的新数组
    const changeList = (str) => {
        return obj.list.filter((item) => {
            if(item.userName.includes(str)){
                return item
            }
        })
    }
    //把changeList的执行放入到useMemo中,并且把筛选条件设置为依赖项
    let newList = useMemo(() => {
        return changeList(obj.filterStr);
    },[obj.filterStr])
    return (
        <div>
            {/* 
            在input中修改obj的inputStr值,因为inputStr并不是newList的依赖项,所以修改了也不会重新触发newList的渲染 ,而			 如果这里我们changStr修改的是filterStr的话情况就完全相反 
            */}
            <input type="text" onChange={(e) => changeStr(e)} value={obj.inputStr} />
            {
                //渲染经过筛选的新数组
                newList.map(item => <div key={item.id}>{item.userName}</div>)
            }
        </div>
    )
}

export default Memo

代码分析:

useMemo传入两个参数

**参数1:**传入一个回调函数,需要缓存的数据通过该回调函数return执行,这里我们可以理解成把第一次加载执行的筛选结果缓存下来,当依赖项发生变化的时候才进行第二次筛选的执行

**参数2:**依赖项,可以看作一个触发条件,当依赖条项发生变化的时候才会去执行参数1的回调函数,如果不变就永远不会执行参数1的回调函数,那么组件中渲染的数据就永远不会发生更新,也就不会进行重复渲染

useCallback函数

useCallback可以看做是useMemo的优化,useMemo是对数据进行缓存,而useCallback是对函数进行缓存,按照上面useMemo的例子来理解,在useMemo中缓存的数据是通过changList方法的调用进行筛选后的来的,那么,useCallback可以直接就把这个changeList函数本身缓存起来

该函数的使用方式与useMemo一样,传入两个参数

参数1:回调函数,把需要缓存的方法可以直接声明在这个回调函数体内(形成相当于闭包的一个形式)

参数2:依赖项,把缓存的函数中使用的数据作为依赖项,当作为依赖项的数据发生变化的时候来决定是否重新调用缓存的方法

注意事项:

因为这里直接缓存的是一个函数,而函数体内可能会调用多个依赖,所以尽量把函数中所用到的所有依赖尽可能的在第二个参数的依赖项中写全

举例:把上面的useMemo的例子做点修改

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

const Memo = () => {
    //......
    //这是只修改obj中str属性值的方法
    const changeStr = (e) => {
        setObj({
            ...obj,
            inputStr:e.target.value
        })
    }
    //把之前的useMemo例子上的changeList方法直接作为useCallback的第一个参数传入直接把方法本身缓存下来
    let newList = useCallback((str) => {           
           return obj.list.filter((item) => {
                if(item.userName.includes(str)){
                    return item
                }
            })
        },[obj.filterStr])
    return (
        <div>
            <input type="text" onChange={(e) => changeStr(e)} value={obj.inputStr} />
            {
                //渲染经过筛选的新数组
                //注意:useCallback返回的是缓存的函数而不是执行结果,所以在调用的时候需要带小括号执行
                newList(obj.filterStr).map(item => <div key={item.id}>{item.userName}</div>)
            }
        </div>
    )
}

export default Memo

代码分析:

其实useCallback与useMemo的性质基本上是一致的,把作为第一个参数传入回调函数返回出来,不同的是在于useMemo返回的是回调函数执行的结果,而useCallback返回的是回调函数本身,所以我们可以认为

useCallback(fn,deps)相当于useMemo(()=> fn,deps)

关于useMemo与useCallback搭配React.memo()的应用

上面的例子主要只是介绍了关于这两个hook函数的语法和简单用法,其还有一个更重要的应用场景,模拟替代我们在类组件中使用shouldComponentUpdate对比state和props更新前后的对比来判断是否需要重新渲染组件来对程序性能进行优化

在react的一般规则中,只要父组件的某一个状态改变,父组件就会重新渲染,在这个重新渲染的过程中,父组件下面所有的子组件不论是否使用了该状态,也都会进行重新渲染。显然,对于没有用到被改变的那个状态的子组件来说,重新渲染是完全没有必要的。

举例:

import React, { useState } from 'react'

const Memo = () => {
    const [strFather,setStrFather] = useState("father");
    const [strSon,setStrSon] = useState("son");
    return (
        <div>
            <h2>我是父组件的strFather:{strFather}</h2>
            <button onClick={() => setStrFather(val => val + "Component")}>修改strFather</button>
            <Son strSon={strSon}></Son>
        </div>
    )
}

const Son = (props) => {
    return ( //在父组件中并没有修改传入到strSon,但是子组件依然还是重新渲染了
        <div>
            {console.log("子组件更新了")}
            <h2>我是子组件的strSon:{props.strSon}</h2>
        </div>
    )
}

export default Memo

代码分析:

上面的例子中,父组件Memo在自己的内部修改了strFather状态,而传入到子组件Son中的strSon并没有修改,通过观察浏览器控制台的打印结果,我们发现当父组件点击按钮触发修改strFather的时候,子组件还是又被渲染了一遍,这个时候的子组件重新渲染毫无意义,只是在无端的消耗浏览器性能

这个时候我们就需要使用到React中的memo方法将子组件包装成一个可以被缓存的组件

举例:

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

const Memo = () => {
    const [strFather,setStrFather] = useState("father");
    const [strSon,setStrSon] = useState("son");
    let newStrFather = useMemo(() => strFather,[strFather])
    return (
        <div>
            <h2>我是父组件的strFather:{newStrFather}</h2>
            <h2>我是父组件的strSon:{strSon}</h2>
            <button onClick={() => setStrFather(val => val + "Component")}>修改strFather</button>
            <Son strSon={strSon}></Son>
        </div>
    )
}

const Son = React.memo((props) => {
    return (
        <div>
            {console.log("子组件更新了")}
            <h2>我是子组件的strSon:{props.strSon}</h2>
        </div>
    )
})

export default Memo

代码分析:

我们现在将Son组件作为参数传入到React.memo方法中,将Son组件包装成了一个缓存组件,这里其实内部执行的props的更新前后对比,父组件传入的数据改变了就重新渲染子组件,反之

同时我们在父组件Memo中还可以通过useMemo缓存组件自己的内部状态实现了state更新前后的对比,从而复刻了shouldComponentUpdate中所实现了组件性能优化,同时从功能性的角度来看还更丰富些,因为shouldComponentUpdate只能返回true或false来决定是否更新,而上面的写法可以返回的东西可以更加丰富

注意事项:

被React.memo()包裹的组件内部只对比props

关于React.memo方法的问题

因为被memo保护的组件在对比props更新前后变化的时候执行的是浅比较,所以,当传入的数据是一个引用类型的时候,即使props改变了也不会重新渲染

举例:

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

const Memo = () => {
    const [strFather,setStrFather] = useState("father");
    const [strSon,setStrSon] = useState([1,2,3,4]);
    //制作一个修改strSon数组的方法
    const changeStrSon = () => {
        setStrSon((val) => {
            val.push(5); //这里千万不要直接返回push方法,因为push的返回值是length
            return val
        })
        console.log(strSon)
    }
    let newStrFather = useMemo(() => strFather,[strFather])
    return (
        <div>
            <h2>我是父组件的strFather:{newStrFather}</h2>
            <h2>我是父组件的strSon:{strSon}</h2>
            <button onClick={() => setStrFather(val => val + "Component")}>修改strFather</button>
            <button onClick={changeStrSon}>修改strSon</button>
            <Son strSon={strSon}></Son>
        </div>
    )
}

const Son = React.memo((props) => {
    const {strSon} = props
    return (
        <div>
            <h2>我是子组件渲染的数组</h2>
            {
                strSon.map(item => <div key={item}>{item}</div>)
            }
            {console.log("子组件更新了")}
            
        </div>
    )
})

export default Memo

代码分析:

我们通过点击执行了changeStrSon方法用来修改strSon的数组,但是我们可以看到我们是通过setStrSon 修改,并且也在控制台打印看到数据也确实修改了,但是子组件却没有重新渲染

原因:

push方法是对原数组直接进行修改,也就是说直接修改的是原数组在内存堆中的储存结果,而memo对比的是栈,而更新前后的栈内储存的堆地址都指向的是同一个内存堆,所以栈里面储存的内存地址自然也都是一样的,自然就不会更新

解决方案:

按照上面所说的原因,如果想更新就需要更新前后对比的栈中映射的堆地址不同,而想要堆地址不同就需要重新再新建一个新的堆空间,所以我们可以使用能够把修改的结果作为一个新数组返回出来的方法,或者直接就做一个新数组出来即可

写法1:

//修改改变strSon的方法
const changeStrSon = () => {
 setStrSon([...strSon,5])
 console.log(strSon)
}

写法2:

//或者直接按修改按钮上直接调用
<button onClick={() => setStrSon([...strSon,5])}>修改strSon</button>

扩展内容

在没有使用memo方法缓存组件的时候,就算props的值不变,也会造成子组件re-render其实主要在于React所使用的判断props变与不变的方式上

react其实判断的并不是props的内容,而是通过 === 判断组件作为一个reactDOM是否前后一致

以上面 <Son strSon={strSon}></Son> 这句话为例,我们之前就讲过react的jsx语法中使用的虚拟标签,会在执行中转换成 React.createElement(Son,{strSon:[1,2,3,4]}) 创建一个ReactDOM,但是每当父组件状态发生变化时,都会创建一个新匿名对象 {strSon:[1,2,3,4]} ,也就是说每次都会新键一个堆,虽然堆(对象)内的数据都没有变化,但是引用的堆地址变了,所以导致了子组件的re-render

而memo方法的介入改变了比较方式,简单来说就是只判断props的内容,实现了类似我们之前在shouldComponentUpdate中props的比较功能,而之所以能实现在memo方法的第二个参数compare ,这个参数是自动注入的,我们可以不去动它,但是注意,这个方法内部也实现的是 === 全等,虽然只比较props了,但是如果props中传入的是引用数据类型的数据也还是会有上面我们提到的关于memo方法问题

Fragment组件替代空标签使用

之前我们在讲组件内的标签结构的时候,我们有说过React组件只能有一个根标签,同时React还支持使用一个空标签作为根标签,如下

return (
	<>
	</>
)

但是空标签有一个问题就是不能设置标签属性,所以当我们需要使用空标签作为根标签并且还要添加标签属性的时候可以使用Fragment组件替换空标签来使用

import {Fragment} from 'react'

return (
	<Fragment className="top">
		.......
	</Fragment>
)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值