React系列:react类组件知识的回顾

756 阅读13分钟

在平时的工作中,自己开发一般都是写着react16.8 提供的hooks写法,也是非常的好用。但是呢? 在最近对老的项目新需求开发,是使用的react的类组件的写法,突然感觉对类组件的知识有点模糊了,好久没有使用了(说明自己的基础不是很扎实)。凑着周末的时间,赶紧抓紧时间来学习一波,回顾react的类组件的写法。

react类组件的constructor函数

ES6中的constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。

那么在react类组价中呢? constructor函数到底写不写?如果不写,在内部会自动添加一个constructor函数。那如果写的话,又有哪些注意事项呢?

解答一:在类组件中写不写constructor函数?

这个一般取决于组件要不要定义属于自己的专属state状态,如果需要有专属的state状态,那么就需要写constructor函数(其实这种说法,也不对, 因为针对实例属性,在react内部中自动添加了state属性,那么就可以把state属性单独提出了),简单来说,就是看你自己的兴趣和习惯。

export default class NoState extends Component {
  // 这里没有状态,就不需要使用contructor函数。
  render() {
    return <div>321321</div>
  }
}
export default class HasState extends Component {
  constructor(props) {
      super(props)
      this.state = { // 这里是有状态值
          message: ''
      }
  }
  render() {
    return <div>321321</div>
  }
}

注意事项: 只要写了constructor函数,就一定要写super函数。不写super,this的指向就有问题。

ES5的继承: 实质上是先创造子类的实例对象this,然后再将父类的方法添加到this上面去(Parent.apply(this))

ES6的继承机制:先创造父类的实例对象this,(必须先调用super方法),然后再用子类的构造函数修改this。

在上面的有状态的组件中,因为实例对象中,默认有state,props属性,那么就可以直接把state提出来使用

export default class HasState extends Component {
  state = {
      message: ''
  }
  render() {
    return <div>321321</div>
  }
}

所以,到底要不要写constructor函数,取决于自己的习惯。

解答二:super() 和 super(props)的区别

如果没有写constructor函数,react内部已经添加。如果写了constructor函数,就必须调用super()函数,这样才能继承React.Component类。

所以,在调用super函数的时候到底传不传递 参数props呢?

记住:想在constructor构造函数中,使用this.props,就传递参数;如果不在构造函数中使用this.props,就可以不写。 传递还是不传递,就是看constructor中需要使用不。

export default class Text extends Component {
    constructor() {
      super()
      console.log(this.props) // undefined
    }
}
export default class Text extends Component {
    constructor(props) {
      super(props)
      console.log(this.props) // {}
    }
}

解答三:如果写super(),那么生命周期中,可以拿到props吗 constructor构造函数也相当于一个生命周期,最开始的时候执行。

那么,如果在写super(), 在构造函数中,拿不到this.props中,那么其他的生命周期呢?

super中的props是否接收,只能影响constructor生命周期能否使用this.props,其他的生命周期已默认存在this.props

react类组件中 this的指向

this的指向,在JavaScript中一直是个难点。需要多花点时间去学习理解。 在react中的类组件定义的时候,是使用 ES6 提供的class写法, 构建一个构造函数。

那么class中的方法中的this是指向什么呢? 指向的是构造函数的实例对象

class Person {
  constructor(name) {
    this.name = name
  }
  eat() {
    console.log(this)
  }
}

const p = new Person('james')
p.eat() // this指向p对象

p.eat() 实例对象p调用了eat方法,所以this指向了p,这里应该没有问题。那么下面的这种情况呢?

const p = new Person('james')
const fn = p.eat
fn()

上面的代码,很好理解,通过实例对象p找到eat方法(这里并没有调用,this的指向是在函数调用的时候才确定的)。

简单来说,就中的一个变量 fn 指向了内存中的 一个内存空间。

fn() fn函数执行。如果研究过this的指向问题,这是属于函数的直接调用, this的指向是window,没有什么疑问。

但是,这里eat是 class类中的方法,class类中的方法有一个特点:类中的方法开启了局部的严格模式,严格模式下的this,就是undefined。 所以,在上面代码fn()代码执行,eat方法中打印的this就是undefined。

理解上面的this是指向undefined是非常非常的重要,在下面的react组件中,会充分的体现出来。

下面来看看,react组件

// 简单的一个测试组件
export default class Test extends Component {
  constructor(props) {
    super(props)
    this.state = {
      message: '测试组件'
    }
  }
  changeMessage() {
    console.log(this) // 这里的this是什么呢?
    // 下面的代码执行会报错吗?
    this.setState({
      message: '信息改变'
    })
  }
  render() {
    return (
      <div onClick={this.changeMessage}>{ this.state.message }</div>
    )
  }
}

解惑:上面react组件中的changeMessage方法中的this指向的是什么?

一步一步分析:

constructor中的this,毫无疑问就是 构造函数的实例对象

render中的this, render方法是UI的渲染,在源码中就是通过 new的形式去调用的render,所以render中的this也是指向实例对象。

changeMessage中的this呢?

01_1.png

理解:

第一步:在中创建了一个changeMessage的变量, 在内存中开辟了一块空间,来保存函数体。

第二步:执行 onClick={this.changeMessage}, 这里不就是引用赋值嘛,把changeMessage指向的堆内存中的地址,赋值给onClick,然后执行onClick事件,就执行堆中的函数体。

那么,是不是就很容易确定,this的值为undefined嘛。因为onClick的事件调用属于函数的直接调用,但是呢又由于类中的方法,开启了函数的局部模式,所以this是undefined。

结论: 类组件中的constructorrender函数中的this,是实例对象,其他类中的方法的this是undefined。(我不知道,这样说对不对,当然除了箭头函数)。

react类组件中方法定义

export default class Test extends Component {
  constructor(props) {
    super(props)
    this.state = {
      message: '测试组件'
    }
  }
  changeMessage() {
    console.log(this) // undefined
  }
  render() {
    return (
      <div onClick={this.changeMessage}>{ this.state.message }</div>
    )
  }
}

在上面的changeMessage方法中,this为undefined,那么想要调用 changeMessage方法,使用this,该怎么做呢?

方式一:bind改变this的指向一(构造函数中定义实例方法)

export default class Test extends Component {
  constructor(props) {
    super(props)
    // 实例方法changeMessage
    this.changeMessage = this.changeMessage.bind(this)
  }
  // 原型上的方法
  changeMessage() {
    console.log(this) 
  }
  render() {
    return (
      <div onClick={this.changeMessage}>{ this.state.message }</div>
    )
  }
}

在构造函数中,使用 this.changeMessage = this.changeMessage.bind(this)把原型上的方法,绑定在实例对象上。

那么onClick点击事件changeMessage就是实例对象上的changeMessage(原型链查找规则:如果自身有,就是使用自身的,如果没有就去查找原型链上的方法)

缺点:

  1. 多定义了一个实例方法,写法麻烦
  2. 不能传递参数

方式二:bind改变this的指向二(直接在jsx中改变this)

export default class Test extends Component {
  state = {
      message: 'james'
  }
  changeMessage() {
    console.log(this) 
  }
  render() {
    return (
      <div onClick={this.changeMessage.bind(this)}>{ this.state.message }</div>
    )
  }
}

可以知道render方法中的this,就是指向实例对象。那么就可以直接在jsx中绑定this。onClick={this.changeMessage.bind(this)}

优点

  1. 可以传递参数

方式三:利用箭头函数寻找上层this的规则(jsx中)

export default class Test extends Component {
  state = {
      message: 'james'
  }
  changeMessage() {
    console.log(this) 
  }
  render() {
    return (
      <div onClick={() => this.changeMessage()}>{ this.state.message }</div>
    )
  }
}

onClick={() => this.changeMessage()} 这里的this是指向实例对象, 实例对象上是没有changeMessage的,从而查找原型上,如果有,就使用原型上的changeMessage方法。 changeMessage是一个箭头函数,this在定义的时候,就已经确定,查找最近上层的this指向,就是实例对象。

方式四:利用箭头函数寻找上层this的规则(实验性语法)

export default class Test extends Component {
  state = {
      message: 'james'
  }
  // 直接使用箭头函数来定义方法
  changeMessage = () => {
    console.log(this) 
  }
  render() {
    return (
      <div onClick={this.changeMessage}>{ this.state.message }</div>
    )
  }
}

这里的changeMessage 跟上面的state是一回事了,都是赋值语句。state被赋值了一个对象,changeMessage被赋值了一个箭头函数。 state绑定在实例对象上, 那么changeMessage也绑定在实例对象上。 因为是箭头函数,this就是指向实例对象。

总结方法绑定的位置:

  1. 方式一:实例对象和原型对象都绑定一个相同功能的函数
  2. 方式二:方法绑定在原型上
  3. 方式三:方法绑定在原型上
  4. 方式四:方法绑定在实例对象上

在上面的几种方式中,习惯使用三四种,写法方便一点。

react 类组件中的ref

在类组件中,ref 就是用来获取DOM节点信息的,(功能类似于id一样的效果)

方式一:refs形式(已废弃)

在类组件的实例对象上:refs

context: {}
props: {}
refs: {}
state: {message: '测试组件'}
updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
_reactInternalInstance: {_processChildContext: ƒ}
_reactInternals: FiberNode {tag: 1, key: null, stateNode: Test, elementType: ƒ, type: ƒ, …}
isMounted: (...)
replaceState: (...)

refs就是用来保存定义的ref

export default class Test extends Component<any, any> {
  btn = () => {
    console.log(this.refs) // {btn: Node}
  }
  render() {
    return (
      <button ref="btn" onClick={this.btn}>点击</button>
    )
  }
}

this.refs 就是用来保存 ref 节点的,算了,不说了。 refs这个对象,已经被废弃了(不想了)

方式二:回调形式的ref

// 内联回调函数的形式
export default class Test extends Component<any, any> {
  btnNode: HTMLButtonElement | null;
  constructor(props: any) {
    super(props);
    this.btnNode = null;
  }
  btn = () => {
    console.log(this.btnNode)
  }
  render() {
    return (
      <button ref={ (node) => this.btnNode = node } onClick={this.btn}>点击</button>
    )
  }
}

ref 的属性值,是一个回调函数,在render渲染的时候,会自动执行。

参数node 就是 ref 当前处于的DOM节点,然后利用实例对象的一个属性(btnNode)来保存当前的节点。这样,直接拿取实例对象的一个属性值,就拿到所绑定的节点。

react:关于回调refs的说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

内联回调函数的形式,在更新的过程中会执行两次ref的回调函数。第一次,用来清除上一次绑定的ref,所以传递的值为null; 第二次,就是用来重新保存DOM节点。

那么该如何解决呢?

使用绑定函数的方式,就能解决。

export default class Test extends Component<any, any> {
  btnNode: HTMLButtonElement | null;
  constructor(props: any) {
    super(props);
    this.btnNode = null;
  }
  btn = () => {
    console.log(this.btnNode)
  }
  // 绑定函数
  saveRef = (node: HTMLButtonElement) => {
    this.btnNode = node
  }
  render() {
    return (
      <button ref={ (node) => this.btnNode = node } onClick={this.btn}>点击</button>
    )
  }
}

这样,saveRef方法就始终在实例对象中,重新render的时候,就不会重新ref绑定的函数。那么,ref只有第一次执行render的时候,会执行。

上面就是回调函数ref形式,两种的写法,区别也在这里。

但是大多数情况下,内联函数执行两次是无关紧要的。一般的写法,还是内联函数的形式。

方式三: createRef形式

React 16.3 版本引入的 React.createRef() API。如果react版本高于16.3,就推荐使用createRef,低于的话,就推荐使用回调函数的ref。

export default class Test extends Component<any, any> {
  btnNode: {current: HTMLButtonElement | null}
  constructor(props: any) {
    super(props);
    this.btnNode = React.createRef<HTMLButtonElement>()
  }
  btn = () => {
    // 打印出节点
    console.log(this.btnNode.current)
  }
  render() {
    return (
      <button ref={this.btnNode} onClick={this.btn}>点击</button>
    )
  }
}

createRef创建的容器保存在实例对象的属性上,这样在整个组件都能引用它们。

  • 当 ref 属性用于HTML元素时,构造函数中使用React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。(原生DOM)
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。(class组件
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。(函数组件)

react 类组件中的生命周期(新)

在 react 16.3提出新的生命周期,16.4就完成了。

即将废弃的生命周期:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

类组件的生命周期,分为三个阶段: 挂载阶段更新阶段卸载阶段

挂载阶段;

  1. constructor
  2. getDerivedStateFromProps
  3. render
  4. componentDidMount

上面的几个生命周期,执行顺序,依次排序。

getDerivedStateFromProps

是一个静态函数,需要加上static

语义:props 中获取 state

作用: 为了让props能更新到组件内部的state,在render之前调用,在初始的时候挂载。

参数: 第一个参数:props, 第二个参数:state; 不能通过this来拿取state中的值

返回值: 如果props对state有影响,返回新的state;如果没有影响,就返回null。

export default class Test extends Component<any, any> {
  constructor(props: any) {
    super(props);
  }
  static getDerivedStateFromProps(props: any, state: any) {
    console.log(props, state)
    return null;
  }
  render() {
    return (
      <button>点击</button>
    )
  }
}

更新阶段;

  1. getDerivedStateFromProps
  2. shouldComponentUpdate
  3. render
  4. getSnapshotBeforeUpdate
  5. componentDidUpdate

顺序排序。

shouldComponentUpdate

语义: 是否应该更新组件。

作用: 阻止一些无用的state和props的更改,来使组件 reRender,造成性能浪费。

参数: 第一个参数:nextProps, 第二个参数:nextState

返回值: boolean值。如果为true更新组件,为false则不更新组件

export default class Test extends Component<any, any> {
  constructor(props: any) {
    super(props);
  }
  shouldComponenetUpdate(nextProps: any, nextState: any) {
   // 比较逻辑
    return true;
  }
  render() {
    return (
      <button>点击</button>
    )
  }
}

getSnapshotBeforeUpdate

语义: Snapshot: 快照。 在更新之前,检查一下

作用: 在render函数后,获取一些DOM信息(滚动信息)等等。然后返回值,给componentWillUpdate提示,作出相应的动作。

参数: 第一个参数:prevProps, 第二个参数:prevState

返回值: any。返回任意类型的值,componentDidUpdate根据返回值,更新。

export default class Test extends Component<any, any> {
  constructor(props: any) {
    super(props);
  }
  static getSnapshotBeforeUpdate(prevProps: any, prevState: any) {
    // 表示没有滚动到底部(随便写的)
    if (this.state.height !== prevProps.height) {
      return 'not-bottom'
    }
    return false
  }
  render() {
    return (
      <button>点击</button>
    )
  }
}

componentDidUpdate

语义: 组件更新完成

作用: 组件更新的最后一步(更新完成之后,想要的逻辑操作)

参数: 第一个参数:prevProps, 第二个参数:prevState,第三个参数:getSnapshotBeforeUpdate的返回值

export default class Test extends Component<any, any> {
  constructor(props: any) {
    super(props);
  }
  componentDidUpdate(prevProps: any, prevState: any, snapShot: any) {
    // 举例
    if (snapShot === 'not-bottom') {
      // 一些列的逻辑操作
    }
  }
  render() {
    return (
      <button>点击</button>
    )
  }
}

卸载阶段:

  • componentWillUnmount

总结:

类组件的生命周期,就大概就三个常用的,

  • componentDidMount: 组件挂载完毕
  • componentDidUpdate: 组件更新完毕
  • shouldComponentUpdate: 组件是否要更新(性能优化)

其他的生命周期,知道就行了。

react 类组件中的ts定义

export default class Text extends Component<P, S> {
    
}

P: 对props的限定
S: 对state的限定

收尾总结

这个周末。两个下午的时间都在总结class中的一些知识点(早上的啦,睡懒觉的啦),当然还有很多高级的知识点没有涉及到。我想的是,先了解基础就行了,可以看懂class的写法,就OK了。现在及以后肯定是hooks开发,class写法很少了,当然不排除以后要写。总结的知识点大致就是这样,如果以后有新的知识,就继续往下面下吧,继续更新中。。。