react生命周期&State

生命周期&State


点击跳转来源:

声明周期方法

我们需要在react组件渲染的不同阶段,触发不同的事件逻辑,所以我们需要有生命周期方法.以下列举几个常用的生命周期函数:
componentDidMount(): 组件已经渲染完毕.
componentDidUpdate(): 组件更新后会被立即调用.
componentWillUnmount(): 组件即将销毁.
不常用生命周期函数:
static getDerivedStateFromProps(props, state): render 方法之前调用,并且在初始挂载及后续更新时都会被调用. 文档:
shouldComponentUpdate(): 根据返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响.
getSnapshotBeforeUpdate(prevProps, prevState): 在最近一次渲染输出(提交到 DOM 节点)之前调用.

class HelloWorld extends React.Component {
      componentDidMount(){
        console.log('组件渲染完毕...');
      }
      componentWillUnMount(){
        console.log('组件即将销毁...');
      }
      render(){
        console.log('render...')
        return (
          <div className={'danger large'}>
            <h2>倒计时</h2>
            { /* className和class属性是一样的,而且必须是驼峰 */ }
            <span>{ new Date().toString() }</span>
          </div>
        )
      }
    }
    // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
    ReactDOM.render(<HelloWorld />,document.getElementById('root'));

在 componentDidMount中声明当前时间:

class HelloWorld extends React.Component {
  componentDidMount(){
    console.log('this',this);
    this.nowTime = new Date().getTime();
  }
  componentWillUnMount(){
    console.log('组件即将销毁...');
  }
  render(){
    console.log('render...')
    return (
      <div className={'danger large'}>
        <h2>倒计时</h2>
        { /* className和class属性是一样的,而且必须是驼峰 */ }
        <span>{ this.nowTime }</span>
      </div>
    )
  }
}
// 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
ReactDOM.render(<HelloWorld />,document.getElementById('root'));

我们发现,可以打印出实例对象,存在nowTime属性,但是页面不渲染.** 为什么?**
因为render只在初始化的时候执行了一次,而nowTime是在componentDidMount生命周期函数中才添加的.
也就是说,如果我们想要得到页面渲染的结果:

  1. 在构造函数中优先创建nowTime
  2. componentDidMount 中修改nowTime的值

构造函数

  1. 在构造函数中优先创建nowTime

我们可以对class添加构造函数 constructor() , 如果对es6 class部分不清楚的同学,需要回顾下相关知识点.
我们发现 constructor()中的 *this * 和 render() 中的this是一致的,指向 HelloWorld组件的实例对象.
注意: 构造函数中的super() 必须调用 , 文档 .

  1. componentDidMount 中修改nowTime的值

我们尝试在生命周期函数中,修改nowTime, 发现也打印修改后的内容,但页面并没有被渲染. 也就是说直接手动修改实例对象的属性,并不会触发**render() ***执行 *(这个和vue的双向数据绑定不同,react是没有双向数据绑定的).
那如何才能在数据被修改的时候,重新触发render()呢?

class HelloWorld extends React.Component {
      constructor(){
        // 如果写了构造函数,那么super必须写
        super();
        console.log('构造函数...');
        this.nowTime = new Date().getTime();
      }
      componentDidMount(){
        console.log('this',this);
        setTimeout(()=>{
          this.nowTime = '新时间';
          console.log(new Date().getTime(),this.nowTime)
        },3000)
      }
      componentWillUnMount(){
        console.log('组件即将销毁...');
      }
      render(){
        console.log('render...')
        return (
          <div className={'danger large'}>
            <h2>倒计时</h2>
            { /* className和class属性是一样的,而且必须是驼峰 */ }
            <span>{ this.nowTime }</span>
          </div>
        )
      }
    }
    // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
    ReactDOM.render(<HelloWorld />,document.getElementById('root'));

state和setState()

创建响应式属性

state 是组件的属性 , 组件中的 state 包含了随时可能发生变化的数据。state 由用户自定义,它是一个普通 JavaScript 对象。
我们不能直接修改state本身,可以通过api **setState()**** **方法来修改state.
利用setState在componentDidMount 中修改nowTime的值:

class HelloWorld extends React.Component {
      constructor(){
        // 如果写了构造函数,那么super必须写
        super();
        console.log('构造函数...');
        this.state = {
          nowTime: new Date().getTime()
        }
      }
      componentDidMount(){
        console.log('this',this);
        setTimeout(()=>{
          this.setState({
            nowTime: '新时间'
          })
          console.log(new Date().getTime(),this.state.nowTime)
        },3000)
      }
      componentWillUnMount(){
        console.log('组件即将销毁...');
      }
      render(){
        console.log('render...')
        return (
          <div className={'danger large'}>
            <h2>倒计时</h2>
            { /* className和class属性是一样的,而且必须是驼峰 */ }
            <span>{ this.state.nowTime }</span>
          </div>
        )
      }
    }
    // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
    ReactDOM.render(<HelloWorld />,document.getElementById('root'));

页面得到更新,也就是说,state可以创建 响应式 对象,我们可以通过setState()来实现对数据的修改,同时触发render() 函数更新页面.

注意1: 不要直接修改 State

// 这样无法触发UI更新
this.state.nowTime = 'xxx';
//而应该使用
this.setState({nowTime='xx'});

注意2: State 的更新可能是异步的

出于性能考虑,React 可能会把多个 [setState()](https://zh-hans.reactjs.org/docs/react-component.html#setstate) 调用合并成一个调用。所以你不要依赖他们的值来更新下一个状态。

// 这种事错误的
this.setState({
  counter: this.state.counter + 1,
});
// 可以通过回调方法来获取到上一次更新后的state.
// 回调方法接收两个参数,props下一章节再讨论
this.setState((state,props) => ({
  counter: state.counter + 1
}));

回调:

setState(updater, [callback])

setTimeout中调用setState()

componentDidMount(){
  console.log('组件挂载完毕...');
  // 1. 更新一次count ,检查setState()是否是异步的
  // 2. setState()会合并更新,某一次更新如果依赖于上一次的值,可能会有问题
  // 3. setState() 如果写到setTimeout/setInterval中,会变成同步函数.
  // 尝试修改nowTime
  setTimeout(()=>{
    this.setState({
      count: this.state.count + 1   // 1
    })
    console.log('this.state.count_1',this.state.count);
    this.setState({
      count: this.state.count + 1  // 2
    })
    // 不能直接修改state的值,需要通过setState()
    this.setState({
      nowTime: new Date().toString()
    })
    // console.log('this.nowTime',this.state.nowTime);
    console.log('this.state.count_2',this.state.count); // 2?
  },3000)
}

注意3: State 的更新会被合并

setState是对state进行了浅合并,只会修改相同的属性,保留其他属性.

constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }
componentDidMount() {
  fetchPosts().then(response => {
    this.setState({
      posts: response.posts
    });
  });

  fetchComments().then(response => {
    this.setState({
      comments: response.comments
    });
  });
}

完整的声明周期函数

声明: 放到Ref课程之后讲解比较合适.

constructor(): 构造函数.
render(): 渲染/更新.
componentDidMount(): 组件已经渲染完毕.
componentDidUpdate(): 组件更新后会被立即调用.
componentWillUnmount(): 组件即将销毁.
不常用生命周期函数:
static getDerivedStateFromProps(props, state): render 方法之前调用,并且在初始挂载及后续更新时都会被调用. 文档:
shouldComponentUpdate(): 根据返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响.
getSnapshotBeforeUpdate(prevProps, prevState): 在最近一次渲染输出(提交到 DOM 节点)之前调用.

class HelloWorld extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1,
    };
    this.ulRef = React.createRef();
    console.log('constructor...');
  }
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps...", this);
    return {
      age: 10,
    };
  }
  componentDidMount() {
    console.log("componentDidMount...");
  }
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate...", snapshot);
  }
  shouldComponentUpdate(){
    console.log('shouldComponentUpdate...',this.state.number)
    // 判断是否渲染UI
    // return (this.state.number%2==0)
    return true;
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate...");
    return null;
  }
  incNumber() {
    this.setState((state) => ({
      number: ++state.number,
      age: ++state.age,
    }));
  }
  render() {
    console.log("render...", this);
    return (
      <div>
        <ul ref={this.ulRef} onClick={() => this.incNumber()}>
          {Array(this.state.number)
            .fill(null)
            .map((item, index) => (
            <li key={index}>{index}__</li>
          ))}
        </ul>
        <p>age: {this.state.age}</p>
      </div>
    );
  }
}
ReactDOM.render(<HelloWorld />, document.getElementById("root"));

了解:getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate()在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给componentDidUpdate()。
此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
应返回 snapshot 的值(或null)。

<style>
    *{
      margin: 0;
      padding: 0;
    }
    .ul-chat{
      width: 200px;
      height: 200px;
      border: 1px solid red;
      margin: 30px;
      overflow: auto;
    }
    .ul-chat>li{
      height: 30px;
      font-size: 20px;
      line-height: 30px;
      text-indent: 2em;
    }

  </style>

class HelloWorld extends React.Component {
      constructor(props){
        super(props);
        console.log('constructor....');
        // 创建ref
        this.chatUlRef = React.createRef();
        this.state = {
          chatItems: [
            {
              id: 1,
              text: '吃了么'
            },
            {
              id: 2,
              text: '吃了!'
            },
            {
              id: 3,
              text: '吃啥了?'
            },
            {
              id: 4,
              text: '吃好吃的!'
            }
          ]
        }
        // bind
        this.handleKeyUp = this.handleKeyUp.bind(this);
      }

      componentDidMount(){

      }
      // 在state被更新后触发,而又没来得及更新到UI的时候
      // 这里可以获取到更新之前的UI状态,比如滚轮位置
      // prevState 上一次的state
      // 此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
      getSnapshotBeforeUpdate(prevProps, prevState){
        // 1. 得到之前容器的高度
        // 注意: 必须是等内容高度发生变化的时候再处理
        if(this.state.chatItems.length > prevState.chatItems.length){
          // 这里没来得及更新UI,所以得到的是上一次更新后的DOM
          var chatUl = this.chatUlRef.current;
          // 之前的容器内容高度
          var preContentHeight = chatUl.scrollHeight - chatUl.scrollTop;
          return preContentHeight;
        }
        // 如果聊天列表高度没变化,则返回null
        return null;

      }
      // snapshot 是通过getSnapshotBeforeUpdate声明周期函数返回的
      componentDidUpdate(prevProps, prevState, snapshot){
        console.log('didUpdate....',this.state);
        console.log('snapshot',snapshot);
        // 2. 用更新后的最新高度 - 原来的内容高度 得到的就是需要被卷进去的高度
        if(snapshot){
          var chatUl = this.chatUlRef.current;
          // 计算需要被卷入的高度(多余的高度)
          var useScrollTop = chatUl.scrollHeight - snapshot;
          // 3. 赋值
          chatUl.scrollTop = useScrollTop;
        }

      }
      // 如果是输入了enter,需要把最新文本内容 更新到state.chatItems
      handleKeyUp(e){
        // keyCode 可以获取键盘按键的 Ascii编码值
        var keyCode = e.keyCode;
        // 如果值== 13 说明是enter键
        if(keyCode == 13){
          // 获取input的value
          // 不能直接修改state的值,必须通过setState才能触发render()!!!
          // 所以如果state的某个值是对象类型,需要先克隆
          // [...xxx] 数组结构
          var chatItems = [...this.state.chatItems];
          chatItems.push({
            id: new Date().getTime(), // 时间戳肯定不同
            text: e.target.value
          })
          // 用setState更新
          this.setState({
            chatItems
          })
          // 更新后还原输入框
          e.target.value = '';
        }
      }
      render(){
        console.log('render....',this.state);
        var liItems = this.state.chatItems.map(item=>(
          <li key={item.id}>{item.text}</li>
        ))
        return (
          <div>
              <input type="text" onKeyUp={this.handleKeyUp} placeholder="请输入聊天内容" />
              <ul className="ul-chat" ref={this.chatUlRef}>
                  {liItems}
              </ul>
          </div>
        )
      }
    }
    // 使用react 相关api 渲染到容器
    ReactDOM.render(<HelloWorld />,document.getElementById('root'));

猜你喜欢

转载自blog.csdn.net/m0_67388537/article/details/131937382