(React基础)组件、状态State、组件间使用props或Context进行通讯、refs

261 阅读14分钟

一、组件的定义

1、组件最好写在独立的js文件中

2、组件名必须以大写字母开头,以此区分组件和普通的react元素

3、在当前js文件中定义并渲染了该组件,若只需要在该页面使用组件,则组件无需export。 如果将组件抽离成了独立的js文件,则需要export。(在其他页面需导入该组件后使用

4、如果组件的返回值为null,表示不渲染任何内容

1. 函数组件

函数组件必须有返回值,表示该组件的结构

import React from 'react'
import ReactDOM from 'react-dom'
// function Hello() {

    //   console.log(this)  //undefined
    /*this本质上是指向它的调用者, this是在函数运行时才绑定, JS里普通函数都是window调用的, 所以指向window, 经由babel编译后开启了严格模式 this是undefined。*/

//   return (
//     <div>函数组件</div>
//   )
// }

const Hello = () => <div>使用箭头函数创建组件</div>

函数组件里没有this,且没有生成实例对象,不存在使用this.state获取状态。

当使用<组件名/>或者执行了ReactDOM.render()将组件渲染到页面之后:

1、React解析组件标签,找到了该组件

2、发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,呈现在页面中。

2. 类组件

使用ES6语法的class创建的组件

  • 类组件应该继承React.Component父类,从而可以使用父类中提供的方法或者属性
  • 类组件必须提供 render 方法
  • render方法中必须要有return返回值,一般返回JSX结构表示该组件的结构

创建class类,继承React.Component,在里面提供render方法,在return里面返回渲染内容

import React from 'react'
import ReactDOM from 'react-dom'

class Hello extends React.Component {
  render() {
    console.log('render中的this:',this)
  
    return (
      <div>创建类组件</div>
    )
  }
}

render方法是放在Hello这个类的原型对象上,供实例使用。

1.jpg

render()里的this指向问题:指向Hello这个类的实例对象,即组件实例对象

2.png

这些常用的属性,是从React.Component这个类继承而来。

当使用<组件名/>或者执行了ReactDOM.render()将组件渲染到页面之后:

1、React解析组件标签,找到了该组件

2、发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。

3、将render返回的虚拟DOM转为真实DOM,呈现在页面中。

3. 抽离成单独的JS文件

每个组件作为一个独立的个体,一般都会放到一个单独的JS文件中

  • 创建Hello.js
  • 在Hello.js 中导入React
  • 创建组件
  • 在Hello.js中导出
import React from 'react'

export default class Hello extends React.Component {
    render(){
        return (
            <div></div>
        )
    }
}
import React from 'react'

// 创建组件
class Hello extends React.Component {
  render() {
    return (
      <div></div>
    )
  }
}

// 导出组件
export default Hello

使用该组件:

  • 在index.js中导入Hello组件
  • 用组件名作为标签名,渲染到页面
import React from 'react'
import ReactDOM from 'react-dom'

// 导入Hello组件
import Hello from './Hello'

// 渲染组件
class Index extends React.Component {
    render(){
        return (
            <Hello></Hello>
        )
    }
}

二、有状态组件和无状态组件

函数组件又叫做 无状态组件,类组件又叫做 有状态组件

  • 状态(state) 即数据
  • 函数组件没有自己的状态,只负责数据展示(除非使用 react hooks
  • 类组件有自己的状态,负责更新UI,让页面动起来

状态.png

1. 类组件中 state的使用

  • 状态(state)是组件内部的私有数据,只能在组件内部使用(和props区分,props是从外界传入组件的数据)
  • state的值是对象,在对象里可以放多个数据
  • state是在组件实例对象上的其中一个属性,所以通过this.state来访问
  • 第二种初始化方式-原理:在类中可以直接写赋值语句,所以不需要传实参的有固定值的属性,可以直接写(无需在constructor里写this.属性 = 固定值
import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  /* constructor(props) {
  //子类有继承行为且写了构造器,必须写super
    super(props)

    // 第一种初始化方式
    this.state = {
      count: 0
    }
  } */

  // 第二种初始化方式:使用 ES6里面的简化语法初始化 state(推荐)
  state = {
    count: 10
  }

  render() {
  // const {count} = this.state
    return (
      <div>
        <h1>计数器:{ this.state.count }</h1>
      </div>
    )
  }
}

2. 类组件中使用 setState() 修改状态

  • 语法:this.setState({要修改的数据}),只会更新对应属性的值,不会影响state中其他属性的值
  • 注意:不要直接修改state中的值,这是错误的
  • setState() 作用:1.修改 state 2.更新UI(指当状态变了后,最新的状态值会自动渲染到页面中,因为React底层会有监听,一旦调用了setState导致了数据的变化,就会重新调用一次render方法,重新渲染当前组件)
  • 在这个过程中,constructor只在生成组件实例的时候调用了1次;render()调用了n+1次,第一次和每次使用setState都会调用
  • 思想:数据驱动视图(数据先改变,驱动页面进行更新)

修改状态.png

import React from 'react'
import ReactDOM from 'react-dom'

export default class App extends React.Component {
    // 初始化state
    state = {
        count:1
    }
    render(){
        return (
            <div>
                <div>计数器 :{this.state.count}</div>
                <button onClick={() => {
                     this.setState({
                        count: this.state.count+1
                      })   
                }}>+1</button>
            </div>
        )
    }
}

三、组件间通讯

1. 通过props通讯

  • 组件是封闭的,要接受外部数据得通过props来实现
  • props的作用:接收传递给组件的数据
  • 传递数据:在页面使用组件时,给组件添加自定义属性。props是一个对象,可以往其中传入多个属性 <Hello name="rose" age={19} />
  • 接收(获取)数据:函数组件通过 参数 props接收数据,类组件通过 this.props接收数据

(1)函数组件

const Hello = props => {
  console.log(props)
  return (
    <div>
      <h1>props:{props.name}</h1>  //rose
    </div>
  )
}

(2)类组件

class Hello extends React.Component {
  render() {
     const { age } = this.props
    // console.log(this.props)
    return (
      <div>
        <h1>props: {age}</h1> //19
      </div>
    )
  }
}

(3)特点

  • 可以给组件传递任意类型的数据
  • props是只读属性,不能对值进行修改
  • 注意:使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props,其他的地方是可以拿到的(比如在render里)

(4)举例:

使用该组件时,传入值

//可以给组件传递任意类型的数据
 <Hello  
   name="rose"
   //传递非字符串数据 需要加{}
   age={19}
   colors={['red', 'green', 'blue']}
   fn={() => console.log('这是一个函数')}
   tag={<p>这是一个p标签</p>} //把JSX作为tag属性的值传递给组件
 />

在组件中,处理接收到的数据

import React from 'react'
import ReactDOM from 'react-dom'

// 类组件:
class Hello extends React.Component {
  // 推荐使用 props 作为 constructor 的参数,这样在 constructor 里可以直接用 props,或者用 this.props也可以
  constructor(props) {
    super(props)

    console.log('在构造函数中,可以使用props,也可以使用this.props:',props)
  }
  
  this.props.fn() //调用从外界传入本组件的函数
  
  // 错误演示!!!不能修改props的值
  // this.props.name = 'tom'

  render() {
    console.log('render里只能用this.props:', this.props)

    return (
      <div>
        <h1>props:{this.props.age}</h1>
      </div>
    )
  }
}

(5)批量传递props

组件:

class Hello extends React.Component {
  render() {
    const { age, name } = this.props
    return (
      <div>
        <h1>{age}</h1>
        <h1>{age}</h1>
      </div>
    )
  }
}

使用组件:

const p = { age:'19', name:'一粒沙' }
 <Hello {...p}/>

注意:...运算符不能用于展开对象,只是由于react+babel的存在,让...p可以用于给标签传递属性

组件间用props通讯的三种情形

(1)父(类组件) —> 子(函数组件)

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

// 父组件
class Parent extends React.Component {
// 1. 父组件提供要传递的数据
  state = {
    lastName: '王'
  }
  render() {
    return (
      <div className="parent">
 // 2. 使用子组件时,通过给子组件标签添加属性的方式,将数据传入子组件
        <Child name={this.state.lastName} />
      </div>
    )
  }
}

// 子组件
const Child = props => {
// 3. 子组件中通过props接收并使用从父组件传过来的数据    
  console.log('子组件接收到的所有数据的集合:', props)
  return (
    <div className="child">
      <p>子组件使用从父组件传来的数据:{props.name}</p>
    </div>
  )
}

(2)子(类组件) —> 父(类组件)

  • 父组件提供回调函数,用来接收数据(谁要接收数据谁就提供回调函数)

  • 子组件调用,将要传递的数据作为回调函数的实参。

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

// 父组件
class Parent extends React.Component {
  state = {
    parentMsg: ''
  }

  // 1、父组件提供回调函数,形参取名data,用来接收数据
  getChildMsg = data => {
    console.log('接收到子组件中传递过来的数据:', data)
    
    // 4、使用从子组件中传递过来的数据:把父组件的数据parentMsg进行更新
    this.setState({
      parentMsg: data 
    })
  }

  render() {
    return (
      <div className="parent">
        父组件:{this.state.parentMsg} //渲染从子组件传过来的数据
      //  2、 使用子组件时,将该函数作为属性的值,传递给子组件
        <Child getMsg={this.getChildMsg} />
      </div>
    )
  }
}

// 子组件
class Child extends React.Component {
  state = {
    msg: '111'
  } //要传递给父组件的数据

  handleClick = () => {
    // 3、子组件通过props调用父组件中传递过来的回调函数(将子组件的数据作为实参,传递给父组件)
    this.props.getMsg(this.state.msg)
  }

  render() {
    return (
      <div className="child">
        <button onClick={this.handleClick}>给父组件传递数据</button> 
      </div>
    )
  }
}

此时回调函数是用箭头函数(推荐使用)写的,箭头函数不会绑定this,所以会向外一层去寻找,外层是render方法,在render方法里面的this指向的是当前实例对象,所以调用回调函数时的this 是指向组件实例的。

(3)兄弟组件间传值

状态提升:将共享状态(二者间需要通讯的数据)提升到最近的公共父组件中,由公共父组件管理这个状态 兄弟传递.png

子组件B把数据传递给子组件A:

import React from 'react'
import ReactDOM from 'react-dom'

// 父组件
class Counter extends React.Component {
// 1. 公共父组件提供要共享的状态
  state = {
    count: 0
  }
// 2. 公共父组件提供操作共享状态的方法
  onIncrement = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
      // 把父组件的数据 通过给子组件添加属性 传给ChildA
        <ChildA count={this.state.count} />
      // ChildB通过回调函数,修改被共享的状态
        <ChildB onIncrement={this.onIncrement} />
      </div>
    )
  }
}

