Skip to content

函数组件

shell
1. 组件名首字母必须大写. 因为react以此来区分组件元素/标签 一般元素/标签
2. 组件内部如果有多个标签,必须使用一个根标签包裹.只能有一个根标签
3. 必须有返回值.返回的内容就是组件呈现的结构, 如果返回值为 null,表示不渲染任何内容
4. 会在组件标签渲染时调用, 但不会产生实例对象(this->undefined),  不能有状态,生命周期(16.4以前)
jsx
 
function App() {
  // return null
  return <div>App</div>
}

// 函数名就是组件名
ReactDom.render(<App />, document.getElementById('root'))

基本介绍

函数组件本质就是一个函数

jsx
语法:
1. 函数名就是组件名,函数名首字母必须大写
2. 函数中必须有return 语句,并且返回react元素
jsx
函数组件的调用: 通过jsx语法调用()
1. 单标签调用: <组件名/>
2. 对标签调用:  <组件名></组件名>
jsx
调用过程:
1. 首先jsx 有首字母大写的标签,会当做组件处理,查找该组件的定义
2. 如果找到了并且发现是函数组件,那么render方法会帮咱们调用函数组件
3. 将函数调用的返回值 [react元素],替换掉组件调用标签的位置

函数组件的定义及调用

jsx
const root = ReactDOM.createRoot(document.querySelector("#root"));
function Header(){
    console.log('Header run');
    console.log('Header this: ', this);// 函数组件中this 指向undefined
    return (
        <div>我是Header组件</div>
    )
}
function Main(){
    return (
        <div>我是Main组件</div>
    )
}
function Footer(){
    return (
        <div>我是Footer组件</div>
    )
}
root.render((
    <>
        <Header></Header>
        <Header/>
        <Main/>
        <Footer/>
    </>
))

自定义render模拟函数组件调用渲染过程

函数组件中的this,指向undefined

jsx
const root = ReactDOM.createRoot(document.querySelector("#root"));

function Header(){
    return (
        <div>我是Header组件</div>
    )
}

function renderByMy(reactComponent, root){
    console.log('reactComponent: ', reactComponent);// react元素对象
    console.log('reactComponent.type',reactComponent.type);// Header组件函数
    // 调用Header组件函数
    const vdom = reactComponent.type(); // vdom 是Header组件调用的返回值【react元素】
    console.log('vdom: ', vdom);
    // 创建真实dom
    const realDom = document.createElement(vdom.type);
    // 添加内容
    realDom.innerHTML = vdom.props.children;
    // 将真实dom渲染到根节点
    root.appendChild(realDom);
}

const oRoot = document.querySelector('#root');
renderByMy(<Header/>, oRoot);

函数组件中事件

JSX语法中的事件就是函数组件中的事件(没有this指向的问题)

绑定事件的语法:【通过标签属性的方式绑定】,函数名用插值表达式包裹

js
<button onClick={函数名}>click</button>
js
*事件回调函数研究:

*this指向问题:react的事件回调函数的调用者是window,所以this指向window(同原生DOM一致),因为react使用的是严格模式,所以,this指向undefined

*事件对象:
1. react的事件对象默认可以通过第一个形参进行接收
2. react的事件对象是一个经过react处理后的事件对象,原生的事件对象的常用属性都有,并且做了兼容性处理
3. 如果想获取原生的事件对象,可以通过nativeEvent 属性获取
4. 通过 e.preventDefault() 阻止默认行为

*事件回调函数传递参数
1. 包裹箭头函数:
<button onClick={()=>click(1,2)}>参数</button>
2. 即传递参数又传递事件对象 click是自己写的回调函数:
<button onClick={(e)=>click(e,1,2)}>参数事件对象同时传递</button>

*jsx中的 onChange事件实际是原生的 oninput事件
1. 触发时机是键盘输入就触发
2. 事件对象是 InputEvent
js
* 在标签属性上通过 on原生事件名=事件回调函数 进行绑定
1. 事件名首字母大写,使用驼峰命名法则:onKeyUp,onKeyDown
2. 事件回调函数填的是函数名[函数的定义、函数的引用地址],而不是函数的调用(原生DOM中写的是函数调用)

* -----------
* this指向问题:undefined 说明 react中的事件回调的调用者也是window,严格模式所以是undefined

* React中事件对象e:
1. 一个经过react包装后的事件对象,原生的事件对象常用属性,基本都有。这个事件对象更好用,已经做过兼容性处理了
2. 如果想获取原生的事件对象:使用e.nativeEvent获取
3. 事件的回调函数是window帮我们调用的,会默认的将事件对象作为第一个实参传递过来

* 事件回调函数参数的传递
* 包裹一个箭头函数,在箭头函数内部调用函数click并传递参数

<button onClick={(e)=>click(e,1,2)}>参数事件对象同时传递</button>

* react中的 onChange事件,实际是原生的 oninput事件,有输入就触发

事件回调函数的绑定及调用使用案例

