组件跨层级通信 - Context
范例:模拟redux存放全局状态,在组件间共享
// 组件跨层级通信import React, { Component } from 'react'// 1、创建上下文对象const Context = React.createContext()// 2、获取 Provider 和 Consumerconst { Provider, Consumer } = Context// Child显示计数器,并能修改它,多个Child之间需要共享数据function Child(props) {return (<div onClick={ () => props.add() }>{props.counter}</div>)}export default class ContextTest extends Component {// state 是要传递的数据state = {counter: 0}// add 方法可以修改状态add = () => {this.setState({ counter: this.state.counter + 1 })}render() {return (<Provider value={{ counter: this.state.counter, add: this.add }}><Consumer>{ value => <Child {...value}></Child> }</Consumer><Consumer>{ value => <Child {...value}></Child> }</Consumer><Consumer>{ value => <Child {...value}></Child> }</Consumer></Provider>)}}
高阶组件-HOC
HOC参考
高阶组件:高阶组件是一个工厂函数,它接收一个组件并返回另一个组件
范例:为展示组件添加获取数据的能力
import React from 'react'// Lesson 保证功能单一,它不关心数据来源,只负责显示function Lesson(props) {return (<div>{props.stage} - {props.title}</div>)}// 模拟数据const lessons = [{stage: '高中',title: '数学'},{stage: '初中',title: '英语'},{stage: '小学',title: '语文'}]// 定义高阶组件 withContent// 包装后的组件可以根据传入的参数显示组件// ES5 写法// function withContent (Comp) {// return function (props) {// const content = lessons[props.idx]// return <Comp {...content}/>// }// }// ES6 写法const withContent = Comp => props => {const content = lessons[props.idx]return <Comp {...content}/>}const LessonWithContent = withContent(Lesson)export default function HOCTest() {return (<div>{ [0, 0, 0].map((item, idx) => <LessonWithContent key={idx} idx={idx} />)}</div>)}
范例:改造前面案例使上下文使用更优雅
这里和前面的范例有所不同,withConsumer 高阶组件工厂,根据配置返回一个高阶组件,,大家想一想 react-redux 里面的 connect 函数是不是和这个很类似。
import React, { Component } from 'react'// 1、创建上下文对象const Context = React.createContext()// 2、获取 Provider 和 Consumerconst { Provider, Consumer } = Contextfunction Child(props) {return (<div onClick={ () => props.add() }>{props.counter}</div>)}// withConsumer 高阶组件工厂,根据配置返回一个高阶组件const withConsumer = Consumer => {return Comp => props => {return <Consumer>{ value => <Comp {...value}/>}</Consumer>}}let ChildWithConsumer = withConsumer(Consumer)(Child)export default class ContextTest extends Component {state = {counter: 0}add = () => {this.setState({ counter: this.state.counter + 1 })}render() {return (<Provider value={{ counter: this.state.counter, add: this.add }}><ChildWithConsumer /><ChildWithConsumer /><ChildWithConsumer /></Provider>)}}
链式调用
// withLog高阶组件,在组件挂载时输出日志const withLog = Comp => {return class extends React.Component {componentDidMount () {console.log('componentDidMount', this.props)}render () {return <Comp { ...this.props }/>}}}// 高阶组件的链式调用(层级过多的话,代码可读性会很差,可以使用装饰器语法)const LessonWithContent = withLog(withContent(Lesson))
装饰器写法
CRA项目中默认不支持js代码使用装饰器语法,需要安装并配置 @babel/plugin-proposal-decorators 之后才能使用:
// package.json"babel": {"plugins": [["@babel/plugin-proposal-decorators",{"legacy": true}]],"presets": ["react-app"]},
范例:通过装饰器来使用高阶组件
// 装饰器import React, { Component } from 'react'// 模拟数据const lessons = [{stage: '高中',title: '数学'},{stage: '初中',title: '英语'},{stage: '小学',title: '语文'}]// 定义高阶组件 withContent// 包装后的组件可以根据传入的参数显示组件const withContent = Comp => props => {const content = lessons[props.idx]return <Comp {...content}/>}// withLog高阶组件,在组件挂载时输出日志const withLog = Comp => {return class extends React.Component {componentDidMount () {console.log('componentDidMount', this.props)}render () {return <Comp { ...this.props }/>}}}// 装饰器写法 @withLog @withContent 装饰器语法只能用在 class 组件上@withLog@withContentclass Lesson extends Component {render() {return (<div>{this.props.stage} - {this.props.title}</div>)}}export default function DecoraterTest() {return (<div>{ [0, 0, 0].map((item, idx) => <Lesson key={idx} idx={idx} />)}</div>)}
组件复合 - Composition
组件复合:类似于 Vue 的插槽,具体内容由外部传入
普通插槽
范例:Dialog组件负责展示,内容从外部传入即可,components/Composition.js**
// 组件复合:类似于 vue 的插槽,具体内容由外部传入import React, { Component } from 'react'function Dialog(props) {// props.children:children 是一个合法的 js 表达式return (<div style={{border: '1px solid #000'}}>{ props.children }</div>)}export default class Composition extends Component {render() {return (<div><Dialog><h1>组件复合</h1><p>通过组件符合去自定义组件内容</p></Dialog></div>)}}
具名插槽
范例:传个对象进去,key表示具名插槽
// 组件复合:类似于 vue 的插槽,具体内容由外部传入import React, { Component } from 'react'function Dialog(props) {// props.children:children 是一个合法的 js 表达式return (<div style={{border: '1px solid #000'}}>{ props.children.def }<div>{ props.children.footer }</div></div>)}export default class Composition extends Component {render() {return (<div><Dialog>{{def: (// React.Fragment的速记语法<></>,仅在最新版本(和Babel 7+)中受支持<><h1>组件复合</h1><p>通过组件符合去自定义组件内容</p></>),footer: (<button onClick={ () => alert('组件复合') }>确定</button>)}}</Dialog></div>)}}
作用域插槽
范例:传个函数进去,实现作用于插槽的功能
// 组件复合进阶用法:实现作用域插槽import React, { Component } from 'react'function Dialog(props) {const message = {foo: { title: 'foo', content: 'foo~' },bar: { title: 'bar', content: 'bar~' }}const { def, footer } = props.children(message[props.msg])return (<div style={{border: '1px solid #000'}}>{ def }<div>{ footer }</div></div>)}export default class HComposition extends Component {render() {return (<div><Dialog msg="foo">{({title, content}) => ({def: (<><h1>{title}</h1><p>{content}</p></>),footer: (<button onClick={ () => alert('组件复合') }>确定</button>)})}</Dialog></div>)}}
实现修改 children
如果 props.children 是 jsx,此时它是不能修改的。
范例:实现 RadioGroup 和 Radio 组件,可通过 RadioGroup 设置 Radio 的 name
// 组件复合进阶用法:实现修改 childrenimport React, { Component } from 'react'function RadioGroup (props) {// 如果 props.children是jsx,不能直接修改它return (<div>{React.Children.map(props.children, radio => {// 要修改虚拟DOM(jsx编译之后的结果),只能克隆后重新设置属性,不能直接修改// 参数1:克隆对象// 参数2:设置的属性return React.cloneElement(radio, { name: props.name })})// props.children.map(radio => {// return React.cloneElement(radio, { name: props.name })// })}</div>)}function Radio ({children, ...rest}) {return (<label><input type="radio" {...rest} />{ children }</label>)}export default class HComposition extends Component {render() {return (<div><RadioGroup name="mvvm"><Radio value="vue">vue</Radio><Radio value="react">react</Radio><Radio value="ng">angular</Radio></RadioGroup></div>)}}
