掌握React基础知识第三章-Props和State

751 阅读8分钟

2022即将到来,前端发展非常迅速,现在的前端已经不再是写写简单的页面了,伴随着web大前端,工程化越来越火,也出现了很多框架。很多公司也不单单是使用单一的框架了,作为前端开发国内最火的两大框架,都是必须要掌握的。所以,我决定在这疫情肆虐的年底把React学习一下,也为自己将来找工作多一分竞争力...

学习阶段,如有不对之处,欢迎评论区留言,我及时更正

本文已连载,其它章节传送门⬇️

第一章-jsx语法认识

第二章-函数组件和class类组件

第四章-事件处理

第五章-Ref和Refs

第六章-生命周期

第七章-跨组件通信Context

第八章-React Hooks

Props和State

React中使用props接收数据,state维护自己的数据。并且props是只读的属性,不能修改,如果修改数据,我们只能去修改父组件的state。

Props

💡 props可以接收多种类型的数据,甚至可以接收一个组件,非常的灵活,我们可以通过props传递任何数据

传递普通数据

import {Component} from 'react'
import ReactDOM from 'react-dom'

class Demo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      date: '2022-01-14'
    }
  }
  render() {
    return(
      <Date date={this.state.date} />
    )
  }
}

function Date(props) {
  console.log(props)
  return (
    <h1>{props.date}</h1>
  )
}
ReactDOM.render(<Demo />, document.querySelector('#root'))

打印下props,我们可以看下它长什么样子

截屏2022-01-14 下午2.43.06.png

传递一段结构

import {Component} from 'react'
import ReactDOM from 'react-dom'

class Demo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      date: '2022-01-14'
    }
  }
  render() {
  const element = <h1>{this.state.date}</h1>
    return(
      <Date date={element} />
    )
  }
}

function Date(props) {
  console.log(props)
  return (
    <div>
      {props.date}
    </div>
  )
}
ReactDOM.render(<Demo />, document.querySelector('#root'))

我们这里改了一下代码,通过props传递一段结构看来也是可以的

截屏2022-01-14 下午2.59.45.png

这里可以看到props接收的date属性是一个react的dom元素,细心的小伙伴发现date属性还有一个props属性,并且里面有一个children属性,里面是我们父组件的date,再去看看父组件,这个值是渲染在 h1 标签里面的,也就是说写在中间的内容会自动给props增加一个属性。

props的children属性

接着上面的我们试着在组件中间传递一段结构或者文本试试

import {Component} from 'react'
import ReactDOM from 'react-dom'

class Demo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      date: '2022-01-14',
      name: '小明'
    }
  }
  render() {
    return(
      <Date date={this.state.date}>
        {this.state.name}
      </Date>
    )
  }
}

function Date(props) {
  console.log(props)
  return (
    <div>
      <h1>{props.date}</h1>
    </div>
  )
}
ReactDOM.render(<Demo />, document.querySelector('#root'))

截屏2022-01-14 下午3.30.52.png 我们可以看到,props身上自动多了一个children属性,由于我们传递的是字符串,这里children属性值就是一个普通的字符串,如果我们传递了一段结构或者一个组件,那么它就是一个对象,如果我们传递了多个结构或者组件,那么它的值就是一个数组

代码略了,直接截图console的结果:

截屏2022-01-14 下午3.43.43.png

截屏2022-01-14 下午3.44.27.png

如果children是数组的话,我们可以通过遍历的方法拿到所有的子节点喔~

State

💡 state提供用来维护组件的数据,类似于vue中的data函数

还是看上面的代码吧,只有类组件才有state,它提供了组件初始化的数据,我们可以通过this.state.属性名拿到。这里很简单就不多说了,接下来让我们看一下如何修改state数据吧

React提供唯一可以修改state的方法:setState()

比如一个小需求,实现一个计数器的功能,点击按钮可以加减数据

class Demo extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 0
    }
  }
  handelClick = (num) => {
    this.setState({
      counter: this.state.counter + num
    })
  }
  render() {
    return(
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={() => {this.handelClick(1)}}>+ 1</button>
        <button onClick={() => {this.handelClick(-1)}}>- 1</button>
      </div>
    )
  }
}
ReactDOM.render(<Demo/>, document.querySelector('#app'))

暂且这么写,这里涉及到this问题,下面会讲,可以看到我们通过setState 方法成功修改了数据

setState使用

React中,setState方法是修改数据唯一的方法,它是一个回调函数,接收一个对象形式的参数或者接收一个回调函数作为参数。 👉关于setState的官网说明

对象参数

💡 问题:当我们调用setState后立马打印此时的数据,不会显示更新后的,而是显示上一次的,并且多个setState会合并为一个,后面的会覆盖上面的,如下图所示

add = () => {
    const { count } = this.state
    this.setState({
      count: count + 1
    })
    this.setState({
      count: count + 2
    })
    this.setState({
      count: count + 1
    })
    console.log(count)
  }

动画.gif

