React18 - 组件化开发(二)

192 阅读6分钟

插槽

在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素

我们应该让使用者可以决定某一块区域到底存放什么内容

因为React中所有元素都是ReactElement对象,所以在React中不需要存在对应的插槽

我们可以使用如下两种方式来模拟插槽:

  1. 组件的children子元素
  2. props属性传递React元素

children模拟插槽

每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容

当只有一个子元素的时候,props.children就是那个子元素

如果存在多个子元素的时候,props.children是由所有的子元素组成的数组

父组件

export class App extends PureComponent {
	render() {
		return (
			<Cpn>
				<button>left</button>
				<h2>center</h2>
				<div>right</div>
			</Cpn>
		)
	}
}

子组件

export class Cpn extends PureComponent {
	render() {
		let { children } = this.props

		children = Array.isArray(children) ? children : [ children ]

		return (
			<div className='wrapper'>
				<div className="left">
					{ children[0] }
				</div>
				<div className="center">
					{ children[1] }
				</div>
				<div className="right">
					{ children[2] }
				</div>
			</div>
		)
	}
}

props实现插槽

通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的元素

另外一个种方案就是使用 props 实现

通过具体的属性名,可以让我们在传入和获取时更加的精准

一般为了见名知意,对应的属性名会以Slot结尾

父组件

export class App extends PureComponent {
	render() {
		return (
			<Cpn
				leftSlot={<button>left</button>}
				centerSlot={<h2>center</h2>}
				rightSlot={<div>right</div>}
			/>
		)
	}
}

子组件

export class Cpn extends PureComponent {
	render() {
		let { leftSlot, centerSlot, rightSlot } = this.props

		return (
			<div className='wrapper'>
				<div className="left">
					{ leftSlot }
				</div>
				<div className="center">
					{ centerSlot }
				</div>
				<div className="right">
					{ rightSlot }
				</div>
			</div>
		)
	}
}

模拟作用域插槽

父组件

export class App extends PureComponent {
	render() {
		return (
			<Cpn
				leftSlot={ctx => <button>{ ctx }</button>}
				centerSlot={ctx => <h2>{ ctx }</h2>}
				rightSlot={ctx => <div>{ ctx }</div>}
			/>
		)
	}
}

子组件

export class Cpn extends PureComponent {
	constructor(props) {
		super(props)

		this.state = {
			leftCtx: 'leftCtx',
			centerCtx: 'centerCtx',
			rightCtx: 'rightCtx'
		}
	}

	render() {
		let { leftSlot, centerSlot, rightSlot } = this.props
		const { leftCtx, centerCtx, rightCtx } = this.state

		return (
			<div className='wrapper'>
				<div className="left">
					{ leftSlot(leftCtx) }
				</div>
				<div className="center">
					{ centerSlot(centerCtx) }
				</div>
				<div className="right">
					{ rightSlot(rightCtx) }
				</div>
			</div>
		)
	}
}

context

在开发中,比较常⻅的数据传递方式是通过props属性自上而下(由父到子)进行传递

但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)

如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作

为此React提供了一个API:Context,专门用于非父子组件间的数据共享

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

基本使用

/src/context/themeContext.js

import React from 'react'

// 因为在提供数据组件和使用数据组件都需要使用对应的ctx,所以将ctx在单独的js文件中进行导出
// ctx命名约定:
// 1. 首字母大写
// 2. 以Context结尾
export const ThemeContext = React.createContext()

context提供者

import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import { ThemeContext } from '../context/themeContext'

export class App extends PureComponent {
	render() {
		return (
			// 使用ctx的生产者为后代传递对应的数据
			// 需要传递的数据定义在value属性中
			<ThemeContext.Provider value={{color: 'skyblue', warnColor: 'red'}}>
				{/*
					将需要使用数据的组件使用ctx进行包裹
					所以和vue的provide和inject类似
					ctx只能由祖先组件向后代组件传递对应数据

					一个组件可以被多个ctx所包裹
				*/}
        {/*
        	ItemList内部使用了Item组件
 					ItemList组件是本例中的中间测试组件
 				*/}
				<ItemList />
			</ThemeContext.Provider>
		)
	}
}

export default App

数据使用者

类组件

import React, { PureComponent } from 'react'
// 在需要使用ctx的组件中引入对应组件
import { ThemeContext } from '../../../context/themeContext'

