React类组件基础02-组件间通信和生命周期

1,223 阅读8分钟

写在前面:

1.本文是个人课上学习内容的总结和梳理,主要知识点来自于“开课吧”线上直播课程,以及 React 官方文档。

2.目前尚处于 React 乃至前端入门阶段,因此很多内容理解的不是很透彻,文章更多是用来学习记录而非干货分享。

因此,建议如果需要解决项目问题,还是去看一下其他大佬的文档以及 React 官方文档(一手资料)

组件间通信

如果不借助第三方库,原生react是只支持单向流动:父级可以向子级通过props的方式发送state和回调函数,而子级不能向父级发送事件或者修改父级的props。所以如果子级想要修改父级组件的状态(也就是数据),只可以通过父级传递下来的回调函数进行执行。

相邻层级组件间的通信(同级/父子)

借助上一篇笔记中最后写到的实例,如果我们想要在展开一栏的时候,关闭掉其他的栏,可是在App组件中是直接实例化了三个Dl组件,这三个组件之间是无法互相通信的,只有在他们的父组件(也就是App)中声明一个state状态,利用props传递给每个实例,并声明一个改变该state状态的回调函数,一并传给三个实例。这样当每个Dl实例触发这个回调函数时,App组件就会修改state状态,从而实现了同级组件之间的通信

//App组件
//声明一个state状态,负责表示哪个Dl窗口是展开的。并声明一个回调函数,负责控制state状态。
state = {
    openname: "" //openname 记录的是当前应该哪一项展开,如果为空则都不展开
}
changeOpen = (name) => { //回调函数触发时,会讲修改state状态
    this.setState({
        openname: name
    })
}
render() {
    const {openname} = this.state;
    return <div className="friend-list">
        {Object.keys(data).map((item,index)=>{
            return <Dl 
              key={index} 
              data={data[item]} 
              name={item}
              openname={openname} //把这个状态传递给所以实例化的Dl组件
              changeOpen = {this.changeOpen} //把更改openname状态的回调函数也传递给所有Dl组件
            />
        })}
    </div>
  }
// Dl组件
render() {
    let { data, name, openname, changeOpen } = this.props; // 从props中拿到openname和changeOpen
    return <dl className={`friend-group ${openname === name? "expanded" : ""}`}>  <!--通过openname判断是否展开-->
      <dt onClick={() => {
       changeOpen(openname === name?"":name); //通过拿到的changeOpen回调函数,修改openname状态
      }}>{data.title}</dt>
      {data.list.map((item, index) => {
        return <dd key={index}>{item.name}</dd>
      })}
    </dl>
  }

跨级组件间通信(爷孙)

假设有这样三个组件:

  • App组件:是根组件
  • Child组件:是App组件的子组件
  • SubChild组件:是Child组件的子组件

这样如果SubChild组件接收来自App组件的信息,那么中间必须要过一道Child,如果用笨方法,可以通过一层层的拿到props=>结构props=>传递给下一层=>下一层继续结构props,这样慢慢的传递下去。显然这样可以,但是很麻烦,所以原生React提供了一种跨级组件通信的方式(有点类似vuex)—— context

首先需要通过createContext()方法声明一个context实例,这里为了方便区分,就叫它contextStorecontextStore实例提供了两个方法ProviderConsumer,从名字就可以看出来是分别表示提供和接收状态的。

import {createContext} from "react";
const contextStore = createContext();
const {Provider,Consumer} = context;
export default contextStore;
export {Provider,Consumer};

在提供状态的App组件中,将需要借用context方法跨级传递的状态放在<Provider>标签的props里面,标签内包裹上需要转递给的组件。

render() {
    const {count,name} = this.state;
    return <>
      <Provider
        value={{
          count,
          name,
          setCount:this.setCount
        }}
      >
        <Child />
      </Provider>  
    </>
  }

这样在Child组件里面就不需要做任何处理,SubChild组件就可以直接拿到数据。当然,如果Child组件想要用这些数据,还是可以直接用this.props取到。取数据有两种方式,分别是使用<Consumer>标签获取和通过contextType静态属性声明。

通过<Consumer>,标签内包裹需要返回的虚拟DOM结构,需要的数据要从context中解构出来。

  render() {
    return <Consumer>
      {(context) => {
        const { count, name } = contextStore;
        return <div>
          <p>count:{count}</p>
          <p>name:{name}</p>
        </div>
      }}
    </Consumer>
  }

还有一种方式,就是通过声明Component类自带的静态属性contextType来告诉类组件从哪里获取数据

class SubChild extends Component {
  static contextType = contextStore;// contextType ,告诉当前组件,当前SubChild组件中context值,从上面写的context.hs文件导出的contextStore实例中获取,也就是可以理解为 let this.context = contextStore。
  render() {
    const { count, name, setCount } = this.context; //这个this.context就是类组件自己的context了,注意不要跟上面的contextStore搞混
    return <div>
      <p>count:{ count}</p>
      <p>name:{name }</p>
      <button onClick={()=>{
        setCount(count +1);
      }}>递增</button>
    </div>
  }
}

PS:不推荐直接使用context处理跨组件通信。context一般是给第三方库用的,后续会通过第三方库来处理跨级通信问题。

类组件的生命周期

挂载阶段

组件创建 => 把组件创建的虚拟DOM => 生成真实DOM =>添加到我们的DOM树中

  • constructor:组件初始化

  • static getDerivedStateFromProps(props) :将传递进来的props关联到state里面,注意这个函数一定要有返回值

  • componentDidMount:生成真实的DOM节点,通常在这个阶段处理副作用(请求)

  • render:组件完成挂载,虚拟DOM已经生成了真实DOM

class Child extends Component {
  constructor(props){
      super(props);
      console.log(0,"组件初始化");
  }
  // 将props 关联到state中
  static getDerivedStateFromProps(props){
      //console.log(this,props);该方法中没有 this
      console.log(1," 将props 关联到state中");
      return { //该生命周期函数必须有返回值,返回值中保存的是从 props 中 要关联到 state 中数据
        info: "要关联的数据"
      }
  } 
  state={
    count: 1
  }
  componentDidMount(){
    console.log(3,"组件挂载完成,虚拟DOM,已经生成了真实DOM");
  }
  render() {
    console.log(2,"生成虚拟DOM",this.state);
    const {count} = this.state;
    return <div>
        <p>{count}</p>
        <button onClick={()=>{
          this.setState({
            count: count + 1
          })
        }}>递增</button>
    </div> 
  }
}

更新阶段

组件状态更新 => 组件重新渲染,从setstate之后组件就开始更新,一直到完成对真实的DOM节点更新

static getDerivedStateFromProps(props, state):这是一个静态方法,用来从props冲获取最新的state

shouldComponentUpdate():判断组件是否需要更新,以及哪些地方需要更新

render():重新渲染

getSnapshotBeforeUpdate():已经完成了新的虚拟DOM节点构建,但是没有更新到真实DOM,用来获取到更新前的DOM快照

componentDidUpdate():组件更新完成

class Child extends Component {
  // 将props 关联到state中
  static getDerivedStateFromProps(props){
      //console.log(this,props);该方法中没有 this
      console.log(1," 将props关联到state中");
      
      return { //该生命周期函数必须有返回值,返回值中保存的是从 props 中 要关联到 state 中数据
        info: "要关联的数据"
      }
  } 
  state={
    count: 1
  }
  shouldComponentUpdate(nextProps,nextState){
      //nextProps 更新后的props, nextState 更新后的state
      //console.log(nextState,this.state);
      console.log(2,"判断组件是否需要更新");
      return true; //返回值为 true 则继续执行更新,返回值为false 则不执行更新,后续的生命周期函数,也不会执行
  }
  getSnapshotBeforeUpdate(){
    //已经完成新的虚拟DOM的构建,但是还未更新真实DOM
    //用来获取更新前的DOM快照
    console.log(4,"已经完成新的虚拟DOM的构建,但是还未更新真实DOM");
    let box = document.querySelector("#box");
    return box.innerHTML;//改返回值会传递给componentDidUpdate的prevDOM参数
  }
  componentDidUpdate(prevProps,prevState,prevDOM){
    // console.log(prevDOM);
    console.log(5,"组件更新完成");
  }
  render() {
    console.log(3,"生成虚拟DOM",this.state);
    const {count} = this.state;
    return <div id="box">
        <p>{count}</p>
        <button onClick={()=>{
          this.setState({
            count: count + 1
          })
        }}>递增</button>
    </div> 
  }
}

卸载阶段

componentWillUnmount():组件即将销毁,可以在这个阶段清空各种监听事件。

class Child extends Component {
  state={
    count: 1
  }
  componentDidMount(){
    let getSize = ()=>{
      let size = document.querySelector("#size");
      size.innerHTML = window.innerWidth;
    }
    getSize();
    window.onresize = getSize;
  }
  componentWillUnmount(){
    window.onresize = null;
  }
  render() {
    const {count} = this.state;
    return <div id="box">
        <p>{count}</p>
        <button onClick={()=>{
          this.setState({
            count: count + 1
          })
        }}>递增</button>
        <div id="size">0</div>
    </div> 
  }
}

受控组件

Vue给我提供了一个语法糖v-model来实现表单与数据的双向绑定,但是React比较懒,专注于渲染视图,并没有提供这样的语法糖,需要我们通过stateprops手动声明,声明好之后就形成了一个双向绑定的受控组件。会用到这个受控组件的场景也很类似,需要输入的表单或者需要打钩的CheckBox

  • value: 将组件的状态,赋值给控件的value属性,通过onChange事件监听控件的value属性发生变化,再通过回调函数赋值给组件的state

  • checked:将组件的状态,赋值给控件的 checked 属性,通过 onChange 事件监听控件的 checked 属性发生变化,再通过回调函数赋值给组件的state

class App extends Component {
  state={
    val:""
  }
  componentDidMount(){
    let text1 = document.querySelector("#text1");
    text1.oninput = function(){
      console.log(this.value);
    }
  }
  render() {
    const {val} = this.state;
    return <div>
        <input
           type="text" 
           value={val}  
           onChange={({target})=>{
              this.setState({
                val: target.value
              })
           }}
        />
        <input type="text" id="text1" />
        <input type="text" id="text2" defaultValue="初始值" />
        <p>{val}</p>
        <button onClick={()=>{
          this.setState({
            val:""
          })
        }}>重置</button>
    </div>
  }
}

非受控组件

  1. 需要设置初始值,只设置初始值,并不希望该控件受控,不要用value|checked,因为只要用 value|checked 的话react 就认为我们做受控组件。所以这里要用 defaultValuedefaultChecked

    <input
    type="text" 
    defaultValue={val}  
    onChange={({target})=>{
       this.setState({
         val: target.value
       })
    }}
    />
    
  2. 初始值,直接留空。

实例

// App.js

import { Component } from "react";
import "./index.css";
import AddMeaasge from "./AddMeaasge";
import MessageList from "./MessageList";

class App extends Component{
    state = {
        data:[{
                id:0,
                name:"宋成昊",
                msg:"作业暗号:海哥真帅"
            }]
    };
    addMeaasge = (newName, newMsg) => {
        const {data} = this.state;
        data.push({
            id:Date.now(),
            name:newName,
            msg:newMsg
        });
        this.setState({
            data
        })

    };
    remove = (id) => {
        const {data} = this.state;
        this.setState({
          data:data.filter(id => {return data.id = !id })
        })
    }
    render(){
        let { data } = this.state
        return <section className="wrap">
        <h2 className="title">留言板</h2>
        <AddMeaasge 
            addMeaasge = {this.addMeaasge}
        />
        <MessageList 
            data = { data }
            remove = { this.remove }
        />
    </section>
    };
}

export default App;
// MessageList.js

import { Component } from "react";

class MessageList extends Component{
    render(){
        const { data,remove } = this.props

        return <ul className="messageList">
        {data.map((item,index) => {
            return <li key={index}>
            <h3>{item.name}</h3>
            <p>{item.msg}</p>
            <a onClick={()=>{
                const id =item.id
                remove(id)
            }}>删除</a>
        </li>
        })}
    </ul>
    };
};

export default MessageList
//AddMeaasge.js

import { Component } from "react";

class AddMeaasge extends Component{
    state={
        newName:"",
        newMsg:""
    }
    render(){
        const {newName, newMsg} = this.state
        const { addMeaasge } = this.props
        return <div className="addMessage">
        <input 
            type="text" 
            placeholder="请输入昵称" 
            autoComplete="off"
            value={newName}
            onChange={({target})=>{
                this.setState({
                    newName:target.value
                })
            }}
            />
        <textarea 
            placeholder="请输入留言"
            value={newMsg}
            onChange={({target})=>{
                this.setState({
                    newMsg:target.value
                })
            }}
            ></textarea>
        <button
            onClick={() => {
                addMeaasge(newName,newMsg);
                this.setState({
                    newName:"",
                    newMsg:""
                })
            }}
        >提交留言</button>
    </div>
    }
};

export default AddMeaasge
// index.css
// 略

最终效果:

最终效果