文章目录
- React的组件化
- 类组件
- render函数的返回值
- 函数组件
- 认识生命周期
- 生命周期解析
- 生命周期函数
- 不常用生命周期函数
- 认识组件间的通信
- 父组件传递子组件 - 类组件和函数组件
- 参数propTypes
- 子组件传递父组件
- React中的插槽(slot)
- children实现插槽
- props实现插槽
- Context应用场景
- Context相关API
- 为什么使用setState
- setState异步更新
- 如何获取异步的结果
- setState默认是异步的(React18之后)
React的组件化
◼ 组件化思想的应用:
有了组件化的思想,我们在之后的开发中就要充分的利用它。
尽可能的将页面拆分成一个个小的、可复用的组件。
这样让我们的代码更加方便组织和管理,并且扩展性也更强。
◼ React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
◼ 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
函数组件、无状态组件、展示型组件主要关注UI的展示;
函数组件、无状态组件、展示型组件主要关注数据逻辑;
◼ 当然还有很多组件的其他概念:比如异步组件、高阶组件等,我们后续再学习。
类组件
◼ 类组件的定义有如下要求:
组件的名称是大写字符开头(无论类组件还是函数组件)
类组件需要继承自 React.Component
类组件必须实现render函数
◼ 在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。
◼ 使用class定义一个组件:
constructor是可选的,我们通常在constructor中初始化一些数据;
this.state中维护的就是我们组件内部的数据;
render() 方法是 class 组件中唯一必须实现的方法;
render函数的返回值
◼ 当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
◼ React 元素:
通常通过 JSX 创建。
例如,
无论是
◼ 数组或 fragments:使得 render 方法可以返回多个元素。
◼ Portals:可以渲染子节点到不同的 DOM 子树中。
◼ 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
◼ 布尔类型或 null:什么都不渲染。
函数组件
◼ 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
◼ 函数组件有自己的特点(当然,后面我们会讲hooks,就不一样了):
没有生命周期,也会被更新并挂载,但是没有生命周期函数;
this关键字不能指向组件实例(因为没有组件实例);
没有内部状态(state);
◼ 我们来定义一个函数组件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hz3EZ8r-1675131641817)(C:/Users/dell0322/AppData/Roaming/Typora/typora-user-images/image-20230130174831013.png)]
◼ 在前面的学习中,我们主要讲解类组件,后面学习Hooks时,会针对函数式组件进行更多的学习。
认识生命周期
◼ 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
◼ React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;
◼ React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
比如实现componentWillUnmount函数:组件即将被移除时,就会回调;
我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
◼ 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命
周期的回调)
生命周期解析
生命周期函数
◼ Constructor
◼ 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
◼ constructor中通常只做两件事情:
通过给 this.state 赋值对象来初始化内部的state;
为事件绑定实例(this);
◼ componentDidMount
◼ componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
◼ componentDidMount中通常进行哪里操作呢?
依赖于DOM的操作可以在这里进行;
在此处发送网络请求就最好的地方;(官方建议)
可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
◼ componentDidUpdate
◼ componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
当组件更新后,可以在此处对 DOM 进行操作;
如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
◼ componentWillUnmount
◼ componentWillUnmount() 会在组件卸载及销毁之前直接调用。
在此方法中执行必要的清理操作;
例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
不常用生命周期函数
◼ 除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
◼ 另外,React中还提供了一些过期的生命周期函数,这****些函数已经不推荐使用。
◼ 更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
认识组件间的通信
◼ 在开发过程中,我们会经常遇到需要组件之间相互进行通信:
比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让
其进行展示;
又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;
也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
◼ 总之,在一个React项目中,组件之间的通信是非常重要的环节;
◼ 父组件在展示子组件,可能会传递一些数据给子组件:
父组件通过 属性=值 的形式来传递给子组件数据;
子组件通过 props 参数获取父组件传递过来的数据;
父组件传递子组件 - 类组件和函数组件
和vue类似,在父组件内填写相应的参数名称在子组件上就可以把值传递到子组件
父组件
import React, { Component } from 'react'
import axios from "axios"
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'
export class Main extends Component {
constructor() {
super()
this.state = {
banners: [],
productList: []
}
}
componentDidMount() {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
const banners = res.data.data.banner.list
const recommend = res.data.data.recommend.list
this.setState({
banners,
productList: recommend
})
})
}
render() {
const { banners, productList } = this.state
return (
<div className='main'>
<div>Main</div>
<MainBanner banners={banners} title="轮播图"/>
<MainBanner/>
<MainProductList productList={productList}/>
</div>
)
}
}
export default Main
子组件MainBanner
子组件通过 props 参数获取父组件传递过来的数据;
import React, { Component } from 'react'
import PropTypes from "prop-types"
export class MainBanner extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
// console.log(this.props)
const { title, banners } = this.props
return (
<div className='banner'>
<h2>封装一个轮播图: {title}</h2>
<ul>
{
banners.map(item => {
return <li key={item.acm}>{item.title}</li>
})
}
</ul>
</div>
)
}
}
// MainBanner传入的props类型进行验证
MainBanner.propTypes = {
banners: PropTypes.array,
title: PropTypes.string
}
// MainBanner传入的props的默认值
MainBanner.defaultProps = {
banners: [],
title: "默认标题"
}
export default MainBanner
子组件MainProductList
import React, { Component } from 'react'
import PropTypes from "prop-types"
export class MainBanner extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
// console.log(this.props)
const { title, banners } = this.props
return (
<div className='banner'>
<h2>封装一个轮播图: {title}</h2>
<ul>
{
banners.map(item => {
return <li key={item.acm}>{item.title}</li>
})
}
</ul>
</div>
)
}
}
// MainBanner传入的props类型进行验证
MainBanner.propTypes = {
banners: PropTypes.array,
title: PropTypes.string
}
// MainBanner传入的props的默认值
MainBanner.defaultProps = {
banners: [],
title: "默认标题"
}
export default MainBanner
参数propTypes
◼ 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;
但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;
◼ 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
◼ 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
比如验证数组,并且数组中包含哪些元素;
比如验证对象,并且对象中包含哪些key以及value是什么类型;
比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
◼ 如果没有传递,我们希望有默认值呢?
我们使用defaultProps就可以了
子组件传递父组件
◼ 某些情况,我们也需要子组件向父组件传递消息:
在vue中是通过自定义事件来完成的;
在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
◼ 我们这里来完成一个案例:
将计数器案例进行拆解;
将按钮封装到子组件中:CounterButton;
CounterButton发生点击事件,将内容传递到父组件中,修改counter的值;
◼ 案例:
父组件
只是让父组件给子组件传递一个回调函数
import React, { Component } from 'react'
import AddCounter from './AddCounter'
import SubCounter from './SubCounter'
export class App extends Component {
constructor() {
super()
this.state = {
counter: 100
}
}
changeCounter(count) {
this.setState({ counter: this.state.counter + count })
}
render() {
const { counter } = this.state
return (
<div>
<h2>当前计数: {counter}</h2>
<AddCounter addClick={(count) => this.changeCounter(count)}/>
<SubCounter subClick={(count) => this.changeCounter(count)}/>
</div>
)
}
}
export default App
子组件:AddCounter
在子组件中调用这个函数即可
import React, { Component } from 'react'
// import PropTypes from "prop-types"
export class AddCounter extends Component {
addCount(count) {
this.props.addClick(count)
}
render() {
return (
<div>
<button onClick={e => this.addCount(1)}>+1</button>
<button onClick={e => this.addCount(5)}>+5</button>
<button onClick={e => this.addCount(10)}>+10</button>
</div>
)
}
}
// AddCounter.propTypes = {
// addClick: PropTypes.func
// }
export default AddCounter
子组件:SubCounter
在子组件中调用这个函数即可
import React, { Component } from 'react'
export class SubCounter extends Component {
subCount(count) {
this.props.subClick(count)
}
render() {
return (
<div>
<button onClick={e => this.subCount(-1)}>-1</button>
<button onClick={e => this.subCount(-5)}>-5</button>
<button onClick={e => this.subCount(-10)}>-10</button>
</div>
)
}
}
export default SubCounter
React中的插槽(slot)
◼ React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
组件的children子元素;
props属性传递React元素;
children实现插槽
◼ 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。
父组件直接插入标签
props.children:它包含组件的开始标签和结束标签之间的内容
props实现插槽
◼ 通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
◼ 另外一个种方案就是使用 props 实现:
通过具体的属性名,可以让我们在传入和获取时更加的精准;
父组件:
具体的属性名,可以让我们在传入和获取时更加的精准
子组件:
直接通过props找到
Context应用场景
◼ 非父子组件数据的共享:
在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。
但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
◼ 但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
React提供了一个API:Context;
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
Context相关API
◼ React.createContext
创建一个需要共享的Context对象:
如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
const MyContext = React.creacteContext(defaultValue)
◼ Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
Provider 接收一个 value 属性,传递给消费组件;
一个 Provider 可以和多个消费组件有对应关系;
多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
<MyContext.Provider value={/*某个值*/} />
◼ Class.contextType
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
这能让你使用 this.context 来消费最近 Context 上的那个值;
你可以在任何生命周期中访问到它,包括 render 函数中;
MyClass.contextType = MyContext
◼ Context.Consumer
这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
这里需要 函数作为子元素(function as child)这种做法;
这个函数接收当前的 context 值,返回一个 React 节点;
<MyContext.Consumer> { value => /*基于 context 值进行渲染 */ } </MyContext.Consumer>
为什么使用setState
◼ 开发中我们并不能直接通过修改state的值来让界面发生更新:
因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
我们必须通过setState来告知React数据已经发生了变化;
◼ 疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?
原因很简单,setState方法是从Component中继承过来的。
setState异步更新
◼ setState设计为异步,可以显著的提升性能;
如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
最好的办法应该是获取到多个更新,之后进行批量更新;
◼ 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
state和props不能保持一致性,会在开发中产生很多的问题;
如何获取异步的结果
◼ 那么如何可以获取到更新后的值呢?
◼ 方式一:setState的回调
setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
格式如下:setState(partialState, callback)
◼ 方式二:在组件生命周期挂载完成后访问
setState默认是异步的(React18之后)
◼ 在React18之后,默认所有的操作都被放到了批处理中(异步处理)。
◼ 如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作: