Class 组件详解

大纲链接 §

[toc]


Class 组件虽然是已淘汰的繁杂用法,没有函数组件那样的简洁清晰的语法,“能效比”低,新项目基本不用,只是使用 Class 组件必须有更扎实的 JS 功底,洞悉this的原理,好比汉字的「识繁书简」

1. 英语小课堂

英语 翻译
derived 导出的,衍生的,派生的
render 渲染
super class 超类、父类
property 属性
state 状态
mount 挂载

2. 两种创建 class 组件的方式

过时的ES5方式

1
2
3
4
5
6
7
8
9
import React from 'react'
const A = React.createClass({
  render() {
    return (
      <div>hi</div>
    )
  }
})
export default A
  • 由于 ES5 不支持 class,才会有这种方式

ES6方式(也过时了,现在用函数组件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import React from 'react'
class B extends React.Component {
  constructor(props) {super(props)}
  render() {
    return (
      <div>hi</div>
    )
  }
}
export default B
  • 关键词 extendsconstructorsuper
  • 构造方法中如果不用添加额外代码的话,可直接删除constructor(props) {super(props)}

哪种好

  • ES6相较来说更好,老项目中只用这种方式创建类组件

浏览器不支持 ES6 怎么办

  • 公司还在搞IE8,肯定是个破公司,跳槽吧
  • webpack + babelES6转译成ES5即可

3. 回顾props

父组件的state传给 子组件作为其 props

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'xxx'}
  }
  clickHandler = () => {}
  render() {
    return (
      <B name={this.state.name}
         clickHandler={this.clickHandler}> hi </B>
    )
  }
}
  • 外部数据 props 被包装为一个对象
  • {name: 'xxx', clickHandler: ..., children: 'hi'}
  • 此处 clickHandler 是一个回调

子组件初始化

1
2
3
4
5
6
class B extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {}
}
  • 要么自动初始化,即不写constructor方法,自动继承
  • 要么手动初始化,且必须写全套(不写super直接报错)
  • 效果是将props放到实例的 this

效果

  • 这么做了之后,this.props就是外部数据 对象的地址

子组件读取props

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class B extends React.Component {
  constructor(props) {super(props)}
  render() {
    return (
      <div onClick={this.props.clickHandler}>
        {this.props.name}
        <div>
          {this.props.children}
        </div>
      </div>
    )
  }
}
  • 通过this.props.xxx读取

原则:不准对组件的 props 进行写入

尝试改写 props 的值(一个对象的地址)

  • this.props = { /* 另一个对象 */ }
  • 这样破坏了单向数据流的原则
  • 外部数据,应该由外部更新

尝试更改 props 的属性

  • this.props.xxx = 'hi'
  • 这样同样破坏了单向数据流的原则
  • 外部数据,不应该从内部改值

原则

  • 应该由数据的 所有者 对数据进行更改
  • 原则上可以用一个内部数据的属性 来承接,修改这个内部数据的属性

props相关的钩子

componentWillReceiveProps 钩子

  • 当组件接受新的 props 时,会触发此钩子
  • 钩子函数 即 「特殊实际调用的函数」
  • 示例

该钩子已经被弃用

  • 更名为 UNSAFE_componentWillReceiveProps
  • 旧项目中 16.9- 可能会看到

props的作用

接受外部数据

  • 只能读,不能写
  • 外部数据由父组件传递,由父组件来维护

接受外部函数

  • 在恰当的时机,调用该函数
  • 该函数一般是父组件的函数

4. 回顾state

初始化 state

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class B extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      user: {name: 'xxx', age: 18}
    }
  }

  render() {/*...*/}
}

读写 state

读数据用 this.state

  • this.state.xxx.yyy.zzz

写数据用 this.setState(???, fn)

  • this.setState(newState, fn)
  • 注意 setState 不会立即改变 this.state
    • 而是会在当前代码运行完后,再去更新 this.state,从而触发 UI 更新
  • 推荐总是使用 this.setState((state, props) => newState, fn)
    • 这种方式的 state 反而更易于理解
    • fn 会在写入成功后执行,是成功后的回调
    • 注意对象格式 this.setState((state, props) => ({xxx: state.xxx + 1 }), fn)

有时会 shallow merge 浅合并(即合并对象的第一层属性)

  • setState 会自动将新 state 与旧state进行一级合并

一种错误地做法:直接对 this.state的属性 进行再次赋值 来修改属性

  • this.state.n += 1
  • this.setState(this.state)
  • 虽然可以起效,但不推荐,因为这样违反了数据不靠边的原则

5. 什么是生命周期

类比如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const app = document.getElementById('app')

// create
const div = document.createElement('div')
div.style.border = '1px solid red'

let state = 0
div.innerHTML =  `
  <p>${state}</p>
  <button>+1</button>
  <button>die</button>
`

// mount
app.appendChild(div)

div.querySelector('button').onclick = () => {
  // update
  state += 1
  div.querySelector('p').innerText = state
}

div.querySelectorAll('button')[1].onclick = () => {
  // destroy
  div.querySelector('button').onclick = null
  div.querySelectorAll('button')[1].onclick = null
  div.remove()
  div = null
}
  • 这是create/constructor创建/构建的过程const div = document.createElement('div')
  • 这是初始化state div.textContent = 'hi'
  • 这是div挂载mount的过程app.appendChild(div)
  • 这是divupdate的过程div.textContent = 'hi2'
  • 这是divunmount的过程div.remove()

同理React组件也有这些过程,称之为组件的生命周期,示例代码 reactLifecycle.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
import React from 'react'
import ReactDOM from 'react-dom'

import './style.css'

// 模拟控制台
const div = document.createElement('div')
document.body.appendChild(div)
console.log = (content) => {
    div.innerHTML += `${content}<br>`
}

class App extends React.Component {
    // 创建
    constructor() {
        super()
        console.log('创建App')
        // 初始化
        this.state = {
            n: 0
        }
    }

    // methods
    onClick() {
        console.log('用户点击了')
        this.setState({
            n: this.state.n + 1
        })
    }

    // 将要 mount App
    UNSAFE_componentWillMount() {
        console.log('将将挂载App')
    }

    // 监听内容变化 更新渲染 // update
    render() {
        console.log('填充 App的内容')
          return (
            <div className="App">
                {this.state.n}
                <button onClick={() => this.onClick()}>
                  +1
                </button>
                <br/>
                收到 {this.props.word}
            </div>);
    }

    // mount App
    componentDidMount() {
        console.log('挂载App完毕')
    }

    // 更新前 再次render前
    UNSAFE_componentWillUpdate() {
        console.log('update App 之前')
    }

    // 更新前 再次render后
    componentDidUpdate() {
        console.log('update App 之后')
    }

    // 销毁前
    componentWillUnmount() {
        console.log('App 销毁前')
    }

    // 接受父组件传值
    UNSAFE_componentWillReceiveProps() {
        console.log('接受父组件传值前')
    }
}

// 销毁
class AppPaBa extends React.Component {
    constructor() {
        super()
        this.state = {
            hasChild: true
        }
    }
    onClick() {
        this.setState({
            hasChild: false
        })
    }
    callSon() {
     this.setState({
         word: 'OK'
     })
    }
    render() {
        return (
          <div>
            BaPa
            <button onClick={() => this.onClick()}> Destroy</button>
            <button onClick={() => this.callSon()}> Call</button>
            {this.state.hsChild? <App word={this.state.word} /> : null}
          </div>
        )
    }
}

const roorElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement)

5.0 生命周期钩子函数列表

  • constructor() 创建时调用,初始化state
  • static getDerivedStateFromProps() 可忽略
  • shouldComponentUpdate() 手动判断是否更新
    • return false 时阻止更新
  • render() 渲染,创建虚拟DOM
  • getSnapshotBeforeUpdate() 可忽略
  • componentDidMount() 挂载后调用,组件已出现在页面
  • componentDidUpdate() 更新后调用,组件已更新
  • componentWillUnmount() 即将卸载时调用,组件将销毁
  • static getDerivedStateFromError() 可忽略
  • componentDidCatch() 可忽略

5.1 生命周期之 constructor

用途

  • 初始化 propsconstructor(props) {super(props)}
  • 初始化 state,但此时不能调用 setState 方法
    • 只能使用 this.state = {/*...*/}
  • 用来写 bind this,也可不写,自动继承
1
2
3
4
5
6
7
8
constructor() {
  /* 其他代码略 */
  this.onClick = this.onClick.bind(this) // 实例对象自身属性
}

// 等价于新语法 在实例对象自身上添加方法
onClick = () => {}
constructor() {/* 其他代码略 */} // 无初始化state时,可不写

5.2 生命周期之 shouldComponentUpdate

用途

面试常问

  • shouldComponentUpdate 有什么用
  • 它允许手动判断是否要进行组件更新
    • 在其中添加自己的业务逻辑,可以根据不同应用场景,灵活地设置返回值,以避免不必要的更新

示例 +1 后立马 -1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {n: 1}
  }
  onClick = () => {
    this.setState( state => ({n: state.n + 1}) )
    this.setState( state => ({n: state.n - 1}) )
    // {n: 1} 和 {n: 1} 不是同一个对象
  }
  shouldComponentUpdate(newProps, newState) {
    ;newState.n === this.state.n
      ? false
      : true
  }
  render() {
    console.log('render了一次')
    return (
      <div>
        {this.state.n}
        <button onClick={this.onClick}> +1 后立马 -1</button>
      </div>
    )
  }
}
  • 首次就会render一次
  • 只要数据一有改动,即使最终值相同,也会执行render
    • {n: 1}{n: 1} 不是同一个对象
    • 但根据DOM diff不会实际去更新页面

启发

  • 其实可以将newStatethis.state的每个属性都对比一下
    • 如果全等,就不更新组件
    • 如果有一个不等,就更新

再启发

  • React内置了这项功能,只不过另外取名叫React.PuerComponent,来代替React.Component,他可以替换绝大多数情况下的shouldComponentUpdate钩子判断

质疑

  • 为什么要用新的对象
  • 为什么不直接在this.state上修改属性?
  • 这涉及到函数式编程,暂略

参考 性能优化 如何在函数组件中实现 shouldComponentUpdate


关于 React.PureComponent

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {n: 1}
  }
  onClick = () => {
    this.setState( state => ({n: state.n + 1}) )
    this.setState( state => ({n: state.n - 1}) )
    // {n: 1} 和 {n: 1} 不是同一个对象
  }
  render() {
    console.log('render了一次')
    return (
      <div>
        {this.state.n}
        <button onClick={this.onClick}> +1 后立马 -1</button>
      </div>
    )
  }
}
  • PureComponent 会在 render 之前
    • 对比新 state 和旧 state 的每一个 key
    • 对比新 props 和旧 props 的每一个 key
    • 作浅对比,值对比数据对象的第一层属性
  • 如果所有 key 的值全都一样,就不会 render
  • 如果有任何一个 key 的值不同,就会 render

参考


5.3 生命周期之 render

用途

  • 展示视图 return (<>...</>) 里面是虚拟DOM
  • render() 方法是 class 组件中唯一必须实现的方法
  • 只能有一个根元素
    • 如果有两个根元素,就要用<React.Fragment></React.Fragment>文档片段包起来
    • <React.Fragment>一般缩写成<></>

打印查看虚拟DOM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {n: 1}
  }
  onClick = () => {
    this.setState( state => ({n: state.n + 1}) )
    this.setState( state => ({n: state.n - 1}) )
  }
  render() {
    const x = (
      <>
        {this.state.n}
        <button onClick={this.onClick}> +1 后立马 -1</button>
      </>
    )
    console.log('x:', x)
    return x
  }
}

技巧

  • render里可以写条件分支表达式
    • 可以写 if...else
    • 可以写 ?...:... 三元表达式
    • 可以写 && 等短路判断
  • render里不能直接写for循环(return结束循环),需要用数组作中间变量
  • render里可以写array.map(循环渲染)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {n: 1}
  }
  onClick = () => {
    this.setState( state => ({n: state.n + 1}) )
  }
  render() {
    let message
    if(this.state.n % 2 === 0) {
      message = <div>偶数</div>
    } else {
      message = <span>奇数</span>
    }
    return (
      <>
        {message}
        <button onClick={this.onClick}> +1 </button>
      </>
    )
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {n: 1}
  }
  onClick = () => {
    this.setState( state => ({n: state.n + 1}) )
  }
  render() {
    return (
      <>
        {this.state.n % 2 === 0 ? 偶数 : 奇数}
        <button onClick={this.onClick}> +1 </button>
      </>
    )
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {arr: [1, 2, 3]}
  }
  render() {
    let result = []
    for(let i = 0; i < this.state.arr.length; i++) {
      result.push(this.state.arr[i])
    }
    return ( <div> {result} </div> )
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class App extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {arr: [1, 2, 3]}
  }
  render() {
    return this.state.arr.map( (n) => {
      <div key={n}>{n}</div>
    })
  }
}

参考


5.4 生命周期之 componentDidMount

用途

  • 在元素插入页面之后执行逻辑,这些代码依赖DOM
    • 例如获取元素高度,前提是元素必须出现在页面中
    • Element.getBoundingClientRect()
    • 不推荐使用id来获取元素
    • 推荐使用ref来获取元素,不会出现id冲突的问题
  • 也可以发起 加载数据AJAX请求(官方推荐)
  • 首次渲染 即会 执行这个钩子函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class App extends React.Component {
  divRedf = undefind
  constructor(props) {
    super(props)
    this.state = {width1: undefind, width2: undefind}
    this.divRef = React.createRef()
  }
  componentDidMount() {
    // id
    const div = document.getElementById('xxx')
    const {width: width1} = div.getBoundingClientRect()
    this.setState({width1})

    // ref
    const div2 = this.divRef.current
    const {width: width2} = div2.getBoundingClientRect()
    this.setState({width2})
  }
  render() {
    return (
      <>
        <div ref={this.divRef}>Hello {this.state.width2}</div>
        <div id="xxx">Hello {this.state.width1}</div>
      </>
    )
  }
}

参考


5.5 生命周期之 componentDidUpdate

用途

  • 在视图更新后执行逻辑
  • 此处也可以发起 加载数据AJAX请求,用于 更新数据 的请求,见文档
    • 区别于 componentDidMount加载数据 的请求
  • 首次渲染 不会 执行这个钩子函数
  • 注意
    • 在此处setState可能会引起 无限循环,两者互相调用,除非放在if判断中
    • shouldComponentUpdate返回false,则不会触发此钩子
1
componentDidUpdate(prevProps, prevState, snapShot) {}

参考


5.6 生命周期之 componentWillUnmount

用途

  • 组件将要被 移除出页面(DOM),然后被销毁(内存) 时执行逻辑
  • 执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等
  • unmount 过的组件不会再次 mount,即组件实例卸载后,将永远不会再挂载它
    • componentWillUnmount() 中不应调用 setState()
    • 因为该组件将永远不会重新渲染

原则:谁污染谁治理(出于性能和用户体验考虑)防止内存泄漏

  • ComponentDidMount里监听了window.scroll
    • 那么就要在ComponentWillUnmount里取消监听
  • ComponentDidMount里创建了timersetTimeout
    • 那么就要在ComponentWillUnmount里清除timerclearTimeout
  • ComponentDidMount里创建了AJAX请求

参考


生命周期函数小结

分阶段查看钩子执行顺序

reactCompLifeCyc

生命周期钩子函数回顾,分为三个阶段

  • 首次渲染
    • constructor() 初始化数据
    • render() 创建虚拟 DOM
    • 更新UI
    • componentDidMount() 组件已出现在页面
  • 再次渲染
    • props变了、setState变了或者forceUpdate触发再次渲染
    • shouldComponentUpdate() 或阻止组件更新
      • 该更新时不要忘记return true否则不会触发更新
    • render() 创建虚拟 DOM
    • 更新UI
    • componentDidUpdate() 组件已更新
  • 销毁
    • componentWillUnmopunt() 组件将死

7. 总结

平时工作中以下推荐顺序写组件

  • 函数组件 > 类组件 > React.PureComponent > React.Component
  • props只读;state可读可写
  • 类组件中 this.setState((state, props) => newState, fn) > this.setState(newState, fn)
组件类型\负担对比 心智负担 手指负担 代码量 代码组合方式
函数组件 较轻 较轻 较少 useXxx钩子函数Hooks API
类组件 较重 较重 较多 类继承、setState、生命周期API

8. 课后习题 测试 Class 组件

  • React Class 组件的 constructor 在某些情况下可以省略
  • 如果有 constructor,则 constructor 中的 super(props) 必须写
  • <B name="xxx" onClick={this.onClick}>hi</B> 组件接收到的 props 的值是 {name:"xxx", onClick:this.onClick, children: 'hi'}
  • setStateshallow mergeReact 只会检查新 state 和旧 state 第一层的区别,并把新 state 缺少的数据从旧 state 里拷贝过来
  • React 弃用的生命周期有
    • componentWillMount()
    • componentWillUpdate()
    • componentWillReceiveProps()
    • 提示,前面加了 UNSAFE_ 前缀的生命周期,都是被弃用的
    • 可以在 React 文档的某个页面里通过 Ctrl + F 找到
    • 搜google


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名