React的类组件

615 阅读5分钟

1. React类组件

// index.js
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            n: 0
        }
    }
    add = () => {
        this.setState(() => {
            return { n: this.state.n + 1 }
        })
    }
    render() {
        return (
            <div>{this.state.n}
                <button onClick={this.add}>+1</button>
            </div>
        )
    }
}

ReactDOM.render(<App />, document.querySelector('#root'))

上面是一个简单的例子,声明了一个App组件,最后将它挂载到root元素上

2. 类组件的写法

class 组件名 extends React.Component{
    constructor(props){
        super(props)
        this.state = { // 定义state
            //...
        }
    }
}

一个组件中有state(组件自身的数据)和props(外部传入组件的数据)

3. state

读取state----this.state

修改state-----this.setState

3.1 setState

将对组件state的更改排入队列,并通知React需要使用更新后的state重新渲染此组件及子组件。

语法:setState(updater,[callback])

参数一为带有形式参数的updater函数(state,props) => stateChangeupdater的返回值会与state进行浅合并,state和props都是最新值

add(){
    this.setState((state,props)=>{
        return {n:state.n+1}
    })
}

参数一除了接收函数外,还可以接受对象类型setState(stateChange[,callBack]),stateChange会将传入的对象浅层合并到新的state中,这种形式的setState()是异步的,并且会在同一周期内会对多个setState进行批处理

add(){
    for (let i = 0; i < 100; i++) {
        this.setState({ n: this.state.n + 1 });
        console.log(this.state.n)//若刚开始n为0,此时,n也为0
    }
}
//最后渲染在页面上的n,显示1,因为每次拿到的n都是0

注意:

  • setState()并不总是更新组件。它会批量延迟更新无论在 React 事件处理程序setState()中调用多少组件,它们都只会在事件结束时产生一次重新渲染,这使得调用setState()后不能通过this.state获取到更新后的值。如果想要得到最新的state,可以使用componentDidUpdate或者setState的回调函数
  • state是对应变化时组件状态的引用。React设计为不可变数据,所以不应该直接被修改,需要使用state和props构建的新对象来表示变化this.setState({n:this.state.n+1})
  • 更新总是按照它们发生的顺序进行浅层合并。所以如果第一次更新是{a: 10},第二次是{b: 20},第三次是{a: 30},渲染状态将是{a: 30, b: 20}。对同一状态键的最新更新

参数二为可选的回调函数,它将在setState完成合并并重新渲染组件后执行。通常,建议使用compnentDisUpdate()来代替此方式

上述例子中的add如果改成传入函数

add() {
    for (let i = 0; i < 100; i++) {
        this.setState((state) => {
            console.log(state.n)
            return { n: state.n + 1 }
        });
    }
}

页面显示100,控制台打印0-99,因为setState函数当接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态中,也就是说state一直都是最新的值,所以推荐在setState中传入函数

4. 绑定事件的写法

construnctor(){...}
add(){
    this.setState({n:this.state.n+1})
}
render() {
    return (
        <div>{this.state.n}
            <button onClick={this.add}>+1</button>
        </div>
    )
}

上面代码会报如下错误

eventError.png

报错的原因是因为<button onClick={this.add}>+1</button>this.addthis指向window

React在执行上面这句代码时,相当于button.onClick.call(null,event),则this.add函数中的this为null,则在浏览器中就是windowthis.add中的有使用this获取state,那么就会报错

如何让add中的this指向这个类呢?可以使用如下写法

  • 第一种,通过bind指定this的值,this.add中的this指向类,此时bindadd函数,所以add函数中的this也是指向类
<button onClick={this.add.bind(this)}>+1</button>
  • 第二种,因为箭头函数不支持this,此时this指的是外层的this,即指向类,所以可以使用
<button onClick={()=>this.add()}>+1</button>
  • 第三种,写成箭头函数,作为实例属性
constructor(){
    this.state = {
        n:0
    }
    this.add=()=>{
        this.setState({n:this.state.n+1})
    }
}
<button onClick={this.add}>+1</button>
  • 第四种,最终写法,与上面第三种写法是等价的
construnctor(){...}
add=()=>{this.setState({n:this.state.n+1})}
<button onClick={this.add}>+1</button>

写法的区别,前面两种写法的add是定义在constructor外,也就是绑定在类的原型上,被所有实例共享后面两种写法add定义在constructor内,绑定在实例上,不同的实例都有属于各自的add属性

5. props

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

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            n: 0,
            message: '传给子组件的消息'
        }

    }
    add = () => { this.setState({ n: this.state.n + 1 }) }
    render() {
        return (
            <div>{this.state.n}
                <button onClick={this.add}>+1</button>
                <Child message={this.state.message} n={this.state.n} addClick={this.add}>hello</Child>
            </div>
        )
    }
}
class Child extends React.Component {
    constructor(props) {
        super(props)// 初始化props,this.props就指向外部数据对象的地址
        console.log(props)
    }
    render() {
        return (
            <div>
            	{this.props.message}
                <div>{this.props.children}</div>
                {this.props.n}
                <button onClick={this.props.addClick}>+1</button>
            </div>
        )
    }
}
ReactDOM.render(<App />, document.querySelector('#root'))

<Child message={this.state.message} n={this.state.n} addClick={this.add}>hello</Child>上述代码会以{message: '传给子组件的消息', n: 0, children: 'hello', addClick: ƒ}形式传给子组件

super(props) 初始化propsthis.props就指向外部数据对象的地址

修改props,不可以修改props,通知传入数据的组件,自己修改

6. 生命周期

6.1 constructor()

初始化props,state

6.2 shouldComponentUpdate()

return false 表示数据没变,不需要重新生成虚拟DOM,从而不会更新UI

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            n: 0
        }
    }
    add = () => {
        this.setState((state) => ({
            n: state.n + 1
        }))
        this.setState((state) => ({
            n: state.n - 1
        }))
    }
    render() {
        console.log('触发了')
        return (
            <div>
                {this.state.n}
                <button onClick={this.add}>+1</button>
            </div>
        )
    }
}

上述代码中,由于先加一再减一,页面上的n是没有改变值的,但是每点一次,render就会被调用一次

  • 为什么不是两次,因为setState批量处理更新

  • 为什么state最终没变,还是会触发更新,因为{n:1}{n:1},是两个对象,地址不同。

  • React通过Diff算法,比较两次虚拟DOM,发现没有任何变化,所以DOM不会更新

既然没有更新,如何控制不触发render?使用shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
        if (nextState.n === this.state.n) {
            return false
        } else {
            return true
        }
    }

如果有多个n这样的元素,想要实现,值变化了才触发render,难道每一个都要手动设置吗?不需要,使用PureComponent就好,如下例所示,render中的输出,只有首次渲染时才会打印出


class App extends React.PureComponent {
    constructor(props) {
        super(props)
        this.state = {
            n: 0
        }

    }
    add = () => {
        this.setState((state) => ({
            n: state.n + 1
        }))
        this.setState((state) => ({
            n: state.n - 1
        }))
    }
    render() {
        console.log('触发了')
        return (
            <div>
                {this.state.n}
                <button onClick={this.add}>+1</button>
            </div>
        )
    }
}

PureComponent会在render之前将新旧state、props进行浅对比,只会对比一层,所以对于简单变量会比较高效,对于数组、对象等引用类型,只有内存地址不同时才会渲染

class App extends React.PureComponent {
    constructor(props) {
        super(props)
        this.state = {
            colorArr: ['red', 'yellow', 'blue']
        }
    }
    delete = () => {
        this.setState((state) => {
            state.colorArr.pop()
            return { colorArr: state.colorArr }
        })
    }
    /* 使用下面的detele,就会触发render,数组对应的地址已经改变
    delete = () => {
        let newArr = [...this.state.colorArr]
        newArr.pop()
        this.setState(() => ({ colorArr: newArr }))
    }
    */
    render() {
        console.log('触发了')
        let { colorArr } = this.state
        return (
            <div>
                <ul>
                    {colorArr.map((item, index) => {
                        return <li key={index}>{item}</li>
                    })}

                </ul>
                <button onClick={this.delete}>删除</button>
            </div>
        )
    }
}

上述代码中,点击删除,不会触发render,页面也不会重新渲染,因为PureComponent只进行浅比较,colorArr地址没变,不会触发render

当以下几种情况推荐使用PureComponent

  • state/props是不可变的对象
  • state/props是普通类型变量
  • 当时据改变时,使用forceUpdate

使用PureComponent,不推荐再使用shouldComponentUpdate

6.3 render() 创建虚拟DOM

只有一个根元素,

也可以使用虚拟标签

<React.Fragment>
    ...//包含多个统计标签
 </React.Fragment>

简写<></>

onClick = ()=>{
    this.setState((state)=>({
        n:state.n+1
    }))
}
render(){
    let message
    if(this.state.n%2===0){
        message = <span>偶数</span>
    }else{
        message = <span>奇数</span>
    }
    return (
        <>
        {message}
        <button onClick={this.onClick}>+1</button>
        </>
    )
}

6.4 componentDidMount()

当组件已经渲染到DOM中执行此方法,首次渲染会执行此方法

只有当虚拟DOM变成真实DOM后,我们才能获取到,如果有依赖DOM操作的代码可以放在此处

官方推荐在此时发起AJAX请求,加载数据

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            width: undefined
        }
        this.divRef = React.createRef() // 创建ref,可以通过此属性获取React元素
    }
    componentDidMount() {
        // const div = document.querySelector('#box')
        const div = this.divRef.current //获取当前值
        const { width } = div.getBoundingClientRect()
        console.log(width)
        this.setState(() => ({ width: width }))
    }
    render() {
        return (
            <div id="box" ref={this.divRef}>div的宽度{this.state.width}</div>
        )
    }
}

6.5 componentDidUpdate(prevProps,prevState,snapshot)

组件更新完成后触发,首次渲染不会触发,数据有更新时才会

可以在此处通过AJAX发起请求,更新数据

一般不推荐在此处调用setState,因为数据更新后又会调用此钩子函数,会进入循环,除非在if条件下使用,比如满足某些条件时更新数据

6.6 componentWillUnmount()

组件即将移除时执行

unmount过的组件不会再次被mount

6.7 其他钩子函数

还有以下生命周期函数,比较少用

static getDerivedStateFromProps()

getSnapshotBeforeUpdate()

static getDerivedStateFromError()

componentDidCatch()

了解详情可见官网介绍