React类组件详解

682 阅读8分钟

一、创建一个类组件

import React from 'react';

class B extends React.Component {
    constructor(props) {
       super(props);
    }
    render() {
       return (
         <div>hi</div>
      )
    }
}
export default B;
//extends, constructor, super强行记忆,别问原因

不需要额外代码时,

constructor(props) {
       super(props);
    }

可以删掉

浏览器不支持ES6的这种写法怎么办:用webpack+babel将ES6翻译成ES5即可。

二、Props-外部数据/属性

定义

  • props 是组件对外的数据接口

作用

  • 接受外部数据

    • 只能读取不能修改写入
    • 外部数据由父组件传递
  • 接受外部函数

    • 在子组件调用外部函数
    • 该函数一般为父组件的函数

代码实例

  1. 初始化 props
// construtor和super可以被省略不写,或者必须写全套
class B extends React.Component {
   constructor(props) {
      super(props);
    }
    render(){}
}

通过初始化props, this.props 就是外部数据对象的地址 注意:constructor 初始化函数里边,如果还需要写其他东西,比如 this.state 等,那就必须把这三行写全,必须把 props 参数写在那。

  1. 传入 props 给B组件
class Parent extends React.Component {
   constructor(props){
   super(props)
   this.state = {name:'frank'}//外部数据一般都是来自于父元素的state
}
onClick = ()=>{}
   render(){
      return <B name={this.state.name} onClick={this.onClick}>hi</B>
      //里面的name(来源:state)和onClick(来源:onClick = ()=>{})就是props
      //此处的onClick是一个回调
}

// parent组件传入`props`给`B`组件, 外部数据被包装成为一个对象

传进去的 props 会被包装成一个对象,{name:'frank',onClick:...,children:'hi'}

  1. 读取 props(this.props.xxx)
class B extends React.Component {
    constructor(props) {
    super(props);
  }
    render(){
      return 
      <div onClick={this.props.onClick}>
         {this.props.name}
         <div>
             {this.props.children}
         </div>
     </div>
   }
}

// `B`组件通过`this.props.xxx`读取`props`

注意:子组件禁止修改写入props

理由:

  • 修改 props 的值,即修改外部数据的地址。例如 this.props = {/*另外一个对象*/} 。既然是外部数据,就应该由外部更新
  • 修改 props 的属性,例如 this.props.xxx ='hi 。既然是外部数据,就不应该从内部组件来修改值
  • 外部数据就由外部数据的主人,即父组件对其进行修改
  1. 相关钩子componentWillReceiveProps
  • 当组件接受新的 props 时,会触发特殊的函数,即钩子 hooks
  • 现在更名为 UNSAFE_componentWillReceiveProps ,但是已经被弃用。不推荐使用该钩子

三、State&setState-内部数据

state 是组件对内的数据接口

  1. 初始化 state
 class B extends React.Component {
    constructor(props) {
    super(props);
     this.state = {
       user: {name:'frank', age:18}
    }
  }
    render() { /* ... */ }
}
  1. 读取 state
this.state.user
  1. 修改/写 statethis.setState(newState, fn)
this.setState({x: this.state.x+1}) // 或者
this.setState((state)=>({x:state.x+1})) // 推荐这种方法

// setState是异步操作,不会立刻改变`this.state`,会等同步任务执行完,再去更新this.state,从而触发UI更新
// show merge 会将新的state和旧的state进行合并
this.setState((state,props)=> newState,fn)
// 也推荐使用这种方式,更好理解,回调函数fn会在写入成功后执行

注意:React 只会检查新 state 和旧 state 第一层的区别,并把新 state 缺少的数据从旧 state 里拷贝过来

四、生命周期

image.png

1. constructor

用途:

  • 初始化 props
  • 初始化 state但此时不能调用setState
  • 用来写 bind this
constructor(){
    /*其他代码略*/
    this.onClick = this.onClick.bind(this)
}

//可以用新语法代替

    onClick = ()=> {}
    constructor(){ /* */ }
  • 如果只需要初始化props,可以不写 constructor

React组件在创建的时候,会调用这个函数

2. shouldComponentUpdate

用途:

  • 返回false,表示阻止UI更新
  • 返回true,表示不阻止UI更新

示例

import React from 'react';

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
    }));
  }

  render() {
    console.log('render了一次')
    return (
      <div>
        App
        <div>
          {this.state.n}
          <button onClick={this.onClick}>+1</button>
        </div>
      </div>
    )
  }
}

export default App;

假设有这样一种情况:点击按钮后,对n做了一系列操作,最后发现n的值没变,还是1。(用先+1,再-1模拟)

点击按钮,发现页面上的n没变,还是1.这是没问题的。因为n的值没变。

但是,log打印了两次。为什么n的值没变,还是调用了render呢?

虽然n的值前后都是1,但是 {n:1}{n:1} 是两个不同的对象,地址不同。所以React认为数据变了,就去调用了 render 函数。然后生成了新的虚拟DOM。对比前后两个虚拟DOM,发现n的值都是1,没有不同的地方,就没有更新UI。但是 render 确是被调用了

所以其实从调用 render 往后,生成新DOM,对比,发现没有不同,停止更新,这几步都是多余的。是数据的地址变了,让React误以为数据变了,但是最后又发现没变,所以中间的那些步骤就多余了。

能不能在n的值没变的情况下,就不让 render 被调用了呢?shouldComponentUpdate 钩子可以做到

import React from 'react';

class App extends React.Component {
  ... ...
  }
  
  shouldComponentUpdate(newProps, newState) {
    if (newState.n === this.state.n) {
      return false
    } else {
      return true
    }
  }
  
  render() {
    console.log('render了一次')
    ... ...
}

export default App;

在这个函数里判断:如果新的n和当前的n一样,就返回false,不用更新了。(手动告诉React,数据没变,不要去render了)这时再点击按钮,就没有log了,说明render没有被调用了。

面试常问:shouldComponentUpdate有什么用? 答:它允许我们手动判断是否要进行组件更新。我们可以根据应用场景灵活地设置返回值,以避免不必要的更新。

React.PureComponent 代替 React.Component

上边的 shouldComponentUpdate 钩子还要自己手动判断,有点麻烦,于是React内置了这个功能 ,叫做 React.PureComponent 。在 class extends 后边继承它,就相当于 should 的功能 。

PureComponent 会在 render 之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key。 如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render。(只对比最外面一层)

如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render

class App extends React.PureComponent {}

3. render

用途:

  • 创建虚拟DOM,展示视图 return (<div>...</div>)
  • return 的元素,只能有一个根元素。
  • 如果有两个根元素,要么用一个<div>包起来,要么用<React.Fragment></React.Fragment>包起来, 可以缩写为 <></>
  • 他们的区别是:<div>会被渲染到页面中,而<React.Fragment></React.Fragment>不会被渲染。

render里可以写任何JS代码,

  • if...else...
class App extends React.PureComponent {
    constructor(props){
        super(props)
        this.state={
            n:1,
            array:[1,2,3]
        }
    }
render() {
  if (this.state.n % 2 === 0) {
    return <div>偶数</div>
  } else {
    return <span>奇数</span>
  }
}
  • ? :
class App extends React.PureComponent {
    ... ...
    }
render() {
  return (
    <>
      {this.state.n % 2 === 0 ?
        <div>偶数</div>:
        <span>奇数</span>}
      <button onClick={ this.onClick}>+1</button>
    </>
  )
}
  • &&
class App extends React.PureComponent {
    ... ...
    }
render() {
  return (
    <>
      {this.state.n % 2 === 0 && <div>偶数</div>}
      <button onClick={ this.onClick}>+1</button>
    </>
  )
}
  • 但是render里不能直接写 for循环 ,因为render是需要 return 的,如果在 for循环 里写 return ,就只能循环第一次,就直接退出循环了。

    可以借助数组,循环的时候,把结果push进数组,循环结束后,return 数组。

class App extends React.PureComponent {
    ... ...
    }
render() {
  let result=[]
  for (let i = 0; i < this.state.array.length; i++) {
    result.push(this.state.array[i])
  }
  return result
}
  • 或者使用array.map()实现循环
class App extends React.PureComponent {
    ... ...
    }
    render(){
        return (
            this.state.array.map(item=><div key={item}>{item}</div>)
        );
    }
}

