React 作为前端框架,其管理的是一个个 UI 控件,而在 React 的语言体系中,我们将之称为 component
– 组件。
狭义上来说,组件一般是 UI 组件,负责展示及和用户的交互。而广义上,组件是带有一定业务含义的,其不仅有与用户的交互,更重要的是数据与UI控件们之间的交互。
我们在之前介绍JSX
时介绍过,React 通过自定义元素的方式实现组件化(虚拟DOM),组件元素被描述成纯粹的 JSON 对象,意味着可以使用方法或是类来构建。React 组件基本上由 3 个部分组成 —— 属性(props)、状态(state)以及生命周期方法。通过 JSX,我们通常将要渲染的组件组成一棵组件树,就像搭乐高玩具一样一步步组成最终我们想要的 UI 界面。
1 组件
1.1 定义 React 组件
定义一个 React 组件 - Welcome.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react'; import PropType from 'prop-types';
class Welcome extends React.Component { constructor(props) { super(props); this.state = { username: props.username}; }
static defaultProps = { username: 'no name' };
render() { const { username } = this.state; return ( <h1>Hello, {username}!</h1>2019-12-24 16:28 ); } }
export default Welcome;
|
React 为 props 提供了默认配置,通过 defaultProps
静态变量的方式来定义。当组件被调用的时候,默认值保证渲染后始终有值。
1.2 无状态函数组件
无状态函数是官方比较推崇的一种组件构造方式,适用于页面内部的简单 UI 组件,看起来就是一个只传入 props
并返回一个 JSX UI 组件的函数。
其没有 state,帮称**”无状态”,也就是说这类的组件完全由其外层容器决定,而且无状态组件也没有生命周期方法。这样一来,其状态对于其父容器来说是固定的,这就更符合 React“单向数据流”**的设计理念。
无状态函数组件 - Star.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import PropType from 'prop-types'; import './star.css';
const Star = ({ selected = true, onClick = f => f }) => { return ( <div className={(selected ? 'star selected' : 'star unselected')} onClick={onClick}> </div> ); };
Star.propType = { selected: PropType.bool, onClick: PropType.func }
export default Star;
|
- 组件可以组合;
- 可以导入 CSS 或图片资源 (基于 Webpack);
1.3 PropTypes
众所周知,JavaScript
不是强类型语言,我们对在没有保证的环境下写 JavaScript
已经习以为 常了。强类型还是弱类型,正是一个开发时的约束问题。React
对此作了妥协,便有了 propTypes
。使用 propTypes
可以指定传入组件 props
的类型,就像上面 Star
示例一样。当传入错误的类型时,浏览器开发工具就会在控制台输入一个错误。
propTypes 官方示例:
propTypes 官方示例1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| MyComponent.propTypes = { optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalSymbol: PropTypes.symbol,
optionalNode: PropTypes.node,
optionalElement: PropTypes.element,
optionalElementType: PropTypes.elementType,
optionalMessage: PropTypes.instanceOf(Message),
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]),
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
optionalObjectWithShape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }),
optionalObjectWithStrictShape: PropTypes.exact({ name: PropTypes.string, quantity: PropTypes.number }),
requiredFunc: PropTypes.func.isRequired,
requiredAny: PropTypes.any.isRequired,
customProp: function(props, propName, componentName) { if (!/matchme/.test(props[propName])) { return new Error( 'Invalid prop `' + propName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } },
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) { if (!/matchme/.test(propValue[key])) { return new Error( 'Invalid prop `' + propFullName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } }) };
|
2 组件的生命周期
在软件设计中,生命周期 (life cycle)
是一个广泛关注的问题,基本上任何对象都有其生命周期。而 React 组件的生命周期一般分为两部分:
- 组件挂载与卸载阶段;(即从为组件分配内存,到将之放到渲染树中的阶段的回调)
- 接收到新数据时的渲染阶段 (render);
2.1 生命周期主要回调 api
这个阶段的主要回调 api 有:
挂载与卸载阶段主要回调1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| constructor () static getDerivedStateFromProps() render()
componentDidMount ()
componentWillUnmount ()
static getDerivedStateFromProps() shouldComponentUpdate()
render () getSnapshotBeforeUpdate () componentDidUpdate ()
|
React 生命周期流程图1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 【 组件挂载与卸载阶段 】 【 数据更新阶段 】
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ReactDOM.render() │ | setState() │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ | │ ▼ | ▼ ┌───────────────────────────┐ | ┌──────────────────────────────────────────────┐ │ constructor () │ | │ componentWillReceiveProps (nextProps) //(过时)│ └───────────────────────────┘ | └──────────────────────────────────────────────┘ │ | │ ▼ | ▼ ┌─────────────────────────────┐ | ┌────────────────────────────────────────────┐ │componentWillMount () //(过时)│ | │ shouldComponentUpdate (nextProps,nextState) │ └─────────────────────────────┘ | └────────────────────────────────────────────┘ │ | │ true ▼ | ▼ ┌───────────────────────────┐ | ┌────────────────────────────────────────────────────┐ │ render () │ | │ componentWillUpdate (nextProps, nextState) //(过时) │ └───────────────────────────┘ | └────────────────────────────────────────────────────┘ │ | │ ▼ | ▼ ┌───────────────────────────┐ | ┌───────────────────────────────────────────┐ │ componentDidMount() │ | │ render() │ └───────────────────────────┘ | └───────────────────────────────────────────┘ | │ | ▼ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ | ┌───────────────────────────────────────────┐ ReactDom.unmountComponentAtNode() | │ componentDidUpdate() │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ | └───────────────────────────────────────────┘ │ ▼ ┌───────────────────────────┐ │ componentWillUnMount() │ └───────────────────────────┘
|
2.2 常用的 React 组件模板
已去掉了最新 React 版本中被注释为 “过时” 的及不常用的方法回调
常用的 React 组件模板1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import React, { Component, PropTypes } from 'react';
class App extends Component {
static defaultProps = { xx: 'xx' };
static propType = { xx: PropType.bool, }
constructor (props) { super(props); this.state = {xx: 'xx'}; }
componentDidMount () { }
componentWillUnmount() { }
shouldComponentUpdate (nextProps, nextState) { }
componentDidUpdate (prevProps, prevState) { }
render() { return ( <div>This is a demo.</div> ); }
}
|
2.3 setState 的批处理与异步特性
出于性能考虑,React 可能会把多个 setState () 调用合并成一个调用。所以,下面这段代码可能会出现意料之外的问题
下面这段代码可能会出现意料之外的问题1 2 3 4
| this.setState({ counter: this.state.counter + this.props.increment, });
|
所以,如果要基于现在有 state 来改变 state 状态,可以让 setState () 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
解决上面 setState () 异步调用导致的问题1 2 3
| this.setState((state, props) => ({ counter: state.counter + props.increment }));
|
3 组件的设计原则
3.1 单向数据流
如果你把一个以组件构成的树想象成一个 props
的数据瀑布的话,那么每一个组件的 state
就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
3.2 尽量使用无状态函数组件
这点前面介绍无状态组件时已经说过了,无状态组件没有 state,也没有生命周期回处理,其状态对于其父容器来说是固定的,这就更符合 React“单向数据流”的设计理念。
3.3 使用 class-fields 语法
像下面的 Button
当点击时,其回调函数 handleClick
中的 this
有可能会改变。因为在 JS 中,一般函数中的 this
指的是调用此方法的对象,因为 Button 的回调是系统回调,所以此 this
有可能会变成全局 window
对象。
没有绑定 this 的有问题用法1 2 3 4 5 6 7 8 9
| handleClick() { this.setState(state => ({ isToggleOn: !state.isToggleOn })); }
<Button onClick={this.handleClick}> Click me </Button>
|
为了解决此问题,我们一般会使用键头函数:
使用键头函数作为回调函数,解决 this 指向问题1 2 3
| <Button onClick={(e) => this.handleClick(e)}> Click me </Button>
|
使用键头函数可以保证函数中的 this
指向其外层定义对象。但是这样有个问题,就是如果回调方法为 props
传入的话,每次渲染 Button
控件时都会创建不同的回调函数,此时组件会造成额外的开销。
所以,回调函数使用 class fields
语法会是更好的选择,其可以解决 this 引用问题,也可以不需要在控件重绘时再重复生成回调函数。用法如下:
定义回调函数使用 class-fields 语法1 2 3 4 5 6 7 8 9 10 11
|
handleClick = (event) => (event) { this.setState(state => ({ isToggleOn: !state.isToggleOn })); }
<Button onClick={this.handleClick}> Click me </Button>
|
- 但是如果需要向函数传递参数,还是建议在指定回调时使用键头函数
使用键头函数传递参数1 2 3 4 5 6 7
| handleClick (e, userId) { this.props.handleClick && this.props.handleClick(userId) }
<Button onClick={(e) => this.handleClick(e, userId)}> Click me </Button>
|
4 React.Children
组件有一个特殊的 this.props.children
来表示组件中的子元素,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
const StarBox = ({ isShow = true, children }) => { return ( <div> { isShow? children : null } </div> ); };
class App extends React.Component { render() { return ( <StarBox isShow={false}> <Star/> <Star/> <Star/> </StarBox> ) } }
|
当 App.js
中的 isShow={false}
时,其组件中定义的 3 个 Star
子组件都不会显示。因为在 StarBox
组件中作了判断,isShow
为 false 时,并没有输出 children
,而这个 children
就是在外层父组件中传入的 3 个 Star
元素。这里的 children
是个数组。
4.1 校验单个子元素
在有些场合你可以使用
使用 React.Childre 校验单个子元素1
| React.Children.only(this.props.children);
|
方法来校验单个子元素。也就是说,如果像上例中输出子元素是一个数组,则会报错
校验非单个子元素会报错1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
const StarBox = ( props ) => { return ( <div> { React.Children.only(props.children) } </div> ); };
class App extends React.Component { render() { return ( <StarBox> <Star/> {/* 这里注意了,只能是一个 Star 了,如果有多个子元素 children 会变成数组,从而不能通过 Children.only 校验 */} </StarBox> ) } }
|
但是上面的例子也要注意的就是 Star 自身可以是复杂的组合组件,这是没问题的。
5 Fragments
我们知道,React 的渲染方式为在 render ()
方法中返回声明式元素,如:
return 只能返回单个 root 元素1 2 3 4 5 6 7 8 9 10 11
| class Table extends React.Component { render() { return ( <table> <tr> <Columns /> </tr> </table> ); } }
|
即在 return 时,只能返回单个 Root 元素,如果上面的 Columns
组件像下面这样返回,将会报错:
错误的返回1 2 3 4 5 6 7 8
| class Columns extends React.Component { render() { return ( <td>Hello</td> <td>World</td> ); } }
|
但是像上面这个场景,在 2 个 <td>
元素外加个 div 层显示也是不行的。这时候就要 Fragments
出场了!
使用 React.Fragment 包裹并列子组件1 2 3 4 5 6 7 8 9 10 11 12 13 14
| return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> );
return ( <> <td>Hello</td> <td>World</td> </> );
|
这样就可以得到我们想要的正确输出:
使用 Fragment 得到的正确输出1 2 3 4 5 6
| <table> <tr> <td>Hello</td> <td>World</td> </tr> </table>
|
6 使用 Context 全局变量
Context 设计目的是为了共享那些对于一个组件树而言是 “全局” 的数据。一般来说,React 推崇的是自顶向下的单方向数据流,数据从 root component 利用 props 属性呈树型地往下传递。而 Contex 则看起来与此理念相悖。但是在有些场合,这种 “全局” 变量又显得那么重要,比如要给每个控件增加一个全局的主题属性,如果没有全局变量的话,这将变得非常复杂 (当然,也可以使用 redux 这种单一数据源的框架来解决)。
这里用的是一个生产者-消费者模型?
6.1 使用 Context 全局变量
6.1.1 React.createContext
我们就用上面说的设置全局主题的示例。我们首先应该要创建一个 Context 对象,使用 React.createContext (defaultValue)
api:
ThemeContext 全局对象1
| const ThemeContext = React.createContext (themes.dark);
|
然后这里涉及 2 个问题:一个是全局对象的读取,另一个是全局对象的修改。一种简单的操作是全局对象只放在顶层容器组件中可修改,其值与顶层容器组件的 state 绑定。
6.1.2 Context.Provider
容器组件 Context.Provider<Context.Provider value={this.state.value}>
可以向下传递 Context 全局对象,同时,让此 Context 值与 state 绑定。
顶层容器向下传递 Context 对象及 state 绑定 context 值1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| constructor(props) { this.state = { ThemeConst.Light, }; }
_toggleTheme = () => { this.setState(state => ({ theme: (state.theme === ThemeConst.Dark)? ThemeConst.Light: ThemeConst.Dark, })); }
<ThemeContext.Provider value={this.state.theme}> <ThemedButton onClick={this._toggleTheme}> Click to change theme </ThemedButton> </ThemeContext.Provider>
|
在上面的代码示例中,context 传递给了 ThemeButton
,那在展示组件中怎样将其值取出呢?
6.1.3 contextType
我们看看此 button 的代码。在 ThemedButton 中,我们可以直接利用 this.context 取出全局变量,从而决定自己的展示。
在底层展示组件中直接利用 this.context 取出全局变量1 2 3 4 5 6 7 8 9
| class ThemedButton extends React.Component { render() { let theme = this.context; return ( <button {...this.props} style={{backgroundColor: theme.Background}} /> ); } } ThemedButton.contextType = ThemeContext;
|
6.2 子组件中修改全局变量
在上面的示例中,子组件只可以被动地接受由 Context.provider 传下来的全局变量。那子组件是否有方法可以修改全局变量的值呢?
答案是有的,就是利用 Context.Consumer
。
首先我们在定义 Context 时将之定义为一个对象,其中包含一个可以改变自身值的函数 toggleTheme
:
定义 Context 对象,包含一个可以改变自身值的回调函数 toggleTheme1 2 3 4
| const ThemeContext = React.createContext({ theme: ThemeConst.Dark, toggleTheme: () => {} });
|
这个回调固然是要在根容器组件中定义:
根容器组件中定义1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| constructor(props) { this.state = { theme: ThemeConst.Light, toggleTheme: this._toggleTheme }; }
_toggleTheme = () => { this.setState(state => ({ theme: state.theme === ThemeConst.Dark? ThemeConst.Light: ThemeConst.Dark })); }
render() { return ( <div> <ThemeContext.Provider value={this.state}> <ThemeToggleButton> Click to change theme </ThemeToggleButton> </ThemeContext.Provider> </div> ) }
|
6.2.1 ThemeContext.Consumer
在子展示组件中,使用 ThemeContext.Consumer
订阅了 Context 的修改。其内包含一个回调,当 Context 改变时此订阅将会得到通知,子组件也将得到刷新
ThemeToggleButton 使用 ThemeContext.Consumer 与全局 Context 绑定1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class ThemeToggleButton extends React.Component { render() { return ( <ThemeContext.Consumer> { (ThemeContext) => ( <button onClick={ThemeContext.toggleTheme} style={{ backgroundColor: ThemeContext.theme.background }}>
{this.props.children} </button> ) } </ThemeContext.Consumer> ); } }
|
可以想象,这一套和 Redux
的单一数据流很像,没错,Redux
就是基于 Context 这一套实现滴~
7 使用events进行组件间通信
还可以使用Node.js events模块通信,这个events
广播模块使用起来很简单,广播使用一个key标识,需要监听事件的地方监听指定的key然后定义一个回调,而发起广播一方直接发送特定的key广播就可以了。这里直接show代码示例(要注意一下就是这个EventEmitter
我们一般使用单例来运用):
定义单例emit.js1 2
| import { EventEmitter } from 'events'; export default new EventEmitter();
|
监听广播1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import emit from './emit';
componentDidMount () { this._myListener = emit.addListener('myEmit', (data) => { console.log(data); }); }
componentWillMount () { if (this._myListener) { emit.removeListener(this._myListener); } }
|
发送广播1 2
| import emitter from './events'; emitter.emit('myEmit', {xx: 'xx'});
|
8 高阶组件(HOC)
高阶组件(HOC)
是 React 中用于复用组件逻辑的一种高级技巧。具体而言,高阶组件是参数为组件,返回值为新组件的函数。组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
在使用高阶组件之前,一直是使用mixins
来解决横切关注点相关问题。但是后面人们发现使用mixins
会存在各种各样的问题,使之变得不再推荐,而是采用高阶组件
来代替mixins
。
为啥要使用高阶组件呢?首先我们来看看啥是高阶组件。。
8.1 高阶组件及其作用
这里的“高阶”就是higher-order
,我们学函数式编程时接触过,higher-order function(高阶函数)
。就是指如果一个函数f()的输入是函数比如g(),而其输出也是函数比如h(),那么这个函数f()我们就称之为高阶函数(因为一般的函数是输入是普通对象参数,返回值也是普通对象参数)。
一般来说我们的组件输入是prop
对象,输出是JSX组件树(会编译成虚拟DOM元素树)。而高阶组件的输入是一个ReactComponent
,输入也是ReactComponent
。
那么,你又会问了,为什么要这样? — 答案就是 – 为了复用!
比如下面这个最简单的例子,在这个例子中,我们就是为了复用统一高宽的一个列表项:
高阶组件1 2 3 4 5 6 7 8 9
| const ListItem = (WrappedComponent) => ( class extends Component { render() { return <WrappedComponent style={{ /*统一的高宽*/ }} />; } } );
|
上面的例子就是比如我们用在一个列表显示页面中,每一个表项大小是统一的,但是里面的内容可能各不想同,使用LiteItem
这个高阶组件就可以将许多如表项高宽之类的代码复用起来,只要定义其内部的组件作为包装即可。
8.2 使用函数做为子组件
9 小结
10 引用
- 《谈一谈 Normalize.css》- 会飞的贼xmy