js
<script type="text/babel">
    const root = ReactDOM.createRoot(document.querySelector("#root"));
    function click1() {
        console.log('click1');
        console.log('this: ', this);//undefined
    }

    function click2(e) {
        console.log(e); // SyntheticBaseEvent 事件对象
        e.preventDefault(); // 阻止默认行为
    }

    function click3(a, b) {
        console.log('click3');
        console.log('a: ', a); // undefined
        console.log('b: ', b); // undefined
    }

    function outFn() {
        click3(1, 2);
    }

    function click4(e, a, b) {
        console.log('e: ', e);
        console.log('a: ', a);
        console.log('b: ', b);
        e.preventDefault();
    }

    function change(e) {
        console.log('change e: ', e); // e: SyntheticBaseEvent 事件对象
        console.log('value: ', e.target.value); // 1 12 123
    }
    
    root.render((
        <div>
            {/* 事件触发执行回调函数click1*/}
            <p><button onClick={click1}>click1</button></p>
            {/* 事件触发执行回调函数click2*/}
            <p><a href="http://baidu.com" onClick={click2}>百度</a></p>

            <h3>事件回调函数传参</h3>
            {/* 不能直接调用函数,这样就相当于插值表达式使用函数的返回值,return 空,相当于undefined,a: undefined,b: undefined ,点击无反应,错误的示范*/}
            <p><button onClick={click3()}>click3</button></p>
            {/* 通过函数传递参数,a: 1,b: 2 ,正确的示范*/}
            <p><button onClick={outFn}>传递参数</button></p>
            {/* 通过包裹匿名函数传递参数,a: 1,b: 2 ,正确的示范*/}
            <p><button onClick={function () { click3(2, 3) }}>包裹匿名函数传递参数</button></p>
            {/* 通过箭头函数传递参数,a: 1,b: 2 ,正确的示范*/}
            <p><button onClick={() => click3(2, 3)}>包裹箭头函数传参</button></p>

            <h3>即传递参数,又同时传递事件对象</h3>
            {/* 通过箭头函数传递参数:e: SyntheticBaseEvent 事件对象 a: 1,b: SyntheticBaseEvent*/}
            <p><button onClick={(e) => { click3(1, e) }}>即传递参数,又同时传递事件对象</button></p>
            {/* 通过箭头函数传递参数:e: SyntheticBaseEvent 事件对象 a: 100,b: 222 */}
            <p><a href="http://baidu.com" onClick={(e) => click4(e, 100, 222)}>百度</a></p>
            {/* 通过函数传递参数 react中的 onChange事件,实际是原生的 oninput事件 有输入就触发*/}
            <input type="text" onChange={change} name="" id="" />
        </div>
    ))
</script>

props 外部数据

组件间通信的借助props传递

父组件向子组件传递数据

jsx
- 父组件如何传递数据给子组件?
  1. 通过子组件调用标签属性的方式传递
  
- 子组件如何接收父组件传递的数据?
  1. 类子组件通过 this.props属性接收	
  2. 函数子组件通过函数的形参接收【一般会直接在参数位置解构】

子组件向父组件传递数据,也是借助props

jsx
1. 在父组件定义一个方法,方法设置一个或多个形参
2. 将该方法改变this指向,让this指向当前组件的实例对象
3. 将该方法通过标签数据性的方式传递给子组件
4. 在子组件中通过 props接收
   1. 类组件: this.props.方法名
   2. 函数组件: props.方法名
5. 在子组件中调用该方法,并将要传递的数据以实参的方式传递

注意:

props外部数据是只读的,在子组件中不可以直接修改

props的children属性,可以接收到组件调用对标签中的子元素

父向子传值(类-函)

函数子组件也可以接收外部数据props,或通过函数组件的形参接收

函数子组件通过形参接收数据

父组件:src->App.js

jsx
import React, { Component } from 'react'
import FunCom from './components/FunCom';
/**
 * 状态数据研究三个方向:
 * 1. 如何定义状态
 * 2. 如何读取状态
 * 3. 如何修改状态
 */
export default class App extends Component {
    state = {
        count:99,
        msg:'atguigu123'
    }
    addCount(num){
        
        this.setState({
            count: this.state.count + num
        })
    }

    changeMsg(){
        this.setState({
            // msg: 'atguigu123123123123123212'
            msg:this.state.msg + '!'
        })
    }

    render() {
        console.log('render run');
        let {count,msg} = this.state;
        return (
            <div>
                <p>count : {this.state.count}-{count}</p>
                <p>msg: {this.state.msg}-{msg}</p>
                <p><button onClick={()=>this.addCount(3)}>count++ </button></p>
                <p><button onClick={()=>this.changeMsg()}>msg + !</button></p>
                <hr/>
                {/* 父组件通过子组件标签属性传递数据给子组件 */}
                {/* <ClassCom num={count} xiaoxi={msg} school='尚硅谷'/> */}
                <ClassCom count={count} msg={msg} school='尚硅谷'/>
                <FunCom/>
            </div>
        )
    }
}

子组件:src->FunCom.js:通过 形参接收

jsx
import React from 'react'
// 直接在参数位置解构props对象中的数据
export default function FunCom({count, msg ,school}) {
    console.log('FunCom run');
    
    return (
        <div>
            <h3>FunCom</h3>
            <p>props count: {count}</p>
            <p>props msg: {msg}</p>
            <p>props school: {school}</p>
        </div>
    )
}

