一. 组件化开发
1.什么是组件化开发呢?
- 组件化是一种分而治之的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
- 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
- 我们需要通过组件化的思想来思考整个应用程序:
- 我们将一个完整的页面分成很多个组件
- 每个组件都用于实现页面的一个功能块
- 而每一个组件又可以进行细分
- 而组件本身又可以在多个地方进行复用

2. React的组件化
- 组件化是
React
的核心思想,前面我们封装的App
本身就是一个组件:- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一颗组件树

- 组件化思想的应用:
- 有了组件化的思想,我们在之后的开发中就要充分的利用它
- 尽可能的将页面拆分成一个个小的、可复用的组件
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强
React
的组件相对于Vue
更加的灵活和多样,按照不同的方式可以分成很多类组件:- 根据组件的定义方式,可以分为:
函数组件(Functional Component )和类组件(Class Component)
- 根据组件内部是否有状态需要维护,可以分成:
无状态组件(Stateless Component )和有状态组件(Stateful Component)
- 根据组件的不同职责,可以分成:
展示型组件(Presentational Component)和容器型组件(Container Component)
- 根据组件的定义方式,可以分为:
- 这些概念有很多重叠,但是他们最主要是关注数据逻辑和
UI
展示的分离:- 函数组件、无状态组件、展示型组件主要关注
UI
的展示 - 类组件、有状态组件、容器型组件主要关注数据逻辑
- 函数组件、无状态组件、展示型组件主要关注
- 当然组件还有很多的其他概念:比如异步组件、高阶组件等
3.类组件
类组件的定义有如下要求:
- 组件名称是 大写字符开头(无论类组件还是函数组件)
class
组件需 继承自React.Component
class
组件需 实现render
函数
在
ES6
之前,可以通过create-react-class
模块来定义类组件,但是目前官网建议我们使用ES6
的class
类定义使用
class
定义一个组件:constructor
是可选的,我们通常在constructor
中初始化一些数据this.state
中维护的就是我们组件内部的数据render()
方法是class
组件中唯一必须实现的方法
jsx// 写法一: 内部有对Component单独导出 import { Component } from 'react' class App extends Component { render() { return <h2></h2> } } // 写法二 import React from 'react' class App extends React.Component { render() { return <h2></h2> } }
4.render
函数
render()
方法执行时机:- 组件第一次渲染的时候
this.props
变化时this.state
变化时
render()
方法的返回值- 当
render
被调用时,它会检查this.props
和this.state
的变化并返回以下类型之一: React
元素:- 通常通过
jsx
创建出来的,都叫做React
元素,因为一般jsx
的代码最终都会转换为React.createElement
的代码,而React.createElement
就是在创建React
元素 - 例如,
<div />
会被React
渲染为DOM
节点,<MyComponent />
会被React
渲染为自定义组件 - 无论是
<div />
还是<MyComponent />
均为React
元素
- 通常通过
Array、fragments
:使得render
方法可以返回多个元素Portals
:可以渲染子节点到不同的DOM
子树中String、Number
:它们在DOM
中会被渲染为文本节点- **
Boolean、null、undefined
**:什么都不渲染
- 当
5.函数组件
函数组件是使用
function
来进行定义的函数,不需要继承自React.Component
,只是这个函数会返回和类组件中render
函数返回一样的内容函数组件有自己的特点(当然,后面用
hooks
,就不一样了):- 没有生命周期函数,也会被更新并挂载
this
关键字不能指向组件实例(因为没有组件实例)- 没有内部状态
state
定义一个函数组件:
jsxfunction App() { const state = { // 没有内部state,每次执行函数的时候都会重新初始化state,所以这个state是无法维护下去的 msg: 'hhh' } return <h2></h2> }
二. 组件生命周期
1.认识生命周期
- 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期
React
组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能- 生命周期和生命周期函数的关系:
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
- 比如装载阶段(
Mount
),组件第一次在DOM
树中被渲染的过程 - 比如更新过程(
Update
),组件状态发生变化,重新更新渲染的过程 - 比如卸载过程(
Unmount
),组件从DOM
树中被移除的过程
- 比如装载阶段(
React
内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:- 比如实现
componentDidMount
函数:组件已经挂载到DOM
上时,就会回调 - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调 - 比如实现
componentWillUnmount
函数:组件即将被移除时,就会回调 - 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能
- 比如实现
- 我们谈
React
生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的(函数式组件也可以通过hooks
来模拟一些生命周期的回调)
2.生命周期解析
- 最基础、最常用的生命周期函数:

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
更新前的一些信息(比如说滚动位置)jsxgetSnapshotBeforeUpdate() { return { scrollPosition: 100 } } componentDidUpdate(preProps, preState, snapshot) { // 可以在 componentDidUpdate 中的 snapshot 中接收到 getSnapshotBeforeUpdate 的返回值 }
shouldComponentUpdate
:该生命周期函数很常用,可以用来做性能优化jsx/* 返回 false 的时候,当组件状态变化时,不会重新执行 render 函数, 在执行 render 函数前,会判断 shouldComponentUpdate 的返回值,默认是 true */ shouldComponentUpdate() { return false }
另外,
React
中还提供了一些过期的生命周期函数,这些函数已经不推荐使用更详细的生命周期相关的内容,可以参考官网:
https://zh-hans.reactjs.org/docs/react-component.html

三. 组件间通信
1.认识组件的嵌套
- 组件之间存在嵌套关系:
- 在之前的案例中,我们只是创建了一个组件
App
- 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护
- 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件
- 再将这些组件组合嵌套在一起,最终形成我们的应用程序
- 在之前的案例中,我们只是创建了一个组件

- 上面的嵌套逻辑如下,它们存在如下关系:
App
组件是Header、Main、Footer
组件的父组件Main
组件是Banner、ProductList
组件的父组件
2.认识组件间的通信
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如
App
可能使用了多个Header
,每个地方的Header
展示的内容不同,那么我们就需要使用者传递给Header
一些数据,让其进行展示 - 又比如我们在
Main
中一次性请求了Banner
数据和ProductList
数据,那么就需要传递给他们来进行展示 - 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件
- 比如
总之,在一个
React
项目中,组件之间的通信是非常重要的环节父组件在展示子组件,可能会传递一些数据给子组件:
- 父组件通过属性=值的形式来传递给子组件数据
- 子组件通过props参数获取父组件传递过来的数据
- 内部自动把
props
保存到当前子组件实例中,等价于this.props = props
- 内部自动把
jsxclass App extends Component { constructor(props) { console.log(props) // { msg: 'aaa' } super(props) // 内部有做一个操作:this.props = props } render() { const { msg } = this.props // 省略constructor不写,this.props也是可以获取到传入进来的数据 console.log(msg) // aaa // ... } } <div> <App msg="aaa"></App> </div>
函数式
jsxfunction App(props) { console.log(props) // { msg: 'aaa' } const { msg } = props console.log(msg) // 'aaa' return ( // ... ) } <div> <App msg="aaa"></App> </div>
3.参数propTypes
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
- 当然,如果你项目中默认继承了
Flow
或者TypeScript
,那么直接就可以进行类型验证 - 但是,即使我们没有使用
Flow
或者TypeScript
,也可以通过prop-types
库来进行参数验证
- 当然,如果你项目中默认继承了
从
React v15.5
开始,React.PropTypes
已移入另一个包中:prop-types
库,方便进行tree-shaking
,因为你不引用的话,就会被删除掉这部分jsximport PropTypes from 'prop-types' Cpn.PropTypes = { name: PropTypes.string, age: PropType.number }
更多的验证方式,可以参考官网:
https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
- 比如验证数组,并且数组中包含哪些元素
- 比如验证对象,并且对象中包含哪些
key
以及value
是什么类型 - 比如某个属性是必须的,使用
requiredFunc
:PropTypes.func.isRequired
如果没有传递,我们希望有默认值呢?
- 我们使用
defaultProps
就可以了
jsxCpn.defaultProps = { name: 'later', age: 18 }
- 我们使用
4.子组件传递父组件
某些情况,我们也需要子组件向父组件传递消息:
- 在
vue
中是通过自定义事件来完成的 - 在
React
中同样是通过props
传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
- 在
我们这里来完成一个案例:
- 将计数器案例进行拆解
- 将按钮封装到子组件中:
AddCount、SubtractCount
- 子组件发生点击事件,将内容传递到父组件中,修改父组件中的
counter
的值
- 子组件发生点击事件,将内容传递到父组件中,修改父组件中的

5.组件通信案例练习
效果图
代码

四. 组件插槽用法
1.React
中的插槽
- 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的
div、span
等等这些元素 - 我们应该让使用者可以决定某一块区域到底存放什么内容
- 这种需求在
Vue
当中有一个固定的做法是通过slot
来完成的,React
呢? React
对于这种需要插槽的情况非常灵活,有两种方案可以实现:- 组件的
children
子元素 props
属性传递React
元素
- 组件的
2.children
实现插槽
- 每个组件都可以获取到**
props.children
:它包含组件的开始标签和结束标签之间的内容** - 组件内部多个子元素的话,
props.children
获取到的是一个数组,里面包含的是组件内部的子元素

如果组件内部只有一个子元素的话,
props.children
获取到的就是那一个子元素(源码中的处理)jsxclass NavBar extends Component { render() { const { children } = this.props console.log(children) // 不是一个数组,而是类名为aaa的元素 return ( ) } } <NavBar> <div className="aaa"> <div className="bbb"></div> </div> </NavBar>
可以通过
propTypes
限制组件内部传入的元素个数,如果别人传入的类型不符合要求,就会报错jsximport PropTypes from 'prop-types' NavBar.propTypes = { children: PropTypes.element, // 限制组件内部children值的类型是React元素,传一个子元素的话children就是传入的单个元素 children: PropTypes.array // 限制组件内部children值的类型是数组,传多个子元素的话children才是数组 }
3.props
实现插槽
- 通过
children
实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生 - 另外一个种方案就是使用
props
实现:- 通过具体的属性名,可以让我们在传入和获取时更加的精准

4.通过props
实现作用域插槽
- 通过子组件调用父组件传入的属性中的回调函数,父组件中通过该回调函数调用所传入的参数
item
,来决定返回什么内容在子组件中显示 - 组件内部引用的样式是全局的,而不是局部的

五. 非父子间组件通信
1.Context
应用场景
非父子组件数据的共享:
- 在开发中,比较常见的数据传递方式是通过
props
属性自上而下(由父到子)进行传递 - 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
- 如果我们在顶层的
App
中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作
- 在开发中,比较常见的数据传递方式是通过
我们实现一个一层层传递的案例:
- 顺便补充一个小的知识点:
Spread Attributes
jsxclass App extends Component { constructor() { super() this.state = { info: { name: 'later', age: 18} } } render() { return ( <TabBar name={ this.state.info.name} age={ this.state.info.age } ></TabBar> { /* 等价于上面写法 */ } <TabBar { ...info }></TabBar> ) } } class TabBar extends Component { render() { return <TabBarItem { ...this.props }></TabBarItem> } } class TabBar extends Component { render() { return <div { ...this.props }></div> } }
- 顺便补充一个小的知识点:
但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
React
提供了一个API
:Context
Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
2.Context
相关API
React.createContext
- 创建一个需要共享的
Context
对象 - 如果一个组件订阅了
Context
,那么这个组件会从离自身最近的那个匹配的Provider
中读取到当前的context
值 defaultValue
是组件在顶层查找过程中没有找到对应的Provider
,那么就使用默认值
jsxconst MyContext = React.createContext(defaultValue)
- 创建一个需要共享的
Context.Provider
- 每个
Context
对象都会返回一个Provider React
组件,它允许消费组件订阅context
的变化 Provider
接收一个value
属性,传递给消费组件- 一个
Provider
可以和多个消费组件有对应关系 - 多个
Provider
也可以嵌套使用,里层的会覆盖外层的数据 - 当
Provider
的value
值发生变化时,它内部的所有消费组件都会重新渲染
jsx<MyContext.Provider value={ /* 某个值*/}
- 每个
Class.contextType
- 挂载在
class
上的contextType
属性会被重赋值为一个由React.createContext()
创建的Context
对象: - 这能让你使用**
this.context
来消费离组件最近Context
上的那个值** - 你可以在任何生命周期中访问到它,包括
render
函数中 - 函数式组件中没有
contextType
jsxMyClass.contextType = MyContext
- 挂载在
Context.Consumer
- 这里,
React
组件也可以订阅到context
变更。这能让你在 函数式组件 中完成订阅context
- 这里需要 函数作为子元素(
function as child
)这种做法 - 这个函数接收当前的
context
值,返回一个React
节点
jsx<MyContext.Consumer> { value => /* 基于 context 值进行渲染 */ } </MyContext.Consumer>
- 这里,
3.Context
代码演练
Context
的基本使用- 什么时候使用默认值
defaultValue
呢?- 当
Home
组件不被UserContext
所包裹,却想访问UserContext
中的数据时,可以访问到UserContext
初始化传入的defaultValue
中的数据(有传的话)
- 当
jsx<div> <Home/> <UserContext.Provider value={ {name: 'later', age: 18} }></UserContext.Provider> </div>
- 什么时候使用默认值
什么时候使用
Context.Consumer
呢?- 1.当使用
value
的组件是一个函数式组件时 - 2.当组件中需要使用多个
Context
时
- 1.当使用

六. setState
的使用详解
1.为什么使用setState
- 开发中我们并不能直接通过修改
state
的值来让界面发生更新:- 因为我们修改了
state
之后,希望React
根据最新的State
来重新渲染界面,但是这种方式的修改React
并不知道数据发生了变化 React
并没有实现类似于Vue2
中的Object.defineProperty
或者Vue3
中的Proxy
的方式来监听数据的变化- 我们必须通过
setState
来告知React
数据已经发生了变化
- 因为我们修改了
- 疑惑:在组件中并没有实现
setState
的方法,为什么可以调用呢?- 原因很简单,
setState
方法是从Component
中继承过来的
- 原因很简单,

setState
用法jsxchangeText() { // 1.基本使用 this.setState({ message: "你好啊, 李银河" }) // 2.setState可以传入一个回调函数 // 好处一: 可以在回调函数中编写新的state的逻辑 // 好处二: 当前的回调函数会将之前的state和props传递进来 this.setState((state, props) => { // 1.编写一些对新的state处理逻辑 // 2.可以获取之前的state和props值 console.log(this.state.message, this.props) return { message: "你好啊, 李银河" } }) // 3.setState在React的事件处理中是一个异步调用 // 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码 // 那么可以在setState中传入第二个参数: callback this.setState({ message: "你好啊, 李银河" }, () => { console.log("++++++:", this.state.message) }) console.log("------:", this.state.message) }
2 .setState
异步更新
this.state = {
message: 'hello world'
}
// ...
changeText() {
this.setState({
message: '你好啊'
})
console.log(this.state.message) // hello world
}
setState
的更新是异步的?- 最终打印结果是
Hello World
- 可见
setState
是异步的操作,我们并不能在执行完setState
之后立马拿到最新的state
的结果
- 最终打印结果是
- 为什么
setState
设计为异步呢?setState
设计为异步其实之前在GitHub
上也有很多的讨论React
核心成员(Redux
的作者)Dan Abramov
也有对应的回复,有兴趣的同学可以参考一下https://github.com/facebook/react/issues/11527#issuecomment-360199710
- 其回答做一个简单的总结:
setState
设计为异步,可以显著的提升性能- 如果每次调用
setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的 - 最好的办法应该是获取到多个更新,之后进行批量更新
- 如果每次调用
- 如果同步更新了
state
,紧跟着还有很多同步代码需要执行,这时还没有立即执行render
函数,那么这时如果有父组件中的state
作为子组件props
的值,而刚好该state
有更新的话,则父组件和子组件中的数据不能保持同步state
和props
不能保持一致性,会在开发中产生很多的问题,像父组件中state
已经更新,这时传递给子组件的props
属性是该state
,而render
函数却还没执行,就会产生数据不一致
- 本次更新会加入
react
内部的一个事件队列queue
- 两个优势:
- 多个
update
放在同一次更新中,执行一次render
函数,提高性能 - 保证在
state
没有被更新的时候,state/props
保持一致
- 多个
3.如何获取异步的结果
那么如何可以获取到更新后的值呢?
方式一:
setState
的回调setState
接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行- 格式如下:
setState(partialState, callback)
jsxchangeText() { this.setState({ message: '你好啊' }, () => { console.log(this.state.message) // 你好啊 }) }
当然,我们也可以在生命周期函数中获取
jsxcomponentDidUpdate(preProps, preState, snapshot) { console.log(this.state.message) }
4.setState
一定是异步吗?(React18
之前)
验证一:在
setTimeout
中的更新:jsxchangeText() { setTimeout(() => { this.setState({ message: '你好啊' }) console.log(this.state.message) // 你好啊 }, 0) }
验证二:原生
DOM
事件:jsxcomponentDidMount() { const btnEl = document.querySelector('.btn') btnEl.addEventListener('click', () => { this.setState({ message: '你好啊' }) console.log(this.state.message) // 你好啊 }) }
在
React18
之前,其实分成两种情况:- 在组件生命周期或
React
合成事件中,setState
是异步(批处理) - 在
setTimeout
或Promise.then回调
或原生dom事件
中,setState
是同步
- 在组件生命周期或
5.setState
默认是异步的(React18
之后)
- 在
React18
之后,默认所有的操作都被放到了批处理中(异步处理)

如果希望代码可以同步拿到,则需要执行特殊的
flushSync
操作:jsximport { flushSync } from 'react-dom' // 在react18之前, setTimeout中setState操作, 是同步操作 // 在react18之后, setTimeout中setState异步操作(批处理) // 正确使用 flushSync(() => { this.setState({ message: "你好啊, 李银河" }) }) console.log(this.state.message) // 你好啊,李银河 // 错误使用 flushSync(() => { this.setState({ message: "你好啊, 李银河" }) console.log(this.state.message) })