组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
React没有多少API可以用,基本上都是组件来完成一个项目。React的工作方式类似于UI=F(state)
,即一旦组件维护的state发生改变,render函数就会重新执行,导致试图改变。因此学好React组件是至关重要的。
容器组件VS展示组件
我们希望尽可能让更多的组件变成傻瓜组件,它只负责将数据展示到视图上,而它所用的数据全部都提升到父级组件。父级组件负责逻辑处理和状态维护,将子组件所需的回调事件和状态通过props传递给子组件。这样单纯的展示组件就会有很好的易用性、复用性和维护性。
import React, { Component } from 'react'
//容器组件
export default class BookList extends Component{
constructor(props){
super(props)
this.state = {
books:[]
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
books:[
{ name: '《你不知道的javascript》', price: 50 },
{ name: '《ES6标准入门》', price: 99 }
]
})
}, 1000)
}
render(){
return(
<div>
{this.state.books.map((book, index) => <Book key={index} {...book}></Book>)}
</div>
)
}
}
//展示组件
function Book({ name, price }){
return (
<div>
<p>书名:{name}</p>
<p>价格:{price}</p>
</div>
)
}
复制代码
展示组件存在的问题
上述展示组件存在一个问题,因为React中数据维持着不变性的原则,只要setState改变books,都会触发render函数的调用以及虚拟DOM的比较。如果books中的值只有一条变化,它也会引发每条数据都引发一次render函数调用。我们怎么去规避展示组件无谓的数据消耗和渲染函数的调用呢?首先我们看这段代码:
import React, { Component, PureComponent } from 'react'
//容器组件
export default class BookList extends Component {
constructor(props) {
super(props)
this.state = {
books: []
}
}
componentDidMount () {
setTimeout(() => {
this.setState({
books: [
{ name: '《你不知道的javascript》', price: 50 },
{ name: '《ES6标准入门》', price: 99 }
]
})
}, 1000)
setTimeout(() => {
this.setState({
books: [
{ name: '《哈利波特》', price: 25 },
{ name: '《ES6标准入门》', price: 99 }
]
})
}, 2000)
}
render () {
return (
<div>
{this.state.books.map((book, index) => <Book key={index} {...book}></Book>)}
</div>
)
}
}
// 展示组件
function Book ({ name, price }) {
console.log('渲染了')
return (
<div>
<p>书名:{name}</p>
<p>价格:{price}</p>
</div>
)
}
复制代码
它在浏览器上打的log如下:
首先由一个空数组变成有两条数据的数组肯定会导致两次render函数调用,但是第二次变化时,只有第一条数据改变,但还是引起了两次render函数的调用。这是因为我们为了维持数据的不变性,每次都会更新books为一个全新的数组。
我们要明确一点,肯定是要维持数据的不变性的。有三种方法可以规避这种无谓的render的调用。在PureComponent没有出现之前,我们在shouldComponentUpdate这个生命周期钩子函数中比较下一个值和当前值,如果相等,则不需要更新该条数据,返回false,写法比较累赘。React15.3之后出现了PureComponent纯组件,它就是在内部实现了在shouldComponentUpdate中比较值。还有一种是React16.6之后出现的React.memo,与使用PureComponent方法的原理和效果是等价的,它是一个高阶组件。
用上述方法在浏览器上打的log为:
shouldComponentUpdate
class Book extends Component {
shouldComponentUpdate ({ name, price }) {
if (this.props.name === name && this.props.price === price) {
return false
}
return true
}
render () {
console.log('渲染了')
return (
<div>
<p>书名:{this.props.name}</p>
<p>价格:{this.props.price}</p>
</div>
)
}
}
复制代码
PureComponent
class Book extends PureComponent {
render () {
console.log('渲染了')
return (
<div>
<p>书名:{this.props.name}</p>
<p>价格:{this.props.price}</p>
</div>
)
}
}
复制代码
React.memo
const Book = React.memo(
function ({ name, price }) {
console.log('渲染了')
return (
<div>
<p>书名:{name}</p>
<p>价格:{price}</p>
</div>
)
}
)
复制代码
高阶组件
上面我们讲到了高阶组件,它是一个函数,接收一个组件,返回一个加强后的组件。组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。高阶组件可以提高组件的复用率,重写生命周期函数。
基础写法:
import React, { Component } from 'react'
//一个简单的展示组件
function Pure (props) {
return (
<div>{props.name} -- {props.age}</div>
)
}
//高阶组件
const withLog = (Comp) => {
console.log(Comp.name + '渲染了')
return (props) => {
return <Comp {...props}></Comp>
}
}
//生成一个新的组件
const NewPure = withLog(Pure)
//使用这个新组件
export default class Hoc extends Component {
render () {
return (
<div>
<NewPure age='19' name='zhunny'></NewPure>
</div>
)
}
}
复制代码
高阶组件的链式调用
高阶组件可以链式调用,且可以在一个链式调用中调用多次同一个高阶组件。
import React, { Component } from 'react'
function Pure (props) {
return (
<div>{props.name} -- {props.age}</div>
)
}
const strengthenPure = Comp => {
const name = 'zhunny'
//返回类组件
return class extends React.Component {
componentDidMount () {
console.log('do something')
}
render () {
return <Comp {...this.props} name={name}></Comp>
}
}
}
const withLog = (Comp) => {
console.log(Comp.name + '渲染了')
return (props) => {
return <Comp {...props}></Comp>
}
}
//高阶组件可以链式调用
const NewPure = withLog(strengthenPure(withLog(Pure)))
export default class Hoc extends Component {
render () {
return (
<div>
<NewPure age='19'></NewPure>
</div>
)
}
}
复制代码
高阶组件的装饰器写法
ES7的装饰器可以简化高阶组件的写法,不过需要引入一个转义decorator语法的插件,并在根目录配置config-overrides.js文件。安装react-app-rewired取代react-scripts,可以扩展webpack的配置 ,类似vue.config.js
npm install --save-dev babel-plugin-transform-decorators-legacy
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save
复制代码
const { injectBabelPlugin } = require("react-app-rewired");
module.exports = function override (config, env) {
//装饰器
config = injectBabelPlugin(
["@babel/plugin-proposal-decorators", { legacy: true }],
config
);
return config;
};
复制代码
因为decorator只能装饰类,因此只能装饰基于类的组件。
@withLog
@strengthenPure
@withLog
class Pure extends React.Component {
render () {
return (
<div>{this.props.name} -- {this.props.age}</div>
)
}
}
复制代码
组件的组合composition
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。组合组件给予你足够的敏捷去定义自定义组件的外观和行为,而且是以一种明确和安全的方式进行。如果组件间有公用的非UI逻辑,将它们抽取为JS模块导入使用而不是继承它。
包含关系
有些组件无法提前知晓它们子组件的具体内容。这些组件可以使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中。
function Dialog (props) {
return (<div style={{ border: `1px solid ${props.color || "blue"}` }}>
{props.children}
<footer>{props.footer}</footer>
</div>)
}
//可以看作一个特殊的Dialog
function WelcomeDialog (props) {
console.log(props)
return (
<Dialog {...props}>
{/*类似于匿名slot插槽*/}
<h1>欢迎光临</h1>
<p>感谢使用React</p>
</Dialog>
)
}
export default function () {
//footer类似于具名slot插槽
const footer = <button onClick={() => { alert('1') }}>footer</button>
return (
<WelcomeDialog color='green' footer={footer}></WelcomeDialog>
)
}
复制代码
props.children可以是任意js表达式,可以是一个函数。
const Api = {
getUser () {
return { name: 'jerry', age: 20 }
}
}
function Fetcher (props) {
const user = Api[props.name]()
return props.children(user)
}
export default function () {
//类似于作用域插槽
return (
<Fetcher name="getUser">
{({ name, age }) => (
<p>
{name}-{age}
</p>
)}
</Fetcher>
)
}
复制代码
示例
function GroupRadio (props) {
//因为props的内容是不可修改的,因此在Radio上增加一个属性需要拷贝一份
return <div>
{React.Children.map(props.children, child => {
return React.cloneElement(child, { name: props.name })
})}
</div>
}
function Radio ({ children, ...rest }) {
return (
<label>
<input type="radio" {...rest} />
{children}
</label>
)
}
export default function () {
return (
<GroupRadio name="mvvm">
<Radio value="vue">vue</Radio>
<Radio value="angular">angular</Radio>
<Radio value="react">react</Radio>
</GroupRadio>
)
}
复制代码