// export default function FunCom(props) {
//     console.log('FunCom run');
//     console.log('props: ', props);
//     let {count, msg ,school} = props
//     return (
//         <div>
//             <h3>FunCom</h3>
//             <p>props count: {props.count}-{count}</p>
//             <p>props msg: {props.msg}-{msg}</p>
//             <p>props school: {props.school}-{school}</p>
//         </div>
//     )
// }

子向父传值(函-类)

jsx
1. 父组件定义方法
2. 通过 子组件标签属性将方法传递给子组件 [注意要改变该方法的this指向,this的指向为父组件实例]
3. 子组件通过props接收
4. 子组件调用方法并将数据以实参的形式传递

父组件:src->App.js

js
import React, { Component } from 'react'
import FunCom from './components/FunCom';
/**
 * 状态数据研究三个方向:
 * 1. 如何定义状态
 * 2. 如何读取状态
 * 3. 如何修改状态
 */
export default class App extends Component {
    state = {
        count:99,
        msg:'atguigu123'
    }
    addCount(num){
        
        this.setState({
            count: this.state.count + num
        })
    }

    changeMsg(){
        this.setState({
            // msg: 'atguigu123123123123123212'
            msg:this.state.msg + '!'
        })
    }

    // 1. 定义一个方法
    decCount(num){
        this.setState({
            count:this.state.count - num
        })
    }

    render() {
        console.log('render run');
        let {count,msg} = this.state;
        return (
            <div>
                <p>count : {this.state.count}-{count}</p>
                <p>msg: {this.state.msg}-{msg}</p>
                <p><button onClick={()=>this.addCount(3)}>count++ </button></p>
                <p><button onClick={()=>this.changeMsg()}>msg + !</button></p>
                <hr/>
                {/* 将decCount this指向改变为当前实例对象后传递给子组件 */}
                <ClassCom count={count} msg={msg} school='尚硅谷' decCount={this.decCount.bind(this)}/>
                {/* 函数子组件也是通过属性传递数据 */}
                <FunCom  count={count} msg={msg} school='尚硅谷' decCount={this.decCount.bind(this)}/>
            </div>
        )
    }
}

子组件:src->FunCom.js

jsx
import React from 'react'
// 直接在参数位置解构props对象中的数据 接收方法 decCount
export default function FunCom({count, msg ,school,decCount}) {
    console.log('FunCom run');
    return (
        <div>
            <h3>FunCom</h3>
            <p>props count: {count}</p>
            <p>props msg: {msg}</p>
            <p>props school: {school}</p>
            <p><button onClick={()=>{
                // 调用方法,并传递参数
                decCount(7);
            }}>子传父</button></p>
        </div>
    )
}

使用拓展运算符

tsx
// 父组件
let [use,setUser] = useState({
	count:1
	msg:'Hanserj'
})

<Child {...user}>
    
// 子组件
export default function FunCom({count, msg}) {}

限定props类型

使用prop-types 包,对传入的外部数据进行类型、必填、默认值的限定

jsx
import PropTypes from 'prop-types'

函数组件通过把函数当作一个对象,然后添加属性的方式实现

jsx
TestFun.propTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number
}

TestFun.defaultProps = {
    age: 10000
}

useState数据状态

基本使用

作用:让函数组件拥有状态

语法

shell
使用useState创建状态数据
let [状态变量,设置状态的方法] = useState(初始值)            
let res = useState(100);
console.log('res: ', res); //  [100, ƒ]
使用解构,从数组中解构出数据和方法
let [count, setCount] = useState(100)
let [状态数据,设置状态的方法] = useState(初始值);
// 例如:
let [count, setCount] = useState(100)
setCount(要设置的count的最新的值)
// 例如:
setCount(count + 1)

setCount:函数调用后会发生两件事:
1. count状态数据的值改变
2. 触发函数组件的重新调用,完成重新渲染,页面更新

调用过程

jsx
useState的执行过程:
useState是一个函数,是一个引用类型,

1. 在window作用域中定义一个函数组件TestState,数据类型是引用数据类型,浏览器会开辟一个堆内存;
2. 堆内存中会存放着TestState函数代码(以字符串的形式存放),并产生一个内存地址xxxfff111;
3. TestState函数名对应的是一个内存地址:xxxfff111,指向堆内存
4. 当代码运行调用TestState函数组件时,-> TestState()===xxxfff111() -> TestState()会进入一个调用执行栈的内存空间
5. 在调用执行栈的内存空间中TestState()会把代码从堆内存中取出来按顺序执行。
6. 代码执行到let[count,setCount] = useState(99); setCount(count+1);时,新的count值100会被放在useState Hook函数中的缓存空间中 count = 100
7. setCount触发函数组件会重新调用 TestState(),TestState()会开辟一个新的执行栈从堆内存获取重新执行代码,代码从上到下执行。 并从useState Hook函数缓存空间中获取count值100,渲染到页面上
shell
let [状态变量,设置状态的方法] = useState(初始值)

useState使用案例

jsx
import React from 'react'
// 1. 导入useState函数
import { useState } from 'react'

