文章目录
一、组件通讯介绍
- 每个组件都是独立的,默认情况下,只能使用组件自己的数据
- 组件化过程中,将完整功能的组件拆分成多个组件,更好的完成整个应用的功能,多个组件不可避免要共享数据
- 为了实现该功能,就需要打破组件的独立封闭性,让其与外界沟通,这个过程就是 组件通讯
二、组件的 props
1. 基础
- 组件是封闭的,需接收外部数据应该通过 props 来实现
- props 的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过 形参props 接收数据,类组件通过 this.props 接收数据
- props 是一个对象,对象里面存储传递的多个属性
- 特点
- props 是只读对象,值不能被修改
- 可以给组件传递任意的类型的数据
// 类组件
class Hello extends Component {
render() {
// 接收数据
return <h1>接收到的数据:{this.props.age}</h1>
}
}
// 传递数据
root.render(<Hello age={20} />)
// 函数组件
function Hello(props) {
return <h1>接收到的数据:{props.age}</h1>
}
// 传递数据
root.render(<Hello age={20} colors={['red', 'blue']} />)
注意:使用类组件,写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取 props!
class Hello extends Component {
// 接收传递的值
constructor(props) {
// 将值传递的父组件
super(props)
console.log(props) // {age:20}
}
render() {
// 接收数据
return <h1>接收到的数据:{this.props.age}</h1>
}
}
// 错误写法
class Hello extends Component {
constructor() {
super()
console.log(this.props) // undefined
}
render() {
// 接收数据
return <h1>接收到的数据:{this.props.age}</h1>
}
}
root.render(<Hello age={20} />)
2. children 属性
- 表示组件标签的子节点,当组件标签中有子节点时,props 就会有该属性
- 跟 props 一样,值可以是任意值(文本、React元素、组件、甚至函数)
function Hello() {
return <Child>{<h1>通过 children 方式传数据</h1>}</Child>
}
function Child(props) {
//
console.log(props)
return (
<div>
// 渲染 h1
{props.children}
</div>
)
}
3. props 校验
-
为什么要对 props 进行校验
- 对于组件来说 props 是外来的,无法保证组件使用者传入什么格式的数据
- 如果传入的数据格式不对,可能导致组件内部错误
- 关键问题:组件的使用者不知道明确的错误信息
-
作用
- 允许在创建组件的时候,指定 props 的类型、格式等
- 捕获使用组件时因 props 导致的错误,给出明确的错误提示,增加组件的健壮性
-
使用
- 安装 prop-types (yarn add prop-types / npm i props-types)
- 导入 prop-types 包
- **组件名.propTypes = {} **来给组件的 props 添加校验规则
- 校验规则由 PropType 对象来指定
import PropTypes from 'prop-types' function Hello() { return <Child age={19} /> } function Child(props) { return ( <div> <h1>number类型:{props.age}</h1> <h1>指定对象属性的类型:{props.obj.name}</h1> </div> ) } // 设置 props 默认值 Child.defaultProps = { // obj 的默认值,值的类型要和约束的一致 obj: { name: '张三' }, } // 给 props 添加约束 Child.propTypes = { // 约定 age 属性为 number 类型 // 类型不对,报出明确的错误,便于分析错误原因 age: PropTypes.number, // 这个对象里面的 name 属性必须是 string 类型 obj: PropTypes.shape({ name: PropTypes.string, }), }
三、组件通讯的四种方式
1. 父传子
- 父组件提供需要传递的 state 数据
- 给子组件标签添加属性,值为 state 中的数据
- 子组件通过 props 接收父组件传递的值
class Hello extends Component {
state = {
age: 19,
}
render() {
// 接收数据
return (
<div>
<h1>传递值给子组件的值:{this.state.age}</h1>
// 子组件
<Child age={this.state.age} />
</div>
)
}
}
function Child(props) {
return <h1>接收父组件的值:{props.age}</h1>
}
2. 子传父
- 利用回调函数,父组件提供回调,子组件调用,将传递的数据作为回调函数的参数
- 父组件提供一个回调函数(用于接收数据)
- 将函数作为属性的值,传递给子组件
- 子组件通过 props 调用回调函数
- 将子组件的数据作为参数传递给回调函数
class Hello extends Component {
state = {
age: 0,
}
// 提供回调
getChildMeg = (res) => {
// 接收子组件的值 res,将 state 中的值进行替换
this.setState({ age: res })
}
render() {
// 接收数据
return (
<div>
<h1>接收子组件的值:{this.state.age}</h1>
// 将父组件提供的回调传递给子组件
<Child getMsg={this.getChildMeg} />
</div>
)
}
}
class Child extends Component {
state = {
age: 20,
}
// 调用父组件传递的方法,将子组件的数据当作函数的参数传递过去
sendData = () => {
this.props.getMsg(this.state.age)
}
render() {
return (
<div>
<h1>子组件数据:{this.state.age}</h1>
<button onClick={this.sendData}>传递给父组件</button>
</div>
)
}
}
3. 兄弟组件(状态提升)
- 将 共享状态 提升到最近的公共父组件中,由 公共父组件 管理这个状态
- 公共父组件职责:
- 提供共享状态
- 提供操作共享状态的方法
- 通讯的子组件只需通过 props 接收状态或操作状态的方法
// 共享数据的组件
class Hello extends Component {
// 共享状态
state = {
count: 0,
}
// 提供修改状态的方法
updata = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<div>
{/* 当数据发生变化后,数据会自动流向该组件 */}
<Child1 count={this.state.count} />
<Child2 updata={this.updata} />
</div>
)
}
}
// 展示数据
function Child1(props) {
// 接收状态
return <h1>计数器:{props.count}</h1>
}
// 操作数据
function Child2(props) {
// 接收修改状态的方法
return <button onClick={props.updata}>+1</button>
}
4. 跨组件传递
- 传统方式:使用 props 一层层组件往下传递(繁琐)
- Context:通过 Consumer 不管隔多远,都能往上找离它最近的 Provider 提供的数据 (推荐)
- 如果两个组件嵌套多层,可以使用 Context 实现组件通讯
- 步骤
- React.createContext 创建 **Provider(提供数据)**和 **Consumer(消费数据)**组件
- 使用 Provide 组件作为父节点
- 通过 value 属性,表示要传递的值
- 使用 Consumer 组件接收数据,回调函数中的 data 就是传递的值
- 注意:Consumer 组件中,写结构的话只能写在回调函数中
const { Provider, Consumer } = React.createContext()
class Hello extends Component {
render() {
return (
<Provider value="blue">
<div>
<h1>我是祖宗</h1>
<Node />
</div>
</Provider>
)
}
}
function Node(e) {
return <Child1 />
}
function Child1(e) {
return <Child2 />
}
function Child2(e) {
return <Child3 />
}
function Child3(e) {
return <Consumer>{(data) => <span>{data}</span>}</Consumer>
四、生命周期
1. 概述
- 组件的生命周期有利于理解组件的运行方式,完成复杂的组件功能、分析组件错误的原因等
- 生命周期:组件从创建到挂载到页面中运行,再到组件不用时卸载的过程
- 生命周期的每个阶段都有对应的方法调用,这些方法就是生命周期的 钩子函数
- 钩子函数的作用:让开发人员可以在不同阶段操作组件
- 只有 类组件 才有生命周期
2. 创建(挂载阶段)
- constructor(1)
- 创建组件时,最先执行
- 初始化 state
- 为事件处理程序绑定 this
- render(2)
- 每次组件渲染都会触发
- 渲染 UI
- 注意:不能调用 setState(),因为使用该方法能会改变状态和视图,发生改变后,又会触发 render,造成递归更新,react 限制了更新的次数,防止无限循环
- componentDitMount(3)
- 组件挂载(完成 DOM 渲染)后触发
- 发送网络请求
- DOM 操作
3. 更新(更新阶段)
-
setState()、forceUpdate、组件接收到新的 props 值,组件就会重新渲染
-
render(1)
-
componentDitUpdate(2)
-
当状态发生变化完成 DOM 渲染后立即触发,首次渲染不触发
-
可以对更新前后的数据进行比较,从而执行对应的操作
-
发送网络请求
-
DOM 操作
-
使用 setState() 必须放在条件里,不能直接调用 setState(),因为使用该方法能会改变状态和视图,
-
发生改变后,又会触发 render,造成递归更新,react 限制了更新的次数,防止无限循环
-
4. 卸载(卸载阶段)
- 组件从页面消失时触发
- componentWillUnmount
- 执行清理工作(定时器)
- 当组件中执行了一些不是 react 自身的操作的话,就需要在当前钩子函数中进行清理,否则可能造成内存泄漏
五、render-props 模式
1. 思路
- 将复用的 state 和操作 state 的方法封装到一个组件中
- 如何拿到该组件中复用的 state
- 添加一个值为 函数的prop,通过 函数参数 来获取(组件内部实现)
- 如何渲染任意的 UI?
- 使用 该函数的返回值 作为渲染的 UI 结构(组件内部实现)
Mouse
// state 复用的状态
render={(state) => (
// 需要渲染的 UI
<h1>
鼠标的坐标:{state.x},{state.y}
</h1>
)}
/>,
2. props
- 创建 Mouse 组件,在组件中提供复用的 状态逻辑 代码(状态、操作状态的方法)
- 将 复用的状态 作为 props.render(state) 方法的参数,暴露到组件外部
- 使用 props.render() 的 返回值 作为渲染的内容(UI、state)
- 即可以通过函数的参数,拿到组件内部的 state,也可以通过函数的返回值指定渲染的内容,做到组件状态逻辑复用
class Mouse extends React.Componen t {
state = {
x: 0,
y: 0,
}
// 鼠标移动事件处理程序
handleMouse = (e) => {
console.log(e)
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 监听鼠标事件
componentDidMount() {
document.addEventListener('mousemove', this.handleMouse)
}
// 组件消失时移除事件绑定,因为它不是 react 自身的操作,不会自动清理
componentWillUnmount() {
document.removeEventListener('mouseover', this.handleMouse)
}
render() {
// 将当前状态传递给 props 传递的方法,渲染方法返回的 UI
return this.props.render(this.state)
}
}
// 添加校验
Mouse.propTypes = {
children: PropTypes.func.isRequired,
}
// 传递数据
root.render(
<div>
<Mouse
// state 复用的状态
render={(state) => (
// 需要渲染的 UI
<h1>
鼠标的坐标:{state.x},{state.y}
</h1>
)}
/>,
// 使用共同的数据,展示不同的 UI
<Mouse
// 接收传递的参数,返回需要展示的 UI 和 state
render={(state) => (
<div>
{state.x}
<img
width={100}
src={img}
style={{
position: 'absolute',
top: state.y - 35,
left: state.x - 50,
}}
alt="react"
/>
</div>
)}
/>
</div>
)
3. children(推荐)
render() {
// 将当前状态传递给 props 传递的方法,渲染方法返回的 UI
return this.props.children(this.state)
}
<Mouse>
{(state) => (
<h1>
鼠标的坐标:{state.x},{state.y}
</h1>
)}
</Mouse>
<Mouse>
{(state) => (
<img
width={100}
src={img}
style={{
position: 'absolute',
top: state.y - 35,
left: state.x - 50,
}}
alt="react"
/>
)}
</Mouse>
4. 总结
- 当组件之间有共同的 state 时,但展示的 UI 不同时,可以使用该模式,为了实现 状态逻辑复用
- 并不是该模式叫 render props 就必须使用名为 render 的 prop,可以使用任意名称的 prop
- prop 是一个函数并且告诉组件要渲染什么内容的技术叫:render props 模式
六、高阶组件
1. 概述
- 目的:实现 状态逻辑复用
- 采用:包装(装饰)模式
- 比如说:手机壳
- 手机:获取保护功能
- 手机壳:提供保护功能
- 高阶组件相当于手机壳,通过包装组件,增加组件功能
2. 思路分析
- 高阶组件(HOC,Higher-Order Component)是 一个函数 ,接收要包装的组件,返回增强后的组件
- 高阶组件内部 创建一个类组件,在这个类组件中 提供复用的状态逻辑 代码,通过 prop 将复用的状态传递给被包装的组件
3. 使用步骤
- 创建一个函数,名称约定 with 开头
- 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
- 函数内部创建一个类组件,提供复用的状态逻辑代码,并返回该组件
- 在该组件中,渲染参数组件,同时将值通过 prop 传递给参数组件
- 调用该高阶组件函数,传入要增强的组件,通过返回值拿到增强后的组件,并渲染到页面中
// 展示数值
const Test = (props) => {
return (
<h1>
鼠标的坐标:{props.x},{props.y}
</h1>
)
}
// 图标跟随鼠标移动
const Test1 = (props) => {
return (
<img
width={100}
src={img}
style={{
position: 'absolute',
top: props.y - 35,
left: props.x - 50,
}}
alt="react"
/>
)
}
// 接收包装好的组件,进行渲染
const WrapedComponent = withMouse(Test)
const WrapedComponent1 = withMouse(Test1)
// withMouse 包装组件的函数
// WrapedComponent 需要包装的组件
function withMouse(WrapedComponent) {
// Mouse 提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0,
}
// 鼠标移动事件处理程序
handlerMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 控制鼠标状态逻辑
componentDidMount() {
document.addEventListener('mousemove', this.handlerMouseMove)
}
// 组件消失时移除事件绑定,因为它不是 react 自身的操作,不会自动清理
componentWillUnmount() {
document.removeEventListener('mouseover', this.handlerMouseMove)
}
render() {
// 将复用状态传递给被包装的组件
return <WrapedComponent {...this.state} />
}
}
// 返回提供复用状态的组件
return Mouse
}
root.render(
<div>
<WrapedComponent />
<WrapedComponent1 />
</div>,
)
4. displayName
- 使用高阶组件的问题:得到的两个组件名称相同
- 原因:默认情况下,react 使用 组件名称 作为 displayName
- 解决:为高阶组件设置 displayName 便于调试时区分不同的组件
- displayName 的作用:用于设置组件的名字,使用 react 调试工具好区分组件
- 设置方式:
// 返回的组件名字:当前高阶组件的名字和被包装的组件名字进行拼接
Mouse.displayName = `withMouse${getDisplayName(WrapedComponent)}`
function getDisplayName(componentName) {
// 被包装的组件有 displayName 就返回该值,没有就把组件的 name 值返回或 component
return componentName.displayName || componentName.name || 'component'
}
5. props 丢失
- 原因:高阶组件函数返回的一个增强后的组件,传递 props 相当于给高阶组件函数中的组件传递,并没有继续往下传递
- 解决:使用高阶组件的时候避免渲染包装组件时,需要将 state 和 props 一起传递组件
<WrapedComponent {...this.state} {...this.props} />
七、总结
- 组件通讯是构建 react 应用必不可少的一环
- props 的灵活性让组件更加强大
- 状态提升是 react 组件的常用模式
- 组件的生命周期有助于理解组件的运行过程
- 钩子函数可以让开发者在特定的时机执行某些功能
- render props 模式和告诫组件都可实现组件的状态逻辑复用
- 组件极简模型:(state,props) => UI