export class Item extends PureComponent {
	// 因为可能提供了多个ctx,所以需要指定使用哪一个ctx
	static contextType = ThemeContext

	render() {
		return (
			<div>
				{/*
					我们可以使用this.context来获取到指定的context
					如果没有设置contextType对应的值,那么this.context的值为{}
				*/}
				{ this.context.color } - { this.context.warnColor }
			</div>
		)
	}
}

export default Item

函数组件

import React from 'react'
import { ThemeContext } from '../../../context/themeContext'

function Item() {
	return (
    // 函数组件中没有this指向
    // 所以需要使用context中的消费者组件
    <ThemeContext.Consumer>
      {
        // 消费者组件中需要传递一个返回需要渲染vDOM的函数作为其children
        // 在实际被调用的时候会将共享的数据传递过来
        theme => {
					/* Consumer中对应的函数需要返回实际所需要渲染的vDOM */
          return <div>
            <div>{ theme.color }</div>
            <div>{ theme.warnColor }</div>
          </div>
        }
      }
    </ThemeContext.Consumer>
	)
}

export default Item

相关概念

React.createContext

  • 创建一个需要共享的Context对象
  • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

Context.Provider

  • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
  • Provider 接收一个 value 属性,传递给消费组件
  • 一个 Provider 可以和多个消费组件有对应关系
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据
  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染

Class.contextType

  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象
  • 这能让你使用 this.context 来消费最近 Context 上的那个值
  • 可以在任何生命周期中访问到它,包括 render 函数中

Context.Consumer

  • React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context
  • 这里需要 函数作为子元素(function as child)这种做法
  • 这个函数接收当前的 context 值,返回一个 React 节点

也就是说,以下场景需要使用Context.Consumer

  1. 当使用value的组件是一个函数式组件时
  2. 当组件中需要使用多个Context时

数据生产者

import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import { ThemeContext } from '../context/themeContext'
import { UserContext } from '../context/userContext'

export class App extends PureComponent {
	render() {
		return (
			<ThemeContext.Provider value={{color: 'skyblue', warnColor: 'red'}}>
				<UserContext.Provider value={{name:'Klaus', age: 23}}>
					<ItemList />
				</UserContext.Provider>
			</ThemeContext.Provider>
		)
	}
}

export default App

数据消费者

import React, { PureComponent } from 'react'
import { ThemeContext } from '../../../context/themeContext'
import { UserContext } from '../../../context/userContext'

export class Item extends PureComponent {
	static contextType = ThemeContext

	render() {
		return (
			<div>
				{ this.context.color } - { this.context.warnColor }
				<UserContext.Consumer>
					{
						userInfo => <div>{userInfo.name} - {userInfo.age}</div>
					}
				</UserContext.Consumer>
			</div>
		)
	}
}

export default Item

默认值

import React from 'react'

// 在createContext方法中可以传入一个对象作为context的默认值
// 如果使用context的组件并不是ThemeContext.Provider的后代组件的时候,就会启用默认值
export const ThemeContext = React.createContext({color: 'gray', warnColor: 'yellow'})

事件总线

/src/utils/index.js

import Mitt from 'mitt'

export default Mitt()

父组件

import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import mitt from '../utils'

export class App extends PureComponent {
	constructor(props) {
		super(props)

		this.state = {
			name: 'Klaus'
		}
	}

	// 因为事件总线中的this指向的值是undefined
	// 所以为了在函数内部可以获取正确的this指向,推荐将函数编写为箭头函数
	changeName = name => {
		this.setState({
			name
		})
	}

	componentDidMount() {
		mitt.on('changeName', this.changeName)
	}

	componentWillUnmount() {
		mitt.off('changeName', this.changeName)
	}

	render() {
		return (
			<div>
				<div>{ this.state.name }</div>
				<ItemList />
			</div>
		)
	}
}

export default App

子组件

import React, { PureComponent } from 'react'
import mitt from '../../../utils'

export class Item extends PureComponent {
	changeName() {
    // mitt在触发事件的时候,只能传递两个参数
    // 参数一: 事件名
    // 参数二: 需要传递的参数,如果需要传递多个参数可以使用对象进行传递
		mitt.emit('changeName', 'Steven')
	}

	render() {
		return (
			<div>
				<button onClick={this.changeName}>change</button>
			</div>
		)
	}
}

export default Item