export default function TestState() {
    console.log('testState run');
    // 2. 使用useState创建状态数据
    /**
     * let [状态变量,设置状态的方法] = useState(初始值)            
     */
    // let res = useState(100);
    // console.log('res: ', res); //  [100, ƒ]
    let [count, setCount] = useState(99);
    return (
        <div>
            <p>state count : {count}</p>
            <p><button onClick={()=>{
                // setCount(要设置的count的最新的值)
                setCount(count+1)
                // 1. 改变状态count
                // 2. 会触发函数组件重新调用,完成页面的重新渲染
            }}>count++</button></p>
        </div>
    )
}

渲染优化

函数组件中的useState:

useState中set[数据]方法在修改数据状态的时候,如果数据状态没有发生改变,函数组件只会多渲染一次,后续再不会重新渲染同类组件不同

(类组件中的setState每次执行都会重新渲染)。

父组件:src->App.jsx

jsx
import React, { useState } from 'react'
import TestState from './components/TestState'

export default function App() {
    console.log('App run');
    let [count, setCount] = useState(88);
    let [msg, setMsg] = useState('atguigu');
    return (
        <div>
            <h3>App</h3>
            <p>app state count: {count}</p>
            <p>app state msg: {msg}</p>
            <p><button onClick={()=>{
                setCount(count+1)
            }}>count++</button></p>
            <p><button onClick={()=>{
                setMsg(msg + '!')
            }}>msg + '!'</button></p>

            {/* 函数组件的useState对状态的改变做了优化,当状态没有改变的时候,函数组件只多渲染一次 */}
            <p><button onClick={()=>{
                setCount(9999);
            }}>count赋值一个固定值</button></p>

            <TestState count={count} msg={msg}/>
        </div>
    )
}

子组件:src->components->Child.jsx

jsx
import React from 'react'

export default function TestState({count,msg}) {
    
    return (
        <div>
            <h3>Son</h3>
            <p>props count: {count}</p>
            <p>props msg: {msg}</p>
        </div>
    )
}

两种传参方式

jsx
1. setXxx(值):直接给xxx赋值
setState(state + 1)
2. setXxx(回调函数(用于接收缓存空间中的参数)): 回调函数的参数是最新缓存区中的值,回调函数的返回值,是要给缓存区设置的值
setState((prevState)=>prevState + 1)

注意:
当使用setXxx(值),发现效果值不对的时候,就要考虑是否出现了闭包,直接使用回调函数的写法即可
setXxx(回调函数) 这种形式是高阶函数形式

产生闭包,导致设置状态值不对的原理

shell
useState的执行过程:
useState是一个函数,是一个引用类型,

1. 在window作用域中定义一个函数组件App,数据类型是引用数据类型,浏览器会开辟一个堆内存;
2. 堆内存中会存放着App函数代码(以字符串的形式存放),并产生一个内存地址;
3. App函数名对应的是一个内存地址:xxxfff111,指向堆内存
<App首次挂载调用>
4. 当代码运行调用App函数组件时,第一次挂载调用App(),App()会进入一个调用执行栈的内存空间
5. 在调用执行栈的内存空间中App()会把代码从堆内存中取出来按顺序执行。
6. 代码执行到 let[msg,setMsg] = useState('atguigu') 时,useState函数会把 'atguigu' 存放在缓存空间中。
7. 代码执行到 useEffect(()=>{},[]) 时,组件挂载完成后,()=>{定时器} 回调函数会执行,在第一次挂载时,App函数作用域中会存在 ()=>{定时器} 回调函数
8. 代码执行render渲染显示{msg},这时定时器还没有到时间。
9. 当触发事件点击按钮后,会执行setMsg(msg + '!'),useState函数会把 'atguigu!' 存放在缓存空间中,并覆盖之前的值。
10. setMsg()触发两件事:1.修改状态2.重新渲染页面->也就是重新调用App()函数
<App第二次挂载调用>
11. 浏览器会把代码从堆内存拿出来,再次开辟一个执行栈,这次调用是由于setMsg()组件更新时触发的调用, useEffect(()=>{},[])是挂载的时候触发调用,所以第二次调用中的代码不包括 ()=>{定时器} 这些代码。
12. 代码执行到 let[msg,setMsg] = useState('atguigu') 时,这时msg的取值不是取初始值,而是取缓存空间中的值'atguigu!',在二次调用时App函数作用域中msg的值为缓存空间中的值'atguigu!'。
13. 代码执行render渲染显示{msg},渲染msg为'atguigu!'

<定时器到期执行>
14. 定时器到期执行 ()=>{setMsg(msg+'-')} 回调函数,这时的msg是首次挂载App()函数作用域中的msg,值为'atguigu',setMsg()调用执行后,setState()函数缓存空间的msg值为'atguigu-'
15. 代码执行render渲染显示{msg},渲染msg为'atguigu-'

产生这种现象的原因是:
	在异步回调获取最新状态并设置状态,在React设计时是这么设计的,看到的数值不是最新的,而是当时渲染的值。
解决办法: 在setXxx(CallBack(xxx))函数的回调函数的参数中传递缓存空间中的变量msg,
即:setMsg((msg)=>{return msg + '-'}) // 这种可以
setMsg(msg + '-') // 这种不行
shell
异步回调获取不到最新值及解决方案
通常情况下 setState 直接使用上述第一种方式传参即可,但在一些特殊情况下第一种方式会出现异常;

