组件化开发
组件化开发
组件化是一种分而治之的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
- 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
- 任何的应用都会被抽象成一颗组件树
React 的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component)
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
这些概念有很多重叠,但是他们最主要是关注数据逻辑和 UI 展示的分离:
- 函数组件、无状态组件、展示型组件主要关注 UI 的展示
- 类组件、有状态组件、容器型组件主要关注数据逻辑
类组件
类组件的定义有如下要求:
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自
React.Component
- 类组件必须实现 render 函数
使用 class 定义一个组件:
- constructor 是可选的,我们通常在 constructor 中初始化一些数据
this.state
中维护的就是我们组件内部的数据- render() 方法是 class 组件中唯一必须实现的方法
当 render 被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型:
- React 元素
- 通常通过 JSX 创建
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件
- 数组或 fragments
- 使得 render 方法可以返回多个元素
- Portals
- 可以渲染子节点到不同的 DOM 子树中
- 字符串或数值类型
- 在 DOM 中会被渲染为文本节点
- 布尔类型或 null
- 什么都不渲染
import React from 'react'
class App extends React.Component {
constructor() {
super()
this.state = {
message: 'App Component'
}
}
render() {
const { message } = this.state
// 1.react元素: 通过jsx编写的代码就会被编译成React.createElement,
return <h2>{message}</h2>
// 2.组件或者fragments(后续学习)
return ["abc", "cba", "nba"]
// 3.字符串/数字类型
return "Hello World"
// 4。布尔类型或 null
return true
}
}
export default App
函数式组件
函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容
函数组件有自己的特点(前提:没有 hooks)
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- this 关键字不能指向组件实例(因为没有组件实例)
- 没有内部状态(state)
function App(props) {
return <h1>App Functional Component</h1>
}
export default App
生命周期
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
- 比如装载阶段(Mount),组件第一次在 DOM 树中被渲染的过程
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程
- 比如卸载过程(Unmount),组件从 DOM 树中被移除的过程
React 内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现
componentDidMount
函数:组件已经挂载到 DOM 上时,就会回调 - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调 - 比如实现
componentWillUnmount
函数:组件即将被移除时,就会回调
Constructor
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
- constructor中通常只做两件事情:
- 通过给
this.state
赋值对象来初始化内部的state - 为事件绑定实例(this)
- 通过给
componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法- 当组件更新后,可以在此处对 DOM 进行操作
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求(例如,当 props 未发生变化时,则不会执行网络请求)
componentWillUnmount
componentWillUnmount()
会在组件卸载及销毁之前直接调用- 在此方法中执行必要的清理操作
- 例如,清除 timer,取消网络请求或清除在
componentDidMount()
中创建的订阅等
还有一些不常用的生命周期函数:
getDerivedStateFromProps
:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 stategetSnapshotBeforeUpdate
:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置)shouldComponentUpdate
:该生命周期函数很常用,性能优化时可以使用另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用
https://zh-hans.reactjs.org/docs/react-component.html
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
import React from 'react'
class HelloWorld extends React.Component {
// 1.构造方法: constructor
constructor() {
console.log('constructor')
super()
this.state = {
message: 'Hello World'
}
}
changeText() {
this.setState({ message: '你好啊, 李银河' })
}
// 2.执行render函数
render() {
console.log('render')
const { message } = this.state
return (
<div>
<h2>{message}</h2>
<button onClick={e => this.changeText()}>修改文本</button>
</div>
)
}
// 3.组件被渲染到DOM: 被挂载到DOM
componentDidMount() {
console.log('componentDidMount')
}
// 4.组件的DOM被更新完成: DOM发生更新
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('componentDidUpdate:', prevProps, prevState, snapshot)
}
// 5.组件从DOM中卸载掉: 从DOM移除掉
componentWillUnmount() {
console.log('componentWillUnmount')
}
// 不常用的生命周期补充
shouldComponentUpdate() {
return true
}
getSnapshotBeforeUpdate() {
console.log('getSnapshotBeforeUpdate')
return {
scrollPosition: 1000
}
}
}
export default HelloWorld
组件间通信
- App 组件是 Header、Main、Footer 组件的父组件
- Main 组件是 Banner、ProductList 组件的父组件
父组件在展示子组件,可能会传递一些数据给子组件:
- 父组件通过 属性=值 的形式来传递给子组件数据
- 子组件通过 props 参数获取父组件传递过来的数据
参数 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 即可
class MainBanner extends React.Component { }
// MainBanner 传入的 props 类型进行验证
MainBanner.propTypes = {
banners: PropTypes.array,
title: PropTypes.string
}
// MainBanner 传入的 props 的默认值
MainBanner.defaultProps = {
banners: [],
title: "默认标题"
}
从 ES2022 开始,你也可以在 React 类组件中将 defaultProps
声明为静态属性
class MainBanner extends React.Component {
static defaultProps = {
banners: [],
title: "默认标题"
}
}
子传父
某些情况,我们也需要子组件向父组件传递消息:
- 在 vue 中是通过自定义事件来完成的
- 在 React 中同样是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
插槽
在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div、span 等等这些元素
- 这种需求在 Vue 当中有一个固定的做法是通过 slot 来完成的,React 呢?
- React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的 children 子元素
- props 属性传递 React 元素
每个组件都可以获取到 props.children
:它包含组件的开始标签和结束标签之间的内容
- 如果传入多个 children,props 里的 children 就是数组
- 如果只传入一个 children,props 里的 children 就是那一个 children(是 React 里的一个 element)
通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生
另外一个种方案就是使用 props 实现
Context
非父子组件数据的共享:
- 在开发中,比较常见的数据传递方式是通过 props 属性自上而下(由父到子)进行传递
- 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI 主题、用户登录状态、用户信息等)
- 如果我们在顶层的 App 中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作
这里补充一个知识点 Spread Attributes
但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
- React 提供了一个 API:Context
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
- Context 设计目的是为了共享那些对于一个组件树而言是全局的数据,例如当前认证的用户、主题或首选语言
// 1.创建一个Context
const ThemeContext = React.createContext({ color: 'blue', size: 10 })
// 2.通过 ThemeContext 中 Provider 中 value 属性为后代提供数据
export class App extends Component {
render() {
return (
<ThemeContext.Provider value={{ color: 'red', size: '30' }}>
<Home {...info} />
</ThemeContext.Provider>
)
}
}
// 3.第三步操作: 设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext
export class HomeInfo extends Component {
render() {
// 4.第四步操作: 获取数据, 并且使用数据
console.log(this.context)
}
}
函数式组件中使用 Context 共享的数据
- 类组件多个数据共享,也可以使用 Consumer 来进行消费
function HomeBanner() {
return (
<div>
<ThemeContext.Consumer>
{value => {
return <h2> Banner theme:{value.color}</h2>
}}
</ThemeContext.Consumer>
</div>
)
}
React.createContext
- 创建一个需要共享的 Context 对象
- 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 context 值
- defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值
const MyContext = React.createContext(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
用法
setState(updater)
setState((state, props) => updater)
setState(updater, callback)
为什么使用 setState
开发中我们并不能直接通过修改 state 的值来让界面发生更新:
- 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化
- React 并没有实现类似于 Vue2 中的
Object.defineProperty
或者 Vue3 中的Proxy
的方式来监听数据的变化 - 我们必须通过 setState 来告知 React 数据已经发生了变化
疑惑:在组件中并没有实现 setState 的方法,为什么可以调用呢?
- 原因很简单,setState 方法是从 Component 中继承过来的
为什么setState设计为异步呢?
React核心成员(Redux的作者)Dan Abramov 也有对应的回复
https://github.com/facebook/react/issues/11527#issuecomment-360199710
setState 设计为异步,可以显著提升性能
- 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的
- 最好的办法应该是获取到多个更新,之后进行批量更新
如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步
- state 和 props 不能保持一致性,会在开发中产生很多的问题
优势:
- 多个 updater 放在一次更新中,执行一次 render 函数,提高性能
- 保证在 state 没有被更新的时候,state、props 保持一致
setState 一定是异步吗
https://zh-hans.reactjs.org/blog/2022/03/29/react-v18.html#whats-new-in-react-18
React 18 之前
在 setTimeout 中的更新
- 在 React18 之前,setTimeout 中 setState 操作,是同步操作
jsxsetTimeout(() => { this.setState({ message: '你好,世界' }) console.log(this.state.message) }, 0)
原生 DOM 事件
- 在组件生命周期或 React 合成事件中,setState 是异步的
- 在 setTimeout、Promise.then 回调、原生 DOM 事件中,setState 是同步的
React18 之后
在 React18 之后,默认所有的操作都放到了批处理中(异步操作)
如果希望代码可以同步拿到,则需要执行特殊的 flushSync 操作
jsximport { flushSync } from 'react-dom' flushSync(() => { this.setState({ message: '你好,世界' }) })