函数组件
1. 组件名首字母必须大写. 因为react以此来区分组件元素/标签 和 一般元素/标签
2. 组件内部如果有多个标签,必须使用一个根标签包裹.只能有一个根标签
3. 必须有返回值.返回的内容就是组件呈现的结构, 如果返回值为 null,表示不渲染任何内容
4. 会在组件标签渲染时调用, 但不会产生实例对象(this->undefined), 不能有状态,生命周期(16.4以前)
function App() {
// return null
return <div>App</div>
}
// 函数名就是组件名
ReactDom.render(<App />, document.getElementById('root'))
基本介绍
函数组件本质就是一个函数
语法:
1. 函数名就是组件名,函数名首字母必须大写
2. 函数中必须有return 语句,并且返回react元素
函数组件的调用: 通过jsx语法调用()
1. 单标签调用: <组件名/>
2. 对标签调用: <组件名></组件名>
调用过程:
1. 首先jsx 有首字母大写的标签,会当做组件处理,查找该组件的定义
2. 如果找到了并且发现是函数组件,那么render方法会帮咱们调用函数组件
3. 将函数调用的返回值 [react元素],替换掉组件调用标签的位置
函数组件的定义及调用
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
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指向的问题)
绑定事件的语法:【通过标签属性的方式绑定】,函数名用插值表达式包裹
<button onClick={函数名}>click</button>
*事件回调函数研究:
*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
* 在标签属性上通过 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事件,有输入就触发
事件回调函数的绑定及调用使用案例
<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传递
父组件向子组件传递数据
- 父组件如何传递数据给子组件?
1. 通过子组件调用标签属性的方式传递
- 子组件如何接收父组件传递的数据?
1. 类子组件通过 this.props属性接收
2. 函数子组件通过函数的形参接收【一般会直接在参数位置解构】
子组件向父组件传递数据,也是借助props
1. 在父组件定义一个方法,方法设置一个或多个形参
2. 将该方法改变this指向,让this指向当前组件的实例对象
3. 将该方法通过标签数据性的方式传递给子组件
4. 在子组件中通过 props接收
1. 类组件: this.props.方法名
2. 函数组件: props.方法名
5. 在子组件中调用该方法,并将要传递的数据以实参的方式传递
注意:
props外部数据是只读的,在子组件中不可以直接修改
props的children属性,可以接收到组件调用对标签中的子元素
父向子传值(类-函)
函数子组件也可以接收外部数据props,或通过函数组件的形参接收
函数子组件通过形参接收数据
父组件:src->App.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 + '!'
})
}
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:通过 形参接收
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>
// )
// }
子向父传值(函-类)
1. 父组件定义方法
2. 通过 子组件标签属性将方法传递给子组件 [注意要改变该方法的this指向,this的指向为父组件实例]
3. 子组件通过props接收
4. 子组件调用方法并将数据以实参的形式传递
父组件:src->App.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
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>
)
}
使用拓展运算符
// 父组件
let [use,setUser] = useState({
count:1
msg:'Hanserj'
})
<Child {...user}>
// 子组件
export default function FunCom({count, msg}) {}
限定props类型
使用prop-types 包,对传入的外部数据进行类型、必填、默认值的限定
import PropTypes from 'prop-types'
函数组件通过把函数当作一个对象,然后添加属性的方式实现
TestFun.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
TestFun.defaultProps = {
age: 10000
}
useState数据状态
基本使用
作用:让函数组件拥有状态
语法
使用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. 触发函数组件的重新调用,完成重新渲染,页面更新
调用过程
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,渲染到页面上
let [状态变量,设置状态的方法] = useState(初始值)
useState使用案例
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
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
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>
)
}
两种传参方式
1. setXxx(值):直接给xxx赋值
setState(state + 1)
2. setXxx(回调函数(用于接收缓存空间中的参数)): 回调函数的参数是最新缓存区中的值,回调函数的返回值,是要给缓存区设置的值
setState((prevState)=>prevState + 1)
注意:
当使用setXxx(值),发现效果值不对的时候,就要考虑是否出现了闭包,直接使用回调函数的写法即可
setXxx(回调函数) 这种形式是高阶函数形式
产生闭包,导致设置状态值不对的原理
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 + '-') // 这种不行
异步回调获取不到最新值及解决方案
通常情况下 setState 直接使用上述第一种方式传参即可,但在一些特殊情况下第一种方式会出现异常;
例如希望在异步回调或闭包中获取最新状态并设置状态,此时第一种方式获取的状态不是实时的,React 官方文档提到:组件内部的任何函数,包括事件处理函数和Effect,都是从它被创建的那次渲染中被「看到」的,所以引用的值任然是旧的,最后导致setState 出现异常
setXxx(CallBack(xxx))使用案例
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的时候可以通过插值表达式
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)。它的作用是安全地访问对象属性,防止在对象为 null 或 undefined 的情况下抛出错误。
具体来说:
hospital?.hosname 的含义是:
如果 hospital 存在,则访问 hospital.hosname;
如果 hospital 为 null 或 undefined,则整个表达式返回 undefined,而不会抛出错误。
这种写法常用于异步获取数据的场景,例如在组件初始化时,hospital 可能还没有数据,从而避免在渲染过程中出现报错。
<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生命周期
流程: render ==> componentDidUpdate
触发: 组件更新完成后更新
只有以上三种情况会触发组件的 render重新调用,进而触发 componentDidUpdate的执行。
useState() 函数
useEffect的回调函数不能使用async直接修饰,需要单独定义async函数,手动调用 --> 04-Hook函数#axios使用案例
基本使用
一个函数组件可以有多个useEffect并且都生效,不同的生命周期执行不同的逻辑处理
作用:函数组件中用来模拟生命周期
用法:
# 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
// 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
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>
)
}
渲染执行问题
父组件和子组件更新渲染问题
注意:父组件重新render,那么子组件也会无条件重新渲染
[如果是类组件-render方法重新调用、如果是函数组件-函数被重新调用]
当子组件的props发生变化时,会触发子组件的重新渲染。
当父组件的state发生变化时,会导致父组件和所有子组件重新渲染。
当子组件的state发生变化时,会导致子组件重新渲染。
当使用React Context时,只要Provider的value发生变化,所有使用了该Context的子组件都会重新渲染。
当使用React Hooks中的useState、useReducer等Hook时,调用对应的更新函数会导致组件重新渲染。
当使用React的forceUpdate方法强制组件重新渲染时,子组件也会重新渲染。
父组件更新,子组件执行 问题
上例中当父组件更新 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
// 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
// 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对象
创建一个ref对象 const inpRef = useRef()
在标签上绑定属性 ref={inputRef}
获取对象 inputRef.current
使用案例
// 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>
)
}
初始值用法
useRef() 可以设置初始值模拟componentDidUpdate单独执行。
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属性,在不同的组件类型有不同的作用
# 在类组件上绑定:
可以通过ref1.current获得类组件的实例对象
类组件:ref.current 可以获取类组件的实例对象
# 在函数组件上绑定:
函数组件本身不能绑定ref,会报错,但是配合React.forwardRef就可以绑定ref属性了
可以实现在父组件上操作子组件中的dom元素
我们希望父组件的ref,只能有限的操作子组件的真实dom,那就需要能够在子组件中自定义父组件ref的current属性,实现有限的功能===>useImpretiveHandle
在类组件上绑定
src->component->ClassTest.jsx
import React, { Component } from 'react'
export default class ClassTest extends Component {
state = {
count: 101
}
render() {
return (
<div>ClassTest</div>
)
}
}
src->App.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
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
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表单非受控组件使用value
或checked
属性给标签添加属性,实现双向绑定的效果。
获取值:直接通过数据状态获取
设置value属性双向绑定案例
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>
)
}
受控组件调色板练习
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表单非受控组件使用defaultValue
或defaultChecked
属性给标签添加属性,设置默认值
获取值:通过ref获取真实dom.value获取
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>
)
}