例如希望在异步回调或闭包中获取最新状态并设置状态,此时第一种方式获取的状态不是实时的,React 官方文档提到:组件内部的任何函数,包括事件处理函数和Effect,都是从它被创建的那次渲染中被「看到」的,所以引用的值任然是旧的,最后导致setState 出现异常

setXxx(CallBack(xxx))使用案例

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

export default function App() {
    let [msg, setMsg] = useState('atguigu');

    useEffect(()=>{// componentDidMount 组件挂载完成后执行
        setTimeout(()=>{
            // 第一种用法:在有闭包的时候,值不对,只能取到初始值=默认值
            // setMsg(msg + '-')

            // 第二种用法, 接收一个回调函数作为参数,回调函数的形参是缓存区中最新变化后的msg值,回调函数的返回值,是要设置缓存区的最新的值
            setMsg((msg)=>{
                return msg + '-'
            })
        },2000)
    },[])

    return (
        <div>
            <p>msg: {msg}</p>
            <p><button onClick={()=>{
                setMsg(msg + '!')
            }}>msg + '!'</button></p>
        </div>
    )
}

数据状态undefined

当数据状态的初始值是undefined的时候可以通过插值表达式

ts
export interface IHospitalItem {
    id: string;
    createTime: string; // 创建时间
    param: {
        hostypeString: string; // 医院等级
        fullAddress: string; // 医院地址
    },
    hoscode: string; // 医院编号
    hosname: string; // 医院名
    hostype: string; // 医院类型
    provinceCode: string; // 省
    cityCode: string; // 市
    districtCode: string; // 区
    address: string; // 地址
    logoData: string; // 医院logo base64URL
    route: string;  //乘车路线
    status: number; // 医院状态
    intro:string | null;
    bookingRule: IBookingRule | null;
}

let [hospital, setHospital] = useState<IHospitalItem>();

const _getHospitalDetail = async ()=>{
    let {bookingRule,hospital} = await getHospitalDetail(id as string);
    setHospital(hospital);
}

<Descriptions.Item labelStyle={{width:200}} label="医院名称" span={1.5}>{hospital?.hosname}</Descriptions.Item>

在 {hospital?.hosname} 中,问号 (?.) 是 JavaScript 中的可选链操作符(Optional Chaining Operator)。它的作用是安全地访问对象属性,防止在对象为 nullundefined 的情况下抛出错误。

具体来说:
hospital?.hosname 的含义是:
如果 hospital 存在,则访问 hospital.hosname;
如果 hospital 为 nullundefined,则整个表达式返回 undefined,而不会抛出错误。
这种写法常用于异步获取数据的场景,例如在组件初始化时,hospital 可能还没有数据,从而避免在渲染过程中出现报错。
ts
<Select placeholder='请选择区' style={{ width: 180 }}>
    {dictList.map(dict => (
        <Option key={dict.id} value={dict.value}>{dict.name}</Option>
    ))}
</Select>

在 {dict.name} 中不需要使用 ?. 的原因是 dictList.map() 方法确保了 dict 是有效的对象,因为 map 会遍历 dictList 数组的每一项。如果 dictList 是一个有效的数组,且其中的每个元素(即 dict)都包含了 id、value 和 name 属性,则无需使用可选链操作符 ?.

不过,为了确保代码更加健壮,最好确认以下几点:

dictList 是一个数组:在调用 map 之前,确保 dictList 是数组类型,否则可能会引发错误。
dict 对象的属性完整性:确保 dictList 数组的每一项 dict 都包含了 id、value 和 name 属性。

如果你无法保证 dictList 中的每个对象都包含 name 属性,那么可以使用可选链操作符 ?. 作为保险
<Option key={dict.id} value={dict.value}>{dict?.name}</Option>
如果某个 dict 对象缺少 name 属性,表达式会返回 undefined,而不会报错

useEffect生命周期

image-20240912150104508

jsx
流程: render  ==>  componentDidUpdate 
触发: 组件更新完成后更新

只有以上三种情况会触发组件的 render重新调用,进而触发 componentDidUpdate的执行。

useState() 函数
js
useEffect的回调函数不能使用async直接修饰,需要单独定义async函数,手动调用 --> 04-Hook函数#axios使用案例

基本使用

一个函数组件可以有多个useEffect并且都生效,不同的生命周期执行不同的逻辑处理

作用:函数组件中用来模拟生命周期

用法

shell
# 1 组件挂载完成后执行 + 监听自己所有的state和所有的外部数据props,数据更新后执行
useEffect(callback())	componentDidMount + componentDidUpdate[all]
# 2 组件只在挂载完成后执行,不监听数据,数据更新不执行。
useEffect(callback(), [])	componentDidMount
# 3 组件挂载完成后执行 + 数组中监听的数据,数据更新后执行
useEffect(callback(),[state,props])	componentDidMount + componentDidUpdate[数组中监听的]
# 4 组件卸载前执行
useEffect(()=>{
	return ()=>{
		// componentWillUnmount
	}
},[])
# 4 组件清理函数,监听组件本身数据更新,props更新,父组件更新,都会执行componentWillUnmount钩子
useEffect(()=>{
	return ()=>{
		// componentWillUnmount
	}
})
# 5 只会在componentDidUpdate时运行
// 通过给useRef设置初始值,初始值只在代码未执行到绑定react元素对象有效
// 当代码执行到绑定react元素对象时,ref对象指向react元素对象
const flagRef = useRef(true)
useEffect(()=>{
	// 原因: mount之后
    if(flagRef.current){
        flagRef.current = false;
        return;
    }
    // 以下代码只会在componentDidUpdate时运行
	console.log("componentDidUpdate is running")
})

useEffect使用案例

父组件:src->App.jsx

jsx
// 1. 从react中解构 useEffect
import React, { useEffect, useState } from 'react'
import Son from './components/Son';

export default function App() {
    console.log('App run');
    let [count, setCount] = useState(100)
    let [msg, setMsg] = useState('atguigu')
    // 1. 组件挂载阶段生命周期
    // useEffect(() => {
    //     console.log('componentDidMount');
    // }, [])

    // 2. // componentDidMount + componentDidUpdate[监听count]
    // 一个函数组件可以有多个useEffect并且都生效,不同的生命周期执行不同的逻辑处理
    // useEffect(() => {
    //     console.log('9999');
    // }, [count])

    // 3. componentDidMount + componentDidUpdate[监听所有的state,和所有的props]
    // useEffect(() => {
    //     console.log('useEffect()不设置第二个参数,监听自己所有的state和所有的外部数据props')
    // });

    return (
        <div>
            <p>count: {count}</p>
            <p>msg: {msg}</p>
            <p>
                <button onClick={() => {
                    setCount(count + 1)
                }}>count</button>

            </p>
            <p>
                <button onClick={() => {
                    setMsg(msg + '!')
                }}>msg</button>

            </p>
            <hr />
            {count % 2 === 0 && <Son count={count} />}
        </div>
    )
}

子组件:src->components->Child.jsx

jsx
import React from 'react'
import { useEffect } from 'react'

export default function Son({ count }) {
    // useEffect(()=>{ // componentDidMount + componentDidUpdate[all state,all props]
    //     console.log('son 111')
    // })

    // 4. componentWillUnmount
    useEffect(() => {
        console.log('son componentDidMount')

        return () => {
            // componentWillUnMount
            console.log('son will unmount')
        }
    }, [])

    useEffect(()=>{ 
        // 这种情况会在每次更新时都会执行 componentWillUnMount
    // componentDidMount + componentDidUpdate
        console.log('son componentDidMount + son componentDidUpdate')
        return ()=>{// componentWillUnMount
            console.log('son will unmount2')
        }
    })
    return (
        <div>
            <p>son props count: {count}</p>
        </div>
    )
}

渲染执行问题

父组件和子组件更新渲染问题

shell
注意:父组件重新render,那么子组件也会无条件重新渲染

[如果是类组件-render方法重新调用、如果是函数组件-函数被重新调用]

当子组件的props发生变化时,会触发子组件的重新渲染。
当父组件的state发生变化时,会导致父组件和所有子组件重新渲染。
当子组件的state发生变化时,会导致子组件重新渲染。
当使用React Context时,只要Provider的value发生变化,所有使用了该Context的子组件都会重新渲染。
当使用React Hooks中的useState、useReducer等Hook时,调用对应的更新函数会导致组件重新渲染。
当使用React的forceUpdate方法强制组件重新渲染时,子组件也会重新渲染。

父组件更新,子组件执行 问题

shell
上例中当父组件更新 msg 时,Son 组件执行 console.log('son will unmount') 的原因是因为 Son 组件的渲染和 useEffect 钩子的行为。

发生的原因分析:
Son 组件的条件渲染: App 组件中,Son 组件是通过如下条件渲染的:
{count % 2 === 0 && <Son count={count} msg={msg} />}

这意味着只有当 count 为偶数时,Son 组件才会被渲染。当 count 变为奇数时,Son 组件会被移除(即卸载),这会触发 useEffect 中的清理函数,也就是 componentWillUnmount。

msg 的更新导致重新渲染: 当父组件中的 msg 更新时,App 组件会重新渲染。这不会直接卸载 Son,但它仍会重新评估条件 count % 2 === 0

为什么触发 will unmount:
 Son 组件中的第二个 useEffect 没有传入依赖项数组,这意味着它在 每次父组件或自身重新渲染时 都会运行。由于每次渲染时都会触发 useEffect,即使是由于 msg 的更新,也会执行 useEffect 的清理函数:

useEffect(() => {
    console.log('son componentDidMount + componentDidUpdate');
    return () => {
        console.log('son will unmount');
    };
});

这个 useEffect 相当于每次渲染时都会先执行清理函数 console.log('son will unmount'),然后再执行新的 useEffect 内部逻辑。因此,即使 Son 组件没有被卸载,useEffect 的清理函数还是会运行。

解决方法:

如果你只希望在组件卸载时运行清理函数,而不是每次渲染时都运行,可以将 useEffect 的依赖项数组设置为空 [],这样它就只会在组件挂载和卸载时运行:
useEffect(() => {
    console.log('son componentDidMount');
    return () => {
        console.log('son will unmount');
    };
}, []);  // 依赖项为空,表示只在挂载和卸载时运行

这种方式可以避免在每次渲染时都执行清理函数。

电子时钟练习

src->App.jsx

jsx
// 1. 从react中解构 useEffect
import React, { useEffect, useState } from 'react'
import Child from './components/Child'
export default function App() {
    let [count,setCount] = useState(66)
    return (
        <div>
            <p>app</p>
            <p>{count}</p>
            <button onClick={()=>{setCount(count + 1)}}>更改Count</button>
            {count % 2 == 0 && <Child />}
        </div>
    )
}

src->Child.jsx

jsx
// 1. 从react中解构 useEffect
import React, { useEffect, useState } from 'react'

import moment from 'moment'
export default function Timer() {
    let [nowDate, setNowDate] = useState(moment().format('YYYY-MM-DD HH:mm:ss'));

    useEffect(() => {// componentDidMount
        let timer = setInterval(() => {
            console.log('定时器 is start...')
            setNowDate(moment().format('YYYY-MM-DD HH:mm:ss'))
        }, 1000)
        return () => { // componentWillUnmount
            console.log('定时器 is Unmount...');
            clearInterval(timer)
        }
    }, [])
    return (
        <div>
            <p>当前日期是: {nowDate}</p>
        </div>
    )
}

useRef函数

通过调用useRef()函数,返回一个ref对象。

获取dom对象

shell
创建一个ref对象 const inpRef = useRef() 
在标签上绑定属性 ref={inputRef}
获取对象 inputRef.current

使用案例

jsx
// 1. 从react中解构 useEffect
import React, { useEffect, useRef, useState } from 'react'

export default function App() {
    let [count,setCount] = useState(66)
    const inpRef = useRef() 
    console.log("inpRef",inpRef) // 返回值是一个对象:{current:undefined} 初始值
    const inputRef = useRef(123); // useRef 可以给初始值 {current:123}
    return (
        <div>
            <p>app</p>
            <p>{count}</p>
            {/* 当绑定后返回值是一个对象:{current:input} */}
            <input type="text" ref={inpRef} />
        </div>
    )
}

初始值用法

jsx
useRef() 可以设置初始值模拟componentDidUpdate单独执行。
jsx
import React, { useEffect, useRef, useState } from 'react'

export default function App() {
    const inputRef = useRef(123) // 设置初始值 123 赋给了current属性 {current:123}
    
    // ref的初始值可以用来单独模拟componentDidUpdate
    const flagRef = useRef(true) //{current:true}
    
    // let flag = true; // 重新渲染都会读取到原来的数值 true
    // let [flag, setFlag] = useState(true); // 重新渲染会调用useState()函数,又会触发重新渲染.
    
    let [count, setCount] = useState(88)
    
   	// 首次执行:componentDidMount()执行时,设置了一个标记,就返回退出了,无执行后续代码
    // componentDidMount代码进来的时候会执行,if(){},if条件成立,
    // flagRef.current = false; flagRef的current的值被设置成 false
    // 并且return跳出函数
    
    // 数据更新,再次执行:
    // 由于 flagRef的current的值被设置成 false, if(){}条件不成立,后续代码可以执行
    // update的代码可以执行
    
    // 这样就实现了 componentDidMount()的时候,代码返回,无执行
    // componentDidUpdate()的时候,if不成立,后续Update代码执行,
    useEffect(()=>{
        if(flagRef.current){
            flagRef.current = false
            return;
        }
		// 下边写Update的代码
        // componentDidUpdate
        console.log('componentDidUpdate')
    });

    return (
        <div>
            <p>count: {count}</p>
            
            <p><button onClick={()=>{
                setCount(count + 1)
            }}>更新数据</button></p>
        </div>
    )
}

绑定组件

ref对象也可以绑定组件,在组件上设置ref属性,在不同的组件类型有不同的作用

shell
# 在类组件上绑定:
	可以通过ref1.current获得类组件的实例对象
	类组件:ref.current 可以获取类组件的实例对象
# 在函数组件上绑定:
	函数组件本身不能绑定ref,会报错,但是配合React.forwardRef就可以绑定ref属性了
	可以实现在父组件上操作子组件中的dom元素
	我们希望父组件的ref,只能有限的操作子组件的真实dom,那就需要能够在子组件中自定义父组件ref的current属性,实现有限的功能===>useImpretiveHandle

在类组件上绑定

src->component->ClassTest.jsx

jsx
import React, { Component } from 'react'

export default class ClassTest extends Component {
    state = {
        count: 101
    }
    render() {
        return (
            <div>ClassTest</div>
        )
    }
}

src->App.jsx

jsx
import React ,{useRef} from 'react'
import ClassTest from './components/ClassTest'

export default function App() {
    const classRef = useRef();
    return (
        <div>
            <h3>类组件 绑定ref</h3>
            <ClassTest ref={classRef} />

            <button onClick={() => {
                console.log('classRef: ', classRef.current); // 类组件的react元素
            }}>获取ref</button>
        </div>
    )
}

在函数组件上绑定

src->component->FunTest.jsx

jsx
import React, { useRef } from 'react'
import { useImperativeHandle } from 'react';

// 通过函数组件中的第二个参数接收父组件传过来的 ref属性
function FunTest(props, ref) {
    console.log('ref:', ref); // 父组件传过来的 ref对象
    const myselfRef = useRef();

    // 设置父组件ref的权限,参数是 父组件ref和回调函数
    // 回调函数中的返回值是一个对象,对象中的方法就是给父组件ref使用的方法
    useImperativeHandle(ref, () => ({
        changeColor() {
            // 使用自己的ref对象绑定元素
            myselfRef.current.style.color = 'green';
        },
        changeFontSize() {
            myselfRef.current.style.fontSize = '30px';
        }
    }))
    return (
        // 未使用useImperativeHandle,父组件会完全控制子组件
        // <div ref={ref}>FunTest</div>
        // 使用useImperativeHandle,父组件只能操作useImperativeHandle中回调函数返回的对象中的方法
        <div ref={myselfRef}>FunTest</div>
    )
}
// 通过forwardRef()函数 让可以在函数组件上设置ref属性
export default React.forwardRef(FunTest)

src->App.jsx

jsx
import React ,{useRef} from 'react'
import FunTest from './components/FunTest';

export default function App() {
    const funRef = useRef('我是谁?');
    return (
        <div>
            <h3>函数组件 绑定ref</h3>
            <FunTest ref={funRef} />
            <hr />
            <button onClick={() => {
                // 操作子组件中的dom
                // funRef.current.style.color='red'
                // funRef.current.style.fontSize = '30px'
                funRef.current.changeColor();
                funRef.current.changeFontSize();
            }}>获取ref</button>
        </div>
    )
}

受控组件

form表单非受控组件使用valuechecked属性给标签添加属性,实现双向绑定的效果。

获取值:直接通过数据状态获取

设置value属性双向绑定案例

jsx
import React, { useRef, useState } from 'react'

export default function App() {
    let [username, setUsername] = useState('MinatoAqua')
    let [password, setPassword] = useState('445')
    const save = (e) => {
        e.preventDefault()
        console.log('username: ', usernameRef.current.value)
        console.log('password: ', passwordRef.current.value)
    }
    function changeUserName(e){
        setUsername(e.target.value)
    }
    function changePassword(e) {
        setPassword(e.target.value)
    }
    // 将chage函数封装成一个函数
    function changeUser(e,fun){
        fun(e.target.value)
    }
    // 通过高阶函数实现:函数嵌套函数
    function funChangeUser(fun){
        console.log('funChangeUser is running')
        return (e) => {
            fun(e.target.value)
        }
    }
    // 创建ref对象
    const usernameRef = useRef()
    const passwordRef = useRef()
    return (
        <div>
            <h3>非受控组件</h3>
            <form onSubmit={save}>
                {/* 绑定事件对象 分开写的方式*/}
                <p>姓名: <input type="text" name="username" ref={usernameRef} value={username} onChange={changeUserName}/></p>
                <p>密码: <input type="text" name="password" ref={passwordRef} value={password} onChange={changePassword} /></p>
                {/* 绑定事件对象 使用函数封装的方式*/}
                <p>姓名: <input type="text" name="username" ref={usernameRef} value={username} onChange={(e) => { changeUser(e, setUsername)}} /></p>
                <p>密码: <input type="text" name="password" ref={passwordRef} value={password} onChange={(e) => { changeUser(e, setPassword) }} /></p>
                {/* 绑定事件对象 通过高阶函数实现*/}
                <p>姓名: <input type="text" name="username" ref={usernameRef} value={username} onChange={funChangeUser(setUsername)} /></p>
                <p>密码: <input type="text" name="password" ref={passwordRef} value={password} onChange={funChangeUser(setPassword)} /></p>
                <p><button>保存</button></p>
            </form>
        </div>
    )
}

受控组件调色板练习

jsx
import React, { useState } from 'react'

export default function App() {
    let [r, setR] = useState(0)
    let [g, setG] = useState(255)
    let [b, setB] = useState(0)
    function change(fn) {
        return e => {
            fn(e.target.value)
        }
    }
    return (
        <div>
            <p>红 <input type="range" min={0} max={255} name="" value={r} onChange={change(setR)} /></p>
            <p>绿  <input type="range" min={0} max={255} name="" value={g} onChange={change(setG)} /></p>
            <p>蓝 <input type="range" min={0} max={255} name="" value={b} onChange={change(setB)} /></p>
            <div style={{ width: 200, height: 200, border: '1px solid red', backgroundColor: `rgb(${r},${g},${b})` }}>

            </div>
        </div>
    )
}

非受控组件

form表单非受控组件使用defaultValuedefaultChecked属性给标签添加属性,设置默认值

获取值:通过ref获取真实dom.value获取

jsx
import React, { useRef, useState } from 'react'

export default function App() {
    let [username, setUsername] = useState('MinatoAqua')
    let [password, setPassword] = useState('445')
    const save = (e) => {
        e.preventDefault()
        console.log('username: ', usernameRef.current.value)
        console.log('password: ', passwordRef.current.value)
    }
    // 创建ref对象
    const usernameRef = useRef()
    const passwordRef = useRef()
    return (
        <div>
            <h3>非受控组件</h3>
            <form onSubmit={save}>
                {/* 绑定事件对象 */}
                <p>姓名: <input type="text" name="username" ref={usernameRef} defaultValue={username} /></p>
                <p>密码: <input type="text" name="password" ref={passwordRef} defaultValue={password} /></p>
                <p><button>保存</button></p>
            </form>
        </div>
    )
}