组件通讯
- 组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。
- 在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能。
- 而在这个过程中,多个组件之间不可避免的要共享某些数据。
- 为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
- 在react中,数据来源主要有两个,一个是state内部数据,另一个是props外部数据,这两个数据发生改变后都会引起组件的更新渲染。
- 父组件在展示子组件,可能会传递一些数据给子组件:
- 父组件通过 属性=值 的形式来传递给子组件数据;
- 子组件通过 props 参数获取父组件传递过来的数据
props
- 组件是封闭的,要接收外部数据应该通过props来实现
- props的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过参数props接收数据,类组件通过this.props接收数据
函数组件通讯
基本数据类型传递
子组件
import React, { memo } from 'react'
// import PropTypes from 'prop-types'
function demoFuncClassComponent(props) {
return (
<div>
<h4>函数组件</h4>
<p>{props.title}</p>
</div>
)
}
// 定义props的类型
// demoFuncClassComponent.propTypes = {}
export default memo(demoFuncClassComponent)
父组件
import './App.css'
import DemoFuncClassComponent from './components/demoFuncClassComponent'
function App() {
return (
<div className="App">
<DemoFuncClassComponent title="我是父组件传递给子组件的数据" />
</div>
)
}
export default App
引用数据类型传递
如果子组件接收的数据是一个一个的字段,在对象中有很多数据需要传入,那么你可以使用es6中的扩展语法将对象的数据全部扩展进去
子组件Props.jsx
import React, { memo } from 'react'
// import PropTypes from 'prop-types'
function demoFuncClassComponent(props) {
return (
<div>
<h4>函数组件</h4>
<p>{props.title}</p>
<p>{props.name}</p>
<p>{props.age}</p>
<p>{props.address}</p>
</div>
)
}
// 定义props的类型
// demoFuncClassComponent.propTypes = {}
export default memo(demoFuncClassComponent)
父组件App.jsx
import './App.css'
import DemoFuncClassComponent from './components/demoFuncClassComponent'
let user = {
name: '张三',
age: 20,
address: '渝北区',
}
function App() {
return (
<div className="App">
<DemoFuncClassComponent title="我是父组件传递给子组件的数据" {...user} />
</div>
)
}
export default App
如果子组件接收的是整个对象,那么你可以直接将对象传入
子组件Props.jsx
import React from 'react'
export default function Props(props) {
console.log(props);
return (
<div>
标题:{props.title}
<br />
姓名:{props.user.name}
<br />
年龄:{props.user.age}
<br />
地址:{props.user.address}
</div>
)
}
父组件App.jsx
import Props from "./components/Props";
let user = {
name: '张三',
age: 20,
address: '渝北区'
}
function App() {
return (
<div>
<Props title='标题' user={user} />
</div>
);
}
export default App;
类组件通讯
类组件的外部数据通过this.props
来访问
import React, { Component } from 'react'
export default class PropsClass extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
标题:{this.props.title}
</div>
)
}
}
父组件:App.jsx传入数据
import Props from "./components/Props";
function App() {
return (
<div>
<Props title='标题' />
</div>
);
}
export default App;
props的特点
- 可以给组件传递任意类型的数据
- props是只读的,不允许修改props的数据
-
注意:在类组件中使用的时候,需要把props传递给super(),否则构造函数无法获取到props
class Hello extends React.Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
}
render() {
return接收到的数据:{this.props.age}
}
}
组件通讯方式
- 父传子
- 子传父
- 非父子
父传子
在vue中,父传子通过props传递,子传父通过$emit
触发自定义事件进行传参。
在react中,父子组件通信,父传子通过 props****传递;子传父通过调用父组件传递的函数通知父组件进行修改数据:
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为 state 中的数据
- 子组件中通过 props 接收父组件中传递的数据
App.js
import './App.css';
import React from 'react';
import Parent from './components/Parent'
function App() {
return (
<div>
<Parent/>
</div>
);
}
export default App;
父组件:提供数据并且传递给子组件--components/Parent.jsx
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
title: '星期一'
}
render() {
return (
<div>
<Child title={this.state.title}></Child>
</div>
)
}
}
子组件:接收数据
import React, { Component } from 'react'
export default class Child extends Component {
render() {
return (
<div>
<p>接收父组件传递过来的参数--{this.props.title}</p>
</div>
)
}
}
子传父
思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。
- 父组件提供一个回调函数(用于接收数据)
- 并将该函数作为属性值,传递给子组件
- 子组件通过 props 调用回调函数
- 将子组件的数据作为参数传递给回调函数
App.js
import './App.css';
import React from 'react';
import Parent from './components/Parent'
function App() {
return (
<div>
<Parent/>
</div>
);
}
export default App;
父组件提供函数--components/Parent.jsx
类似于js里的将函数当做参数传递给子组件
import React, { Component } from 'react';
import ClassSub from './classSub';
class classHello extends Component {
constructor(){
super()
this.state = {
counter: 0
}
}
// 父组件提供一个回调函数(用于接收数据)
changeCounter = (count) => {
this.setState({
counter: this.state.counter + count
})
}
render() {
const {counter} = this.state
return (
<div>
<p>当前值:{counter}</p>
<ClassSub changeCounter = {(count) => this.changeCounter(count)} />
</div>
);
}
}
export default classHello;
子组件接收函数并且调用--components/Child.jsx
import React, { Component } from 'react'
export default class classSub extends Component {
handlerClick = (count) => {
this.props.changeCounter(count)
}
render() {
return (
<div>
<button onClick={() => this.handlerClick(1)}>子传父+1</button>
<button onClick={() => this.handlerClick(5)}>子传父+5</button>
<button onClick={() => this.handlerClick(10)}>子传父+10</button>
</div>
)
}
}
父子通讯示例
tab栏
利用父传子,子传父
父组件:
import React, { Component } from 'react'
import TabControl from './TabControl'
export default class classHello extends Component {
constructor(){
super()
this.state = {
titles: ["流行","新款", "精选"],
tabIndex: 0
}
}
tabClick = (index) => {
this.setState({
tabIndex: index
})
}
render() {
const {titles, tabIndex} = this.state
return (
<div>
<TabControl titles={titles} tabClick={(index) => this.tabClick(index)} />
<div>{titles[tabIndex]}</div>
</div>
)
}
}
子组件:
import React, { Component } from 'react';
import './TabControl.css'
class TabControl extends Component {
constructor(){
super()
this.state = {
currentIndex: 0,
}
}
itemClick = (index) => {
this.setState({
currentIndex: index
})
this.props.tabClick(index)
}
render() {
const {titles} = this.props
const {currentIndex} = this.state
return (
<div className='tab-control'>
{
titles.map((item, index) => {
return (
<div
className={`item ${index === currentIndex ? 'active' : ''}`}
key={item}
onClick={() => this.itemClick(index)}
>
<span className='text'>{item}</span>
</div>
)
})
}
</div>
);
}
}
export default TabControl;
样式:
.tab-control {
display: flex;
align-items: center;
text-align: center;
height: 40px;
}
.tab-control .item {
flex: 1;
}
.tab-control .item.active {
color: red;
}
.tab-control .item.active .text {
border-bottom: 3px solid red;
padding: 3px;
}
props 和 callback 一起使用
// 子组件
function Son(props) {
const { fatherSay, sayFather } = props;
return (
<div className="son">
我是子组件
<div>父组件对我说:{ fatherSay }</div>
<input placeholder="我对父组件说" onChange={ e => sayFather(e.target.value)}></input>
</div>
)
}
// 父组件
function Father() {
const [ childSay, setChildSay ] = useState("");
const [ fatherSay, setFatherSay ] = useState("");
return(
<div className="box father">
我是父组件
<div>子组件对我说:{ childSay }</div>
<input placeholder="我对子组件说" onChange={ e => setFatherSay(e.target.value)}></input>
<Son fatherSay={fatherSay} sayFather={setChildSay}></Son>
</div>
)
}
组件插槽实现
在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素
我们应该让使用者可以决定某一块区域到底存放什么内容。
这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?
React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的children子元素
- props属性传递React元素
children实现插槽
- 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容
- 通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的元素
父组件:
import React, { Component } from 'react'
import NavBar from './NavBar'
export default class classHello extends Component {
render() {
return (
<div>
<NavBar>
<button>按钮</button>
<h2>标题</h2>
<i>斜体文字</i>
</NavBar>
</div>
)
}
}
注意:如果在父组件里只传递一个元素,那么在子组件里这一个就是children,并不是数组了
例如:
父:
<NavBar>
<i>斜体文字</i>
</NavBar>
子:
<div className="left">{children}</div>
子组件:
import React, { Component } from 'react';
import './navBar.css'
class NavBar extends Component {
render() {
// 注意:放置多个的时候拿到的children就是一个数组
// 放置一个的时候children就是那一个元素/内容
const {children} = this.props
return (
<div className='nav-bar'>
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
);
}
}
export default NavBar;
样式:
*{
padding: 0;
margin: 0;
}
.nav-bar {
display: flex;
height: 40px;
text-align: center;
line-height: 40px;
}
.left, .right {
width: 80px;
background-color: green;
}
.center {
flex: 1;
background-color: aqua;
}
props实现插槽
- 通过具体的属性名,可以让我们在传入和获取时更加的精准
父组件:
import React, { Component } from 'react'
import NavBar from './NavBar'
export default class classHello extends Component {
render() {
const lefeSlot = <button>按钮</button>
const centerSlot = <h2>标题</h2>
const rightSlot = <i>斜体文字</i>
return (
<div>
<NavBar lefeSlot={lefeSlot} centerSlot={centerSlot} rightSlot={rightSlot} />
</div>
)
}
}
子组件:
import React, { Component } from 'react';
import './navBar.css'
class NavBar extends Component {
render() {
const {lefeSlot, centerSlot, rightSlot} = this.props
return (
<div className='nav-bar'>
<div className="left">{lefeSlot}</div>
<div className="center">{centerSlot}</div>
<div className="right">{rightSlot}</div>
</div>
);
}
}
export default NavBar;
样式:
*{
padding: 0;
margin: 0;
}
.nav-bar {
display: flex;
height: 40px;
text-align: center;
line-height: 40px;
}
.left, .right {
width: 80px;
background-color: green;
}
.center {
flex: 1;
background-color: aqua;
}
作用域插槽
- 根据回调函数可以返回不同的内容来实现
- 组件的标签由父组件决定,但是渲染的内容由子组件决定
父组件:
import React, { Component } from 'react'
import TabControl from './TabControl'
export default class classHello extends Component {
constructor(){
super()
this.state = {
titles: ["流行","新款", "精选"],
tabIndex: 0
}
}
tabClick = (index) => {
this.setState({
tabIndex: index
})
}
getTabItem = (item) => {
if(item === '流行'){
return <span>{item}</span>
}else if(item === '新款'){
return <button>{item}</button>
} else {
return <i>{item}</i>
}
}
render() {
const {titles, tabIndex} = this.state
return (
<div>
<TabControl
titles={titles}
tabClick={(index) => this.tabClick(index)}
/**itemType={(item) => <span>{item}</span>} */
itemType={item => this.getTabItem(item)}
/>
<div>{titles[tabIndex]}</div>
</div>
)
}
}
子组件:
import React, { Component } from 'react';
import './TabControl.css'
class TabControl extends Component {
constructor(){
super()
this.state = {
currentIndex: 0,
}
}
itemClick = (index) => {
this.setState({
currentIndex: index
})
this.props.tabClick(index)
}
render() {
const {titles, itemType} = this.props
const {currentIndex} = this.state
return (
<div className='tab-control'>
{
titles.map((item, index) => {
return (
<div
className={`item ${index === currentIndex ? 'active' : ''}`}
key={item}
onClick={() => this.itemClick(index)}
>
{/* <span className='text'>{item}</span>*/}
{itemType(item)}
</div>
)
})
}
</div>
);
}
}
export default TabControl;
样式:
.tab-control {
display: flex;
align-items: center;
text-align: center;
height: 40px;
}
.tab-control .item {
flex: 1;
}
.tab-control .item.active {
color: red;
}
.tab-control .item.active .text {
border-bottom: 3px solid red;
padding: 3px;
}
兄弟组件
- 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
- 思想:状态提升
- 公共父组件职责:
-
- 提供共享状态
-
- 提供操作共享状态的方法
- 要通讯的子组件只需通过 props 接收状态或操作状态的方法
状态提升前
状态提升之后
利用状态提升
其实就是利用子传父、父传子实现
App.js
import React, { Component } from 'react'
import Jack from './components/Jack'
import Rose from './components/Rose'
export default class App extends Component {
// 1. 状态提升到父组件
state = {
info: ''
}
changeInfo = (info) => {
this.setState({
info
})
}
render() {
return (
<div>
{/*4. 更改属性值*/}
<Jack say={this.changeInfo} />
{/* 2. 将状态给子组件进行显示 */}
<Rose info={this.state.info} />
</div>
)
}
}
components/Jack.jsx
import React, { Component } from 'react'
export default class Jack extends Component {
// 3. 子传父
say = () => {
this.props.say('you jump i look')
}
render() {
return (
<div>
<h3>Jack:</h3>
<button onClick={this.say}>say</button>
</div>
)
}
}
components/Rose.jsx
import React, { Component } from 'react'
export default class Rose extends Component {
render() {
return (
<div>
<h3>Rose</h3>
<p>Jack say:{this.props.info}</p>
</div>
)
}
}
利用 ref 实现
在 React 的开发模式中,通常情况下不需要,也不建议直接操作DOM,但是某些特殊的情况需要获取到DOM进行某些操作:
- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
App.js
import './App.css';
import React from 'react';
import Parent from './components/Parent'
function App() {
return (
<div>
<Parent/>
</div>
);
}
export default App;
components/Parent.jsx
通过父组件作为中转站去获取子组件实例操作子组件的方法或数据
import React, { Component } from "react";
import Child from "./Child";
import Child2 from "./Child2";
export default class Parent extends Component {
constructor(props) {
super(props);
this.childRef = React.createRef();
}
// 在父组件里通过 ref 去调用子组件里的那个更改自己属性值的方法,并将参数传递过去
changeTitle = () => {
this.childRef.current.changeTitle("马上国庆节");
};
render() {
return (
<div>
<Child
ref={this.childRef}
/>
<Child2 onChangeTitle={this.changeTitle} />
</div>
);
}
}
components/Child.jsx
定义内部state的title
import React, { Component } from 'react'
export default class Child extends Component {
state = {
title: '今天星期一'
}
// 在子组件内自定义一个方法去更改自己的属性值
changeTitle(title) {
this.setState({
title
})
}
render() {
return (
<div>
title: {this.state.title}
</div>
);
}
}
components/Child2.jsx
import React, { Component } from 'react'
export default class Child2 extends Component {
updateTitle = () => {
this.props.onChangeTitle();
}
render() {
return (
<div>
<button onClick={this.updateTitle}>修改title</button>
</div>
)
}
}
总结:
这种方式稍微感觉有点麻烦,如果组件嵌套过深,那么通信显得非常繁琐,会一层一层往上传递,然后又一层一层往下传递。
基于事件总线
事件总线的方式,其实就是一种设计模式,被称为订阅和发布模式,设计模式一前辈们总结出来的思想,设计模式有23三种,比如还有单例模式,策略模式,适配模式等等
使用 events 插件
当然利用 eventBus 也可以实现组件通信,但是在 React 中并不提倡用这种方式,我还是更提倡用 props 方式通信。如果说非要用 eventBus,我觉得它更适合用 React 做基础构建的小程序,比如 Taro。
在react没有内置事件总线的方式,我们需要下载第三方插件 events**;**
- 触发事件:emit('事件名称', 参数)
- 监听事件:addListener('事件名称', 监听函数)
- 移除事件:removeLIstener('事件名称', 监听函数)
-
安装插件
yarn add events
or
npm i events
-
创建eventBus文件
src/utils/eventBus/eventBus.jsimport { EventEmitter } from 'events';
export default new EventEmitter(); // 生成一个eventBus实例
-
App.js
import './App.css';
import React from 'react';
import Parent from './components/Parent'function App() {
return (
<Parent/>
);
}export default App;
components/Parent.jsx
import React, { Component } from "react";
import Child from "./Child";
import Child2 from "./Child2";
export default class Parent extends Component {
render() {
return (
<div>
<Child/>
<Child2 />
</div>
);
}
}
-
在组件中订阅和发布事件
订阅事件:Child.jsx
components/Child.jsximport React, { Component } from "react";
import eventBus from "../utils/eventBus/eventBus.js";export default class Child extends Component {
state = {
age: 20,
};//子组件自定义更改年龄方法
changeAge = (age) => {
this.setState({
age,
});
};componentDidMount() {
// 订阅事件
eventBus.addListener("updateAge", this.changeAge);
}
componentWillUnmount() {
// 移除事件
eventBus.removeListener('updateAge', this.changeAge)
}
render() {
return (
age: {this.state.age}
)
}
}
发布事件:Child2.jsx
components/Child2.jsx
import React, { Component } from 'react'
import eventBus from "../utils/eventBus/eventBus.js";
export default class Child2 extends Component {
changeAge = () => {
eventBus.emit('updateAge', 18)
}
render() {
return (
<div>
<button onClick={this.changeAge}>修改age</button>
</div>
)
}
}
使用 hy-event-store 插件
安装:
yarn add hy-event-store
# or
npm i hy-event-store
App.jsx
import React, { Component } from 'react'
import Home from './Home'
import eventBus from './utils/event-bus'
export class App extends Component {
constructor() {
super()
this.state = {
name: "",
age: 0,
height: 0
}
}
componentDidMount() {
// eventBus.on("bannerPrev", (name, age, height) => {
// console.log("app中监听到bannerPrev", name, age, height)
// this.setState({ name, age, height })
// })
eventBus.on("bannerPrev", this.bannerPrevClick, this)
eventBus.on("bannerNext", this.bannerNextClick, this)
}
bannerPrevClick(name, age, height) {
console.log("app中监听到bannerPrev", name, age, height)
this.setState({ name, age, height })
}
bannerNextClick(info) {
console.log("app中监听到bannerNext", info)
}
componentWillUnmount() {
eventBus.off("bannerPrev", this.bannerPrevClick)
}
render() {
const { name, age, height } = this.state
return (
<div>
<h2>App Component: {name}-{age}-{height}</h2>
<Home/>
</div>
)
}
}
export default App
Home.jsx
import React, { Component } from 'react'
import HomeBanner from './HomeBanner'
export class Home extends Component {
render() {
return (
<div>
<h2>Home Component</h2>
<HomeBanner/>
</div>
)
}
}
export default Home
HomeBanner.jsx
import React, { Component } from 'react'
import eventBus from "./utils/event-bus"
export class HomeBanner extends Component {
prevClick() {
console.log("上一个")
eventBus.emit("bannerPrev", "why", 18, 1.88)
}
nextClick() {
console.log("下一个")
eventBus.emit("bannerNext", {nickname: "kobe", level: 99})
}
render() {
return (
<div>
<h2>HomeBanner</h2>
<button onClick={e => this.prevClick()}>上一个</button>
<button onClick={e => this.nextClick()}>下一个</button>
</div>
)
}
}
export default HomeBanner
utils/event-bus.js
import { HYEventBus } from "hy-event-store"
const eventBus = new HYEventBus()
export default eventBus
总结:
eventBus订阅发布的方式给组件之间直接通信仍然存在一些缺陷,订阅和发布事件绑定太多的话,会非常的分散,对于以后的维护来说不太好维护,你就不知道哪里在监听,哪里在触发,哪里要移除事件,不太好管理
我们可以使用其他的方式,例如redux,进行数据的管理
跨级通讯-context
上下文
在 vue 里是通过 provide inject ,但是在 react 中可以通过 context 实现类似的
基本概念
思考:App 组件要传递数据给 Child 组件,该如何处理?
处理方式:使用 props 一层层组件往下传递(繁琐)
更好的用法:使用 Context
作用:跨组件传递数据(比如:主题、语言等)
Context相当于一个公共的存储空间:
- 我们可以将多个组件中需要访问的数据统一存储到一个Context中
- 这样我们无需通过props逐层传递,即可使组件中访问到这些数据
相关API
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={/某个值/}></MyContext>
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>
什么时候会用到Context.Consumer?当使用value的组件是一个函数组件时、当组件中需要用到多个Context时
使用方式一
类组件中:
context/them-context.js
import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()
export default ThemContext
祖先组件:classHello
import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './them-context/them-context'
class classHello extends Component {
render() {
return (
<div>
<ThemContext.Provider value={{color: 'red', size: '30'}}>
<Home />
</ThemContext.Provider>
</div>
);
}
}
export default classHello;
父组件:Home
import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
export class Home extends Component {
render() {
return (
<div>
Home
<hr />
<HomeInfo />
</div>
)
}
}
export default Home
孙组件:HomeInfo
import React, { Component } from 'react'
import ThemContext from './them-context/them-context'
export class HomeInfo extends Component {
render() {
console.log(this.context);
return (
<div>HomeInfo:{this.context.color}</div>
)
}
}
HomeInfo.contextType = ThemContext
export default HomeInfo
函数组件中:
context/them-context.js
import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()
export default ThemContext
祖先组件:
import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './them-context/them-context'
class classHello extends Component {
render() {
return (
<div>
<ThemContext.Provider value={{color: 'red', size: '30'}}>
<Home />
</ThemContext.Provider>
</div>
);
}
}
export default classHello;
父组件:
import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
export class Home extends Component {
render() {
return (
<div>
Home
<hr />
<HomeInfo />
</div>
)
}
}
export default Home
孙组件:
import React from 'react'
import ThemContext from './them-context/them-context'
export default function HomeBanner() {
return (
<div>
HomeBanner(函数组件):
{
<ThemContext.Consumer>
{
value => {
return <span>{value.color}</span>
}
}
</ThemContext.Consumer>
}
</div>
)
}
多个context:
context/them-context.js
import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()
export default ThemContext
context/user-context.js
import React from 'react';
// 创建一个上下文
const UserContext = React.createContext()
export default UserContext
祖先组件:
import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './context/them-context'
import UserContext from './context/user-context'
class classHello extends Component {
render() {
return (
<div>
<UserContext.Provider value={{nickName: 'zs'}}>
<ThemContext.Provider value={{color: 'red', size: '30'}}>
<Home />
</ThemContext.Provider>
</UserContext.Provider>
</div>
);
}
}
export default classHello;
父组件:
import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
import HomeBanner from './HomeBanner'
export class Home extends Component {
render() {
return (
<div>
Home
<hr />
<HomeInfo />
<HomeBanner />
</div>
)
}
}
export default Home
孙组件:
import React, { Component } from 'react'
import ThemContext from './context/them-context'
import UserContext from './context/user-context'
export class HomeInfo extends Component {
render() {
console.log(this.context);
return (
<div>
<p>HomeInfo(类组件):{this.context.color}</p>
<UserContext.Consumer>
{
value => {
return <h2>Info User:{value.nickName}</h2>
}
}
</UserContext.Consumer>
</div>
)
}
}
HomeInfo.contextType = ThemContext
export default HomeInfo
孙组件:
import React from 'react'
import ThemContext from './context/them-context'
export default function HomeBanner() {
return (
<div>
HomeBanner(函数组件):
{
<ThemContext.Consumer>
{
value => {
return <span>{value.color}</span>
}
}
</ThemContext.Consumer>
}
</div>
)
}
使用方式二
步骤:
1.引入context组件
import TestContext from '../store/testContext';
2.使用 Xxx(模块名).Consumer 组件来创建元素
Consumer的标签体需要一个回调函数
它会将context设置为回调函数的参数,通过参数就可以访问到context对象
- 回调函数中,形参ctx就是Context中的对象
store/testContext.jsx
import React from "react";
const TestContext = React.createContext({
name: "孙悟空",
age: 18,
});
export default TestContext;
App.js
import React, { Component } from 'react'
import TestContext from './store/testContext';
export default class App extends Component {
render() {
return <TestContext.Consumer>
{
(ctx) => {
return <div>
{ctx.name} - {ctx.age}
</div>
}
}
</TestContext.Consumer>
}
}
使用方式三
App.js
import React, { Component } from 'react'
import Father from './components/Father'
// 1. createContext
// 2. Provider 包裹根元素,Provider组件就是最大的根元素
const {Provider, Consumer}= React.createContext()
export { Consumer }
export default class App extends Component {
state = {
color: 'green'
}
render() {
return (
<Provider value={this.state.color}>
<div>
<Father />
</div>
</Provider>
)
}
}
components/Father.jsx
import React, { Component } from 'react'
import Son from './Son'
export default class Father extends Component {
render() {
return (
<div>
<h3>Father</h3>
<Son />
</div>
)
}
}
components/Son.jsx
import React, { Component } from 'react'
import Sun from './Sun'
import {Consumer} from '../App'
export default class Son extends Component {
render() {
return (
<div>
<Consumer>
{
(value) => <h3>Son--{value}</h3>
}
</Consumer>
<Sun />
</div>
)
}
}
components/Sun.jsx
import React, { Component } from 'react'
import {Consumer} from '../App'
export default class Sun extends Component {
render() {
return (
<div>
<h3>Sun</h3>
<Consumer>
{
(value) => <div style={{color: value}}>孙--{value}</div>
}
</Consumer>
</div>
)
}
}
高级:
App.js
import React, { Component } from 'react'
import Father from './components/Father'
// 1. createContext
// 2. Provider 包裹根元素,Provider组件就是最大的根元素
const {Provider, Consumer}= React.createContext()
export { Consumer }
export default class App extends Component {
state = {
color: 'green',
bgColor: 'pink'
}
changeColor = (color) => {
this.setState({
color
})
}
render() {
const {color, bgColor} = this.state
return (
<Provider value={{color, bgColor, changeColor:this.changeColor}}>
<div>
<Father />
</div>
</Provider>
)
}
}
components/Sun.jsx
import React, { Component } from 'react'
import {Consumer} from '../App'
export default class Sun extends Component {
render() {
return (
<div>
<h3>Sun</h3>
<Consumer>
{
({color, bgColor, changeColor}) =>
<div>
<div style={{color, backgroundColor: bgColor}}>孙--</div>
<button onClick={() => changeColor('skyblue')}>修改</button>
</div>
}
</Consumer>
</div>
)
}
}
总结:
- 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
- Context提供了两个组件:Provider 和 Consumer
- Provider组件:用来提供数据
- Consumer组件:用来消费数据
对于复杂的组件通讯,将来会用 redux
状态管理方式
React-redux 或 React-mobx 状态管理方式。
对于这种方式在后面会有专门的章节去讲,这里就不去叙述了。
props深入
children属性
-
children属性:表示该组件的子节点,只要组件有子节点,props就有该属性
-
children 属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)
function Hello(props) {
return (
该组件的子节点:{props.children}
)
}<Hello>我是子节点</Hello>
props校验
- 目的:校验接收的props的数据类型,增加组件的健壮性
- 对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
- 如果传入的数据格式不对,可能会导致组件内部报错。组件的使用者不能很明确的知道错误的原因
- 更多校验知识请参考官网:https://zh-hans.reactjs.org/docs/typechecking-width-proptypes.html
- props校验允许在创建组件的时候,就约定props的格式、类型等
- 作用:规定接收的props的类型必须为数组,如果不是数组就会报错,增加组件的健壮性。
通过静态属性设置propTypes
属性去校验外部数据的类型
propTypes
是固定的单词,不能写错了,写错了校验不生效。
如果是老版本,你需要手动下载prop-types
插件
yarn add prop-types
- 导入 prop-types 包
- 使用组件名.propTypes = {} 来给组件的props添加校验规则
-
校验规则通过 PropTypes 对象来指定
import PropTypes from "prop-types"
MainBanner.propTypes = {
banners: PropTypes.array.isRequired,
title: PropTypes.string
}
类组件中
可以在类外部设置静态属性propTypes
import React, { Component } from 'react'
import PropTypes from 'prop-types';
class PropsClass extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
标题:{this.props.title}
</div>
)
}
}
PropsClass.propTypes = {
title: PropTypes.string.isRequired,
isShow: PropTypes.bool
}
export default PropsClass;
也可以在类内部设置static静态属性propTypes
import React, { Component } from 'react'
import PropTypes from 'prop-types';
class PropsClass extends Component {
static propTypes = {
title: PropTypes.string,
isShow: PropTypes.bool
}
constructor(props) {
super(props);
}
render() {
return (
<div>
标题:{this.props.title}
</div>
)
}
}
export default PropsClass;
函数组件
import React from 'react'
import PropTypes from 'prop-types';
function Props(props) {
console.log(props);
return (
<div>
标题:{props.title}
<br />
姓名:{props.user.name}
<br />
年龄:{props.user.age}
<br />
地址:{props.user.address}
</div>
)
}
Props.propTypes = {
title: PropTypes.string.isRequired
}
export default Props;
约束规则
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
-
特定结构的对象:shape({})
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
props默认值
场景:分页组件 每页显示条数
作用:给 props 设置默认值,在未传入 props 时生效
类组件中使用props默认值
- 通过静态属性
defaultProps
设置外部数据的默认值 defaultProps
这个单词是固定的,不能写错,写错了默认值不生效。
1)在类里面通过static
设置静态属性
import React, { Component } from 'react'
class PropsClass extends Component {
static defaultProps = {
title: '标题'
}
constructor(props) {
super(props);
}
render() {
return (
<div>
标题:{this.props.title}
</div>
)
}
}
export default PropsClass;
2)可以在类外部设置静态属性--推荐
import React, { Component } from 'react'
class PropsClass extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
标题:{this.props.title}
</div>
)
}
}
PropsClass.defaultProps = {
title: '标题'
}
export default PropsClass;
这两种方式是等价的,都是静态属性。
函数组件中使用props默认值
-
外部数据props的默认值,需要在组件上添加静态属性
defaultProps
-
defaultProps
这个单词是固定的,不能写错,写错了默认值不生效。import React from 'react'
function Props(props) {
return (
)
}Props.defaultProps = {
title: '标题'
}
export default Props;
组件性能优化
- 功能第一
- 性能优化
减轻state
- 减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)
- 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等
- 对于这种需要在多个方法中用到的数据,应该直接放在 this 中
-
- this.xxx = 'bbb'
-
-
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() { ... }
}
-
vue中不要把和渲染无关的数据放到data中
避免不必要的重新渲染
- 组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
- 问题:子组件没有任何变化时也会重新渲染 (接收到的props没有发生任何的改变)
- 如何避免不必要的重新渲染呢?
- 解决方式:使用钩子函数
shouldComponentUpdate(nextProps, nextState)
- 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染
-
触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate => render)
class Hello extends Component {
shouldComponentUpdate() {
// 根据条件,决定是否重新渲染组件
return false
}
render() {...}
}
案例:随机数
纯组件
- 纯组件:
React.PureComponent
与React.Component
功能相似
- 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较
-
原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件
class Hello extends React.PureComponent {
render() {
return (
纯组件
)
}
}
只有在性能优化的时候可能会用到纯组件,不要所有的组件都使用纯组件,因为纯组件需要消耗性能进行对比
纯组件比较-值类型
- 说明:纯组件内部的对比是 shallow compare(浅层对比)
-
对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // falsestate = { number: 0 }
setState({
number: Math.floor(Math.random() * 3)
})
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件
纯组件比较-引用类型
- 说明:纯组件内部的对比是 shallow compare(浅层对比)
-
对于引用类型来说:只比较对象的引用(地址)是否相同
const obj = { number: 0 }
const newObj = obj
newObj.number = 2
console.log(newObj === obj) // truestate = { obj: { number: 0 } }
// 错误做法
state.obj.number = 2
setState({ obj: state.obj })
// PureComponent内部比较:
最新的state.obj === 上一次的state.obj // true,不重新渲染组件
纯组件的最佳实践:
注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!
// 正确!创建新数据
const newObj = {...state.obj, number: 2}
setState({ obj: newObj })
// 正确!创建新数据
// 不要用数组的push / unshift 等直接修改当前数组的的方法
// 而应该用 concat 或 slice 等这些返回新数组的方法
this.setState({
list: [...this.state.list, {新数据}]
})