注意:所有循环都需要一个不重复的key

4. componentDidMount

用途:

  • 在元素插入页面后执行代码,这些代码依赖DOM
  • 此处可以发起加载数据的AJAX请求(官方推荐)
  • 首次渲染执行此钩子

具体解释:

componentDidMount() 会在组件挂载后(插入 DOM 树中,出现在页面)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方(发起加载数据的AJAX请求)。当然,首次渲染会执行这个钩子

应用场景:在页面渲染一个div,同时展示这个div的宽度。

解析:要展示一个div的宽度,前提是它已经出现在页面里了,才能去获取。如果在 constructor 里获取,肯定是拿不到的,因为 constructor 是在元素出现在内存后被调用的,此时还没被挂载到页面里。所以需要使用该钩子,在组件挂载后,去获取div的宽度

class App extends React.PureComponent {
    constructor(props){
        super(props)
        this.state={
            n:1,
            width:undefined
        }
    }
    componentDidMount(){
        const div=document.querySelector('#xxx')
        const width=div.getBoundingClientRect().width
        //析构:const {width}=div.getBoundingClientRect()
        this.setState({width:width})
        //析构:this.setState({width})
    }
    render(){
        return (
            <div id="xxx">
                hello world {this.state.width}px
            </div>
        );
    }
}

访问页面里的DOM节点的第二种方式-Refs

上边我们通过id访问DOM节点。React提供了一种访问页面里的DOM节点的方式-Refs

class App extends React.PureComponent {
    divRef=undefined// 可以先声明一下,表示我之后要动态创建Ref
    constructor(props){
        super(props)
        this.state={
            width:undefined
        }
        this.divRef=React.createRef()   // 1. 创建Refs
    }
    componentDidMount(){
        const div=this.divRef.current   // 3. current属性访问节点
        const width=div.getBoundingClientRect().width
        this.setState({width:width})
    }
    render(){
        return (
            <div ref={this.divRef}>     // 2. 通过ref属性,关联到元素上
                hello world {this.state.width}px
            </div>
        );
    }
}

好处是不会出现id冲突的问题。因为创建的时候,divRef是绑定到this上,也就是实例上的,而不是state上。

5.componentDidUpdate(prevProps, prevState, snapshot)

用途:

  • 在视图更新后执行代码
  • 首次渲染不会执行此钩子(因为没更新)
  • 在此处 setState 可能会引起无限循环,除非放在if里
  • shouldComponentUpdate 返回false,则不触发此钩子
  • 如果要发起AJAX请求,除了可以在 componentDidAmount 里请求加载数据,还可以在这个钩子里也可以发起AJAX请求,不过是用于更新数据
    • 官方文档: 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,userid改变,需要获取新的用户信息)。
componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

就是说,假如用户的id变了,那就需要发起一个网络请求获取新的用户信息,这时网络请求就要在componentDidUpdate里做,因为视图更新了才知道用户信息变了。

6. componentWillUnmount()

用途:

  • 组件将要被移出页面(取消挂载)然后销毁(从内存消失)时执行
  • unmount过的组件不会再次mount

例如:

  • componentDidMount 里面监听了window scroll
    • 那么就要在 componentWillUnmount() 取消监听
  • componentDidMount 里面创建了Timer
    • 那么就要在 componentWillUnmount() 取消Timer
  • componentDidMount 里面创建了AJAX请求
    • 那么就要在 componentWillUnmount() 取消请求
  • 原则:谁污染谁治理

总结

分阶段看一下这些钩子会在什么时候执行,以及他们的执行顺序: image.png

  • constructor() 初始化state和props,创建元素
  • shouldComponentUpdate() return false阻止更新,不要忘了return true,否则会变成undefined
  • render() 渲染、创建虚拟DOM
  • componentDidMount() 组件已出现在页面
  • componentDidUpdate() 组件已更新
  • componentWillUnmount() 组件将死