React会把setState接收的对象参数放到一个更新队列里面,把所有的对象提取出来合并的到一起,然后才会触发组件更新,所以多次调用setState只会触发一次重新渲染,毕竟dom更新成本是比较昂贵的

回调参数

💡 思考:如果我们需要多次调用,数据依赖于上一次的结果,有没有办法解决呢? — 除了对象形式的参数,setState还可以接收一个回调形式的参数,回调参数接收一个参数,此参数就是上一次执行的结果

add = () => {
    const { count } = this.state
    this.setState((pre) => {
      return {
        count: pre.count + 1
      }
    })
    this.setState((pre) => {
      return {
        count: pre.count + 1
      }
    })
    this.setState((pre) => {
      return {
        count: pre.count + 1
      }
    })
    console.log(count)
  }

动画.gif

需要注意的是,无论是对象形式的回调还是 函数形式的回调,setState调用后立马都是拿到上一次的数据,而不是这次的,那么我们就需要在组件更新完成后再去获取数据,而不是立即获取

setState可以接收二个函数作为参数,第二个函数参数会等待setState执行后在再执行,那么我们也可以在第二个回调参数中去获取数据

setState同步or异步

💡结论: setState是同步,表现异步行为是由于React的优化机制,setState只在React引发的事件处理(如onClick)和钩子函数中表现“异步”的,在原生事件(选定DOM 监听addEventListener)和setTimeout 中都是同步的

👉关于setState同步异步官网说明

React事件处理

class Demo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      date: '2022-01-14',
      name: '小明',
      count: 0
    }
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count, '第一次log')
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count, '第二次log')
    setTimeout(() => {
      console.log(this.state.count, '第三次log')
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count, '第四次log')
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count, '第五次log')
    })
  }
  render() {
    return(
      <div>
        <h1>显示当前count:{this.state.count}</h1>
        <button onClick={this.handleClick}>按钮</button>
      </div>
    )
  }
}

截屏2022-01-17 上午10.35.24.png

我们可以看到第二次log的时候,由于刚调完setState 立即打印的话,拿到的还是初始值,这也说明了多次调用react会合并到一个队列中,并不会立即去更新state,而是一次性的更新state,也就是表现的像异步;接着看第三次log以后的就已经是立即更新state了,说明在setTimeout中是同步的。

React钩子函数

componentDidMount = () => {
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count, '第一次log')
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count, '第二次log')
    setTimeout(() => {
      console.log(this.state.count, '第三次log')
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count, '第四次log')
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count, '第五次log')
    })
  }

我们把 handleClick函数改为react生命周期钩子函数componentDidMount ,此时打印结果和事件处理中的是一样的,再次说明了上面的结论。

为什么 React 不同步地更新 this.state?

看了官网的说明,意思是为了高效的更新渲染,react把setState 放到队列中等待一次性执行,从而避免了组件会重复多次渲染的问题。试想,如果不这样处理,调用一次直接渲染一次,性能开销和体验肯定也是不好的,一次更新只需要重新渲染一次就可以解决的,没必要多次重新渲染导致重绘

setState不可变的力量

我们知道js中,简单类型的数据是直接占的内存,而复杂类型的数据只是一个引用,当我们修改简单类型的数据时,是直接可以修改掉的。而当我们修改复杂类型的数据时候,比如修改obj.name = ‘小明’,其实它的引用地址并没有修改,还是指向的同一块内存空间

export default class Index extends Component {
  constructor(props) {
    super(props)
    this.state = {
      list: [
        {
          name: '小明',
          id: 1
        },
        {
          name: '小红',
          id: 2
        }
      ]
    }
  }
  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextState)
    if(this.state.list !== nextState.list) {
      return true
    }
    return false
  }
  addName = () => {
    const obj = {name: '小刚', id: 3}
    this.state.list.push(obj)
    this.setState({
      list: this.state.list
    })
  }
  render() {
    console.log('render函数执行了');
    return (
      <div>
        <ul>
          {
            this.state.list.map((item) => {
              return <li key={item.id}>{item.name}</li>
            })
          }
        </ul>
        <button onClick={this.addName}>按钮</button>
      </div>
    )
  }
}

比如我们有一个小需求,点击按钮,数组新增一条数据,并且在shouldComponentUpdate 钩子函数里,通过对比list的值是否变化来决定是否更新组件,此时如果我们是直接往list里push一个对象的话,我们知道数组是引用类型,引用的那个地址并没有变化,在对比前后state的时候就不会有变化,导致return false,组件并不会更新。但是打印看nextState此时实际是已经加进去了的,无论点击多少次组件都不会更新,因为引用地址始终没有变化

我们修改下代码:

addName = () => {
    const arr = [...this.state.list]
    arr.push({
      name: '小刚',
      id: 3
    })
    this.setState({
      list: arr
    })
  }

运用es6的扩展运算符,把数组扩展到一个新数组里面,此时并不是之前的引用地址了,而是一个全新引用地址的数组,我们再添加数据后 重新设置list的值就会生效了,可以看到页面也更新重新渲染了。