React组件全方位学习
作者:互联网
在React中,组件是应用程序的基石,页面中所有的界面和功能都是由组件堆积而成的。合理的组件设计有利于降低系统各个功能的耦合性,并提高功能内部的聚合性。
组件声明方式 |
在React中创建组件的方式有3种:
- ES 5写法:React.createClass()(老版本用法,不建议使用);
- ES 6写法:React.Component;
- 函数式写法
组件只能包含一个顶层标签。
组件名称必须以大写字母开头,React 会将以小写字母开头的组件视为原生 DOM 标签。
class 组件 |
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
函数组件 |
React 16.7之前,如果一个组件不需要管理state,只是单纯地展示,那么就可以定义成无状态组件。这种方式声明的组件可读性好,能大大减少代码量。无状态函数式组件可以搭配箭头函数来写,更简洁,它没有React的生命周期和内部state。
React 16.7.0-alpha(内测)中引入了Hooks,这使得在函数式组件内可以使用state和其他React特性。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const Welcome=(props)=>{
return <h1>Hello, {props.name}</h1>;
}
组件的主要成员 |
在React中,数据流是单向流动的,从父节点向子节点传递(自上而下)。子组件可以通过属性props接收来自父组件的状态,然后在render()
方法中渲染到页面。每个组件同时又拥有属于自己内部的状态state,当父组件中的某个属性发生变化时,React会将此改变了的状态向下递归遍历组件树,然后触发相应的子组件重新渲染(re-render)。
如果把组件视为一个函数,那么props就是从外部传入的参数,而state可以视为函数内部的参数,最后函数返回虚拟DOM。
state(状态) |
每个React组件都有自己的状态,相比于props,state只存在于组件自身内部,用来影响视图的展示。React通过this.state访问状态,调用this.setState()方法来修改状态。每当使用setState()
时,React会将需要更新的state合并后放入状态队列,触发调和过程(Reconciliation),而不是立即更新state,然后根据新的状态结构重新渲染UI界面,最后React会根据差异对界面进行最小化重新渲染。
import React from "react";
import { render } from "react-dom";
class MyBox extends React.Component{
constructor(props){
super(props);
this.state={
text:'Hello'
}
}
handleClick=()=>{
this.setState({text:this.state.text=='Hello'?'Hi':'Hello'})
}
render(){
return(<button onClick={this.handleClick}>{this.state.text},{this.props.name}</button>)
}
}
render(<MyBox name='Joe'/>, document.querySelector("#app"));
props(属性) |
props
就是连接各个组件信息互通的“桥梁”,React本身是单向数据流,所以在props
中数据的流向非常直观,并且props
是不可改变的。props
的值只能从默认属性和父组件中传递过来,如果尝试修改props
,React将会报出类型错误的提示。
import React from "react";
import { render } from "react-dom";
function MyBox(props){
return(<div>{props.name}</div>)
}
render(<MyBox name='Joe'/>, document.querySelector("#app"));
props是不可改变的 |
试图改变props
将报错
function MyBox(props){
props.name='kk';
return(<div>{props.name}</div>)
}
默认 Props |
通过组件类的 defaultProps
属性为 props 设置默认值。
function MyBox(props){
return(<div>{props.name}</div>)
}
MyBox.defaultProps={
name:'Joe'
}
Props 验证 |
Props 验证使用 propTypes,它可以保证我们的应用组件被正确使用,React.PropTypes 提供很多验证器 (validator) 来验证传入数据是否有效。当向 props 传入无效数据时,JavaScript 控制台会抛出警告。
安装 prop-types 库
npm i -D prop-types
以下实例创建一个 MyBox组件,属性 name是必须的且是字符串,非字符串类型会自动转换为字符串 :
import React from "react";
import { render } from "react-dom";
import PropTypes from 'prop-types'
function MyBox(props){
return(<div>{props.name}</div>)
}
MyBox.propTypes = {
name: PropTypes.string
};
render(<MyBox name={1}/>, document.querySelector("#app"));
数值会转为字符串,但是控制台会发出警告。
render()方法 |
组件内的render()方法用于渲染虚拟DOM,返回ReactElement类型。
render()
方法是一个类组件必须拥有的特性,其返回一个JSX元素,并且外层一定要使用一个单独的元素将所有内容包裹起来。元素是React应用的最小单位,一个元素可以构成一个组件,多个元素也可以构成一个组件。
以下写法是错的:
render() {
return(
<div>a</div>
<div>b</div>
<div>c</div>
)
}
要写成这样:
render() {
return(
<div>
<div>a</div>
<div>b</div>
<div>c</div>
</div>
)
}
render()返回元素数组 |
返回的数组跟其他数组一样,需要给数组元素添加一个key来避免key warning。同级元素外层实际上是没有包裹其他元素的,这样能减少DOM元素的嵌套。
return (
<>
<div key='a'>a</div>
<div key='s'>s</div>
<div key='d'>d</div>
</>
)
简写的<></>
其实是React 16中React.Fragment的简写形式,不过它对于部分前端工具的支持还不太好,建议使用完整写法,具体如下:
return (
<React.Fragment>
<div>a</div>
<div>b</div>
<div>c</div>
</React.Fragment>
);
组件之间的通信 |
父组件向子组件通信 |
React的数据是单向流动的,只能从父级向子级流动,父级通过props属性向子级传递信息。
import React from "react";
import { render } from "react-dom";
class Child extends React.Component{
render(){
return(
<div>{this.props.fromFather}</div>
)
}
}
class Father extends React.Component{
render(){
return(
<Child fromFather='This message is from Dad!'></Child>
)
}
}
render(<Father/>, document.querySelector("#app"));
子组件向父组件通信 |
React数据流是单向的,但并不影响子组件向父组件通信。通过父组件可以向子组件传递函数这一特性,利用回调函数来实现子组件向父组件通信。
import React from "react";
import { render } from "react-dom";
class Child extends React.Component{
render(){
return(
<input onChange={(e)=>this.props.handleChange(e.target.value)}></input>
)
}
}
class Father extends React.Component{
constructor(props){
super(props);
this.state={
data:''
}
}
handleChange=text=>{
this.setState({data:text})
}
render(){
return(
<>
<p>来自子组件的信息:{this.state.data}</p>
<Child handleChange={this.handleChange}></Child>
</>
)
}
}
render(<Father/>, document.querySelector("#app"));
一般情况下,回调函数会与setState()
成对出现。
跨级组件通信 |
组件层层嵌套,要实现跨组件通信,可以利用props
一层层去传递信息。但这种写法有点累赘。在React中,一般使用context
来实现跨级父子组件通信。
context的设计目的就是为了共享对于一个组件树而言是“全局性”的数据,可以尽量减少逐层传递,但并不建议使用context。因为当结构复杂的时候,这种全局变量不易追溯到源头,不知道它是从哪里传递过来的,会导致应用变得混乱,不易维护。
context
适用的场景最好是全局性的信息,且不可变的,比如用户信息、界面颜色和主题制定等。
import React from "react";
import { render } from "react-dom";
import PropTypes from 'prop-types';
class Button extends React.Component{
render(){
return(
<button style={{background:this.context.color}}>
{this.props.children}
</button>
)
}
}
Button.contextTypes={
color:PropTypes.string
}
class Father extends React.Component{
render(){
return(
<Button children='This message is from dad!'></Button>
)
}
}
class App extends React.Component{
getChildContext(){
return{color:'orange'}
};
render(){
return <Father/>
}
}
App.childContextTypes={
color:PropTypes.string
}
render(<App/>, document.querySelector("#app"));
上述代码中,App 为context
的提供者,通过在App 中添加childContextTypes
和getChildContext()
。React会向下自动传递参数,任何组织只要在它的子组件中(这个例子中是Button),就能通过定义contextTypes
来获取参数。如果contextTypes
没有定义,那么context
将会是个空对象。
context中有两个需要理解的概念:一个是context
的生产者(provider);另一个是context
的消费者(consumer),通常消费者是一个或多个子节点。所以context
的设计模式是属于生产-消费者模式。在上述示例代码中,生产者是父组件App ,消费者是孙组件Button。
值得注意的是,很多优秀的React第三方库都是基于context
来完成它们的功能的,比如路由组件react-route
通过context
来管理路由,react-redux
的<Provider/>
通过context
提供全局Store
,拖曳组件react-dnd
通过context
分发DOM的Drag和Drop事件等。
非嵌套组件通信 |
非嵌套组件就是没有包含关系的组件。这类组件的通信可以考虑通过事件的发布-订阅模式或者采用context
(加一个共同父组件,也导致耦合度增加)来实现。
发布-订阅模式又叫观察者模式。其实很简单,举个现实生活中的例子: 很多人手机上都有微信公众号,读者所关注的公众号会不定期推送信息。
这就是一个典型的发布-订阅模式。在这里,公众号就是发布者,而关注了公众号的微信用户就是订阅者。关注公众号后,一旦有新文章或广告发布,就会推送给订阅者。这是一种一对多的关系,多个观察者(关注公众号的微信用户)同时关注、监听一个主体对象(某个公众号),当主体对象发生变化时,所有依赖于它的对象都将被通知。
优点:耦合度低 易扩展 易测试 灵活性高
React在非嵌套组件中只需要某一个组件负责发布,其他组件负责监听,就能进行数据通信了。下面通过代码来演示这种实现:
1.安装events包
npm install events
2.新建一个公共文件events.js,引入events包,并向外提供一个事件对象,供通信时各个组件使用:
import {EventEmitter} from "events";
export default new EventEmitter();
3.组件App.js:
import React from "react";
import { render } from "react-dom";
import emitter from "./events";
class ComponentA extends React.Component{
constructor(params) {
super(params);
this.state={
data:'init'
}
}
componentDidMount(){
this.eventEmitter=emitter.addListener('callMe',(data)=>{
this.setState({
data
})
})
}
componentWillUnmount(){
emitter.removeListener(this.eventEmitter);
}
render(){
return(
<div>{this.state.data}</div>
)
}
}
class ComponentB extends React.Component{
render(){
const cb=(data)=>{
return ()=>{
// 触发自定义事件
// 可传多个参数
emitter.emit('callMe',data);
}
}
return(
<button onClick={cb('Hey')}>点击</button>
)
}
}
class App extends React.Component{
render(){
return (
<>
<ComponentA/>
<ComponentB/>
</>
)
}
}
render(<App/>, document.querySelector("#app"));
组件的生命周期 |
在React组件的整个生命周期中,props和state的变化伴随着对应的DOM展示。每个组件提供了生命周期钩子函数去响应组件在不同时刻应该做和可以做的事情:创建时、存在时、销毁时。
只说最新版本的生命周期内容,以下是React17版本中组件相关的部分生命周期方法,可见很多方法被移除了。
注意:无状态函数式组件没有生命周期,除了React 16.7.0的新特性Hooks。
组件的挂载 |
React将组件渲染→构造DOM元素→展示到页面的过程称为组件的挂载。一个组件的挂载会经历下面几个过程:
constructor()
:ES6中类的默认方法,通过new
命令生成对象实例时自动调用该方法。 子类必须在constructor()
中调用super()
方法,否则新建实例会报错。如果没有用到constructor()
,React会默认添加一个空的constructor()
。static getDerivedStateFromProps(object props,object state)
:在组件装载时,以及每当props更改时被触发,用于在props(属性)更改时更新组件的状态,返回的对象将会与当前的状态合并。render()
:方法用于将组件渲染虚拟DOM,返回ReactElement类型(最终创建的react对象)。componentDidMount()
:在组件挂载完成以后,也就是DOM元素已经插入页面后调用。而且这个生命周期在组件挂载过程中只会执行一次,通常会将页面初始数据的请求在此生命周期内执行。
import React from "react";
import { render } from "react-dom";
class App extends React.Component {
constructor(props) {
super(props);
console.log("constructor");
this.state={}
}
static getDerivedStateFromProps() {
console.log("getDerivedStateFromProps");
return null;
}
render() {
console.log("render");
return "test";
}
componentDidMount() {
console.log("componentDidMount");
}
}
render(<App />, document.querySelector("#app"));
数据的更新过程 |
组件在挂载到DOM树之后,当界面进行交互动作时,组件props或state改变就会触发组件的更新。假如父组件render()
被调用,无论此时props是否有改变,在render()
中被渲染的子组件就会经历更新过程。一个组件的数据更新会经历下面几个过程:
static getDerivedStateFromProps(object props,object state)
shouldComponentUpdate(nextProps,nextState)
:用于判断组件是否需要更新。它会接收更新的props和state。默认情况下,该方法返回true。当返回值为false时,则不再向下执行其他生命周期方法。componentWillUpdate() / UNSAFE_componentWillUpdate()
:在组件接收到新的props或者state但还没有render时被调用render()
getSnapshotBeforeUpdate(prevProps, prevState)
:在最近一次的 render 完将要 commit 给 DOM 的时候会调用,这个方法能够使得组件可以在可能更改之前从 DOM 捕获一些信息,比如滚动的位置等等。这个方法一般来说是不会使用的,不过它可能会出现在需要以特殊方式进行处理UI,比如像是聊天线程中处理滚动位置。componentDidUpdate(nextProps, nextState, snapshot)
:渲染后的props和state。
组件自身state更新 |
shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
父组件props更新 |
static getDerivedStateFromProps() —> shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
组件的卸载 |
componentWillUnmount()
当组件将要被卸载之前调用,可以在该方法内执行任何可能需要清理的工作。比如清除计时器、事件回收、取消网络请求,或清理在componentDidMount()
中创建的任何监听事件等。
import React, { Component } from "react";
class Hello extends Component {
componentDidMount() {
this.timer = setTimeout(() => {
console.log("挂在this上的定时器");
}, 500);
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
}
}
=============================================================================================
行文至此,还望指点,若觉还行,点赞三连。
标签:return,render,全方位,React,state,props,组件 来源: https://blog.csdn.net/wangfeijiu/article/details/113931123