// 3. 要通讯的子组件A只需通过props接收状态(父传子),子组件B通过props调用修改状态的方法(子传父)

const ChildA = props => { 
  //接收数据
  return <h1>计数器:{props.count}</h1>
}

const ChildB = props => { 
//调用传入本组件的函数onIncrement
  return <button onClick={() => props.onIncrement()}>+1</button>
}

props进阶

(1)children属性

  • children属性: 表示组件标签的子节点,使用某组件时,该组件名包裹着的 就是它的子节点。当组件标签有子节点时,props就会自动有该属性
  • children属性与普通的props一样,值可以是任意值(如 文本、react元素、组件、甚至是函数)

children为:函数子节点

使用该组件时:

  <App>
  {
      () => console.log('这是一个函数子节点')
  }
  </App>

定义组件时:

import React from 'react'
import ReactDOM from 'react-dom'

const App = props => {
  console.log(props)
  props.children() //调用了本组件的函数子节点

  return (
    <div>
      <h1>组件标签的子节点:</h1>
      {/* {props.children}  子节点如果是函数,不能直接打印,只能调用*/}
    </div>
  )
}

children为:jsx(如p标签)或组件(如button组件)

使用该组件时:

const Test = () => <button>我是button组件</button>
   <App>
     {/* <p>我是子节点,是一个p标签</p> */}
     <Test />
   </App>

定义组件时:

import React from 'react'
import ReactDOM from 'react-dom'

 const App = props => {
   console.log(props)
   return (
     <div>
       <h1>组件标签的子节点:</h1>
       {props.children}
     </div>
   )
 }

children为:文本节点“我是子节点”

使用该组件时:

<App>我是子节点</App>

(2)props校验

允许在定义组件的时候,指定props的类型、格式等

对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,
就是说组件调用者可能不知道组件封装着需要什么样的数据。如果传入的数据不对,可能会导致报错

开启props校验的作用:使用组件时如果传递的数据不对,会报明确的错误提示

props-错误提示.png 使用步骤

  • 安装包 prop-types ( yarn add prop-types | npm i props-types)
  • 导入prop-types 包
  • 通过propTypes对象,给组件的props添加校验规则 组件名.propTypes={}

举例:

import React from 'react'
import ReactDOM from 'react-dom'

import PropTypes from 'prop-types'

const App = props => {
  const arr = props.colors
  const lis = arr.map((item, index) => <li key={index}>{item}</li>)

  return <ul>{lis}</ul>
}

// 添加props校验
App.propTypes = {
    //约定该属性为array类型
    //如果类型不对,则报出明确错误,便于分析错误原因
  colors: PropTypes.array
}

使用组件时,传入数据:

  <App colors={['red', 'blue']} />

常见的校验规则

  • 常用的类型: array、bool、func、number、object、string
  • React元素类型:element
  • 必填项:isRequired
  • 特定结构的对象: shape({})

举例:

添加props校验:

属性 a 的类型: 数值(number)

属性 fn 的类型: 函数(func)并且为必填项

属性 tag 的类型: React元素(element)

属性 filter 的类型: 对象({area: '上海', price: 1999})

import React from 'react'
import ReactDOM from 'react-dom'

import PropTypes from 'prop-types'

const App = props => {
  return (
    <div>
      <h1>props校验:</h1>
    </div>
  )
}

App.propTypes = {
  a: PropTypes.number,
  //在类型后加上了.isRequired,表示该类型必选
  fn: PropTypes.func.isRequired,
  tag: PropTypes.element,
  //特定结构的对象
  filter: PropTypes.shape({
    area: PropTypes.string,
    price: PropTypes.number
  })
}

使用组件时,函数是必传项:

  <App fn={() => {}} />

(3)props的默认值

  • 场景举例:分页组件 -> 每页显示条数
  • 作用:在自己未传入props时生效
import React from 'react'
import ReactDOM from 'react-dom'

const App = props => {
  console.log(props)
  return (
    <div>
      <h1>此处展示props的默认值:{props.pageSize}</h1>
    </div>
  )
}
//设置默认值
App.defaultProps = {
  pageSize: 10
}

使用组件时:

//未传入props,以默认值为准
// <App />
//传入了props,以传入值为准
<App pageSize={20} />

2. 使用Context通讯

如果两个组件相隔层级比较多(嵌套多层,比如爷爷传递数据给孙子),使用Context实现组件间通讯更方便(如果用props,需要一层层传递和接收,较为繁琐)

作用: 跨组件传递数据

函数组件

React Hooks-useContext的使用

类组件

  1. 调用 React.createContext() 创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件 const { Provider, Consumer } = React.createContext()

  2. 使用Provider组件作为父节点(一般在最外层设置)

  3. 设置value属性,表示要传递的数据

<Provider value="pink">
        <div className="app">
          <Node />
        </div>
</Provider>
  1. 调用Consumer组件接收数据:
  • 哪一层想要接收数据,就用Consumer进行包裹。
  • 其中的回调函数的参数data就是接收到的传递过来的值pink,回调函数的返回值是要渲染的JSX结构

Comsumer.png

举例:

组件间的嵌套关系:App—>Node—>SubNode—>Child

需求:通过APP组件把数据传递给Child组件

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

const { Provider, Consumer } = React.createContext()

class App extends React.Component {
  render() {
    return (
      <Provider value="pink">
        <div className="app">
          <Node />
        </div>
      </Provider>
    )
  }
}

const Node = props => {
  return (
    <div className="node">
      <SubNode />
    </div>
  )
}

const SubNode = props => {
  return (
    <div className="subnode">
      <Child />
    </div>
  )
}

const Child = props => {
  return (
    <div className="child">
      <Consumer>{data => <span>渲染数据:{data}</span>}</Consumer>
    </div>
  )
}

四、refs的使用

组件内的标签可以定义ref属性来标识自己

字符串形式的ref (已废弃)

其值"input1",就是实例对象的ref属性值中的一个key,该key的value就是当前标识了ref为"input1"的这个节点

回调函数形式的ref(较早版本的react):

其参数就是当前标识了ref的这个节点

class Demo extends React.Component{
    showData = ()=>{
        // console.log('箭头函数的this继承外界作用域,即组件对象实例',this)

        //字符串形式的ref(已废弃)
        // const { input1 } = this.refs
        // console.log('拿到的是虚拟DOM转成真实DOM之后的节点',input1)
        // alert(input1.value)

        //2、从实例自身上取input2
        const { input2 } = this
        alert(input2.value)
    }

    render(){
        return(
            <div>
                <input ref="input1" type="text" placeholder="字符串形式的ref"/>&nbsp;

                <button onClick={this.showData}>提示输入的数据</button>&nbsp;
                {/* 1、参数c拿到的就是当前标识了ref的这个节点,把该节点放在了组件实例自身上,取名叫input2*/}
                <input ref={c => this.input2 = c} type="text" placeholder="回调函数形式的ref"/>&nbsp;
            </div>
        )
    }
}

PS:在JSX里的注释方式,先写{},表明其中的是js表达式,然后再/* */

关于回调refs的说明:官方文档

class Demo extends React.Component{
    state = {isHot:false}
    showInfo = ()=>{
        const {input1} = this
        alert(input1.value)
    }
    changeWeather = ()=>{
        //获取原来的状态
        const {isHot} = this.state
        //更新状态
        this.setState({isHot:!isHot})
    }
    saveInput = (c)=>{
        this.input1 = c;
        console.log('参数c:',c);
    }
    render(){
        const {isHot} = this.state
        return(
            <div>
                <h2>今天天气很{isHot ? '炎热':'凉爽'}</h2>
                
                {/* 用内联方式写回调函数:*/}
                {/*<input ref={(c)=>{this.input1 = c;console.log('参数c:',c);}} type="text"/><br/><br/>*/}
                {/*将 ref 的回调函数定义成 class 的绑定函数的方式*/}
                <input ref={this.saveInput} type="text"/><br/><br/>
                
                <button onClick={this.showInfo}>提示输入的数据</button>
                <button onClick={this.changeWeather}>更新状态</button>
            </div>
        )
    }
}

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。所以:

页面初次render:参数c:<input type="text">

后续更新状态时调用render:

参数c:null
参数c:<input type="text">

解决方式:将 ref 的回调函数定义成 class 的绑定函数的方式

ref={this.saveInput}把函数放在了类实例对象的自身上

无关紧要,写成内联也可以。

使用createRef()创建的ref属性:React 16.3 版本引入

调用React.createRef(),返回一个容器,其中的current属性用于存储被ref标识的节点

class Demo extends React.Component{
    //给Demo类的实例对象上追加一个属性a,值为1
    a = 1	

    //给Demo类的实例对象追加属性,值为一个容器。
    myRef = React.createRef()
    //React.createRef()创建的容器,只能专用(存一个节点)
    myRef2 = React.createRef()

    showData = ()=>{
        console.log("容器myRef:",this.myRef) // {current:input}  current是固定属性
        alert(this.myRef.current.value);
    }
    showData2 = ()=>{
        alert(this.myRef2.current.value);
    }
    render(){
        return(
            <div>
                {/*把当前ref所标识的input节点,存储到了myRef这个容器里*/}
                <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showData}>点击按钮提示数据</button>&nbsp;

                <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>&nbsp;
            </div>
        )
    }
}