第2章:React面向组件编程

347 阅读42分钟

2.1 基本理解和使用

2.1.1 函数式编程

概要总结

1、创建函数式组件

2、函数式组件运行原理

一、创建函数式组建

<script type="text/babel">
  // 1.创建函数式组件
  function demo() {
    return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
  }
</script>

二、渲染组件到页面

<script type="text/babel">
  // 1.创建函数式组件
  function demo() {
    return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
  }
  // 2.渲染组件到页面
  ReactDOM.render(demo, document.getElementById('test'))
</script>

注意:直接把函数放在渲染函数里,它会报错:函数类型是不能作为React节点的。

解决方案:对于函数式组件,渲染时应该使用标签。

<script type="text/babel">
  // 1.创建函数式组件
  function demo() {
    return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
  }
  // 2.渲染组件到页面
  ReactDOM.render(<demo/>, document.getElementById('test'))
</script>

注意:此时用了标签,它还会报错:标签是不被浏览器所允许的。

解决方案:jsx的语法规则,如果标签首字母是小写,则该标签转为html的同名元素,很明显html是没有demo这个标签的,因此报错。所以如果是组件,那么首字母必须是大写,只需要把方法名和标签名的首字母都改为大写即可。

<script type="text/babel">
  // 1.创建函数式组件
  function Demo() {
    return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>

三、函数组件式原理

1、this指向

在函数里的this,是undefined,而不是window,因为它要经过babel翻译,会开启了严格模式。

2、React对函数式组件的工作流程

(1)React解析了组件标签,找到了MyComponent组件

(2)发现组件式使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中

2.1.2 类式组件

概要总结

1、创建类式组件

2、类式组件运行原理

一、创建类式组建

1、继承React.Component

2、类里必须要有render函数

3、render函数必须有return返回值

<script type="text/babel">
  // 1.创建类式组件
  class MyComponent extends React.Component {
    render() {
      return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
    }
  }
</script>

二、渲染组件到页面

<script type="text/babel">
  // 1.创建类式组件
  class MyComponent extends React.Component {
    render() {
      return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>

三、类式组件原理

1、React对类式组件的工作流程

(1)React解析了组件标签,找到了MyComponent组件

(2)发现组件式使用类定义的,随后new出来该类的实例,并通过实例调用到原型上的render方法

(3)将render返回的虚拟DOM转为真实DOM,随后呈现在页面中

2、render函数里的this指向

由于render函数是在类组件内部,因此this指向类的实例对象。

<script type="text/babel">
  // 1.创建类式组件
  class MyComponent extends React.Component {
    render() {
      // render是放在哪里的? —— MyComponent的原型对象上,供实例使用
      // render中的this是谁? —— MyComponent的实例对象。
      console.log('render中的this', this)
      return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>

2.2 组件三大核心属性1: state

2.2.1 初始化state

概要总结

1、初始化state属性

2、render函数读取state属性

一、初始化state属性

1、类式组件添加构造器

因为类式组件它是继承于React.Component,这个state属性是继承之后就已经存在了,它的默认值为null。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    render() {
      console.log(this)
      return <h1>今天天气很炎热</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

现在我们需要对state重新赋值,因此要先写一个构造器,把父组件的东西继承下来,然后再改写:

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props);
    }
    render() {
      console.log(this)
      return <h1>今天天气很炎热</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

2、重写state属性

React规定,state属性必须是一个对象。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props);
      this.state = {isHot: true}
    }
    render() {
      console.log(this)
      return <h1>今天天气很炎热</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

二、render函数读取state属性

由于render函数里的this就是指向组件类本身,而state是类的属性,因此直接在render函数里使用this.state就能读取成功。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      this.state = {isHot: true}
    }
    render() {
      const {isHot} = this.state
      return <h1>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

2.2.2 react的事件绑定

概要总结

1、react的3种绑定事件方式

2、react绑定与原生事件绑定的区别

一、react绑定事件的3种方式

1、使用addEventListener方法绑定

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 id="title">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  const title = document.getElementById('title')
  title.addEventListener('click', () => {
    console.log('标题被点击了')
  })
</script>

2、使用onclick方法绑定

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 id="title">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  const title = document.getElementById('title')
  title.onclick = () => {
    console.log('标题被点击了')
  }
</script>

3、在元素里绑定事件

前两个方法虽然可以,但是这毕竟都是原生的绑定方法,跟React无关。在元素里绑定事件是React推荐的。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onclick="demo()">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  function demo() {
    console.log('标题被点击了')
  }
</script>

注意:它会报错:onclick是一个不被允许的属性,你是不是想用"onClick"?

解决:React把原生的事件都进行了一次重写,命名规则使用了驼峰式命名。例如onclick会被改写成onClick,onblur改写成onBlur

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick="demo()">今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  function demo() {
    console.log('标题被点击了')
  }
</script>

注意:它会报错:onClick监听者必须是一个函数,但是我得到了一个不想要的string类型。

解决:在React里跟原生js有所不同,React绑定事件需要传一个函数进去即可,可以将"demo()"写成{demo()}。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={demo()}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  function demo() {
    console.log('标题被点击了')
  }
</script>

注意:绑定事件写成{demo()},现在的情况是还没触发事件自己就已经先执行了。

解决:React在渲染组件的时候,React会初始化一个实例,实例会调用render方法,然后执行return返回值。发现里面绑定了一个onClick事件,它的值是demo方法的返回值。现在就变成了把demo的返回值赋值给onClick事件,demo方法的返回值是undefined的。React做了处理,发现了是undefined赋值给事件的,它就不会进行处理。最终的解决办法就是把()去掉,把demo函数给onClick作为回调。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  function demo() {
    console.log('标题被点击了')
  }
</script>

2.2.3 类中方法中的this

概要总结

1、通过类内部的this赋值给外部变量来获取类属性

2、类内部定义函数的this指向问题

一、在方法里指定this指向类实例对象

由于方法在类的外部,如果想在方法里操作类里的属性,首先要解决this的指向问题。因为在方法里的this是没办法指向类实例的,而类实例是由React自己创建的。可以通过外部定义一个变量,将类里的this赋值给变量,那么方法就可以操作类属性。

<script type="text/babel">
  let that
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
      that = this
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))

  function demo() {
    console.log(that)
  }
</script>

说明:这样虽然可以操作类属性,但是由于里面的逻辑都属于类的内部,而方法定义在外面显然是不合理的。

二、将事件绑定的方法挪到类的内部

将方法写在类的内部,绑定的事件需要加上this.即可,而方法里可以通过this来获取类里的属性。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
    changeWeather() {
      // changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
      // 通过Weather实例调用Weather时,changeWeather中的this就是Weather实例
      console.log(this.state.isHot)
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

注意:这里报错说的是在undefined里没有state属性,也就是说this是undefined。

说明:

(1)首先类的constructor和render两个函数,是React在构造的时候内部自己调用的,因此this一定是指向类的实例对象,而自己写的changeWeather方法并不是类实例调用的,因此它的this并不是指向类实例

(2)在绑定事件的过程中,onClick={this.changeWeather}这个写法就相当于把changeWeather这个函数赋值给了onClick作为回调而已,这只是一个赋值语句。所以在触发这个点击事件的时候,它就把这个changeWeather函数从堆里面找出来直接调用,那里面的this应该指向window而不是类实例。

(3)在类中定义的方法,它在局部都会开启严格模式,是类自己开的与babel没有关系,所以这个this就变成了undefined。

2.2.4 解决类中this的指向问题

概要总结

1、使用bind方法解决this的指向问题

一、使用bind方法解决this的指向问题

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
      this.changeWeather = this.changeWeather.bind(this)
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
    changeWeather() {
      // changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
      // 通过Weather实例调用Weather时,changeWeather中的this就是Weather实例
      console.log(this.state.isHot)
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

1、this是指向类实例,那么在this.changeWeather = this.changeWeather.bind(this)中,右边的this.changeWeather在实例属性是不存在的,但是它会顺着往原型链上找到该方法。

2、bind会生成一个新的函数,而且改变this指向。.bind(this)的意思是用这个传入的this来改变函数内部的this指向,传入的this就是类实例,而函数内部的this原本指向window,所以.bind(this)就把函数内部指向window的this变成指向了类实例。

3、把改变了this的新函数赋值给左边的this.changeWeather,这个this.changeWeather属于实例属性。

此时在自身属性和原型链都存在changeWeather,那么onClick绑定事件的时候,它会按照就近原则先去找实例,再找原型,因此它目前绑定的是实例上的changeWeather方法。

2.2.5 setState的使用

概要总结

1、直接改变state属性

2、使用setState改变状态

3、setState方法是合并操作

4、构造器、render函数以及自定义方法的调用次数

一、直接改变state内部属性

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
      this.changeWeather = this.changeWeather.bind(this)
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
    changeWeather() {
      // 获取原来的isHot值
      const isHot = this.state.isHot
      this.state.isHot = !isHot
      console.log(this.state.isHot)
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

注意:此时我们发现,state里面的值已经改变了,但是视图并没有切换内容,因为React不承认你直接修改状态属性的行为,也就是说不能直接改state。

二、使用setState方法修改状态

我们查看这个类就会发现,首先最外层的就是这个类的属性和原型,这个原型也就是这个类的原型。由于这个类是继承了React.Component,因此原型里的原型,就是React.Component,而这个setState方法就是从React.Component里继承过来的。

此时使用setState方法,传入对象进行修改对应的属性即可:

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      // 初始化状态
      this.state = {isHot: true}
      this.changeWeather = this.changeWeather.bind(this)
    }
    render() {
      // 读取状态
      const {isHot} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
    changeWeather() {
      // 获取原来的isHot值
      const isHot = this.state.isHot
      this.setState({isHot: !isHot})
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

三、setState方法是合并,而不是替换

setState修改的过程中,它是一个合并的操作,只会改变对应的属性值,其它的保持不变。

四、构造函数、render、自定义方法分别调用次数

1、constructor是类初始化的时候调用1次

2、render函数是1+n次,1是类初始化的时候调用,n是代表状态的改变次数,也就是说setState调用了几次,render函数就执行几次

3、自定义方法就是触发几次执行几次

2.2.6 setState的简写方式

概要总结

1、直接改变state属性

2、使用setState改变状态

3、setState方法是合并操作

4、构造器、render函数以及自定义方法的调用次数

一、类的constructor用法

<script type="text/babel">
  class Car {
    constructor(name, price) {
      this.name = name
      this.price = price
    }
  }
  const c1 = new Car('奔驰c63', 199)
  console.log(c1)
</script>

对于初始化传入的参数,那必须得在constructor里去接收。但是对于一些实例的公共参数,例如汽车有4个轮子这样的参数,是不需要外部传入的,我们也没必要写在constructor里,可以写在类的内部。

<script type="text/babel">
  class Car {
    constructor(name, price) {
      this.name = name
      this.price = price
    }
    wheel = 4
  }
  const c1 = new Car('奔驰c63', 199)
  console.log(c1)
</script>

注意:在类里写赋值语句,它是给类的实例对象添加属性,而不是给原型添加属性!

二、精简constructor里的state初始化

我们在constructor里对state重新赋值,无非是React给我们初始化实例的state属性值为null,不符合我们需求。那我们可以把写在constructor里的state拿出来放在类的内部:

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
      this.changeWeather = this.changeWeather.bind(this)
    }
    
    state = {isHot: false, wind: '微风'}

    render() {
      const {isHot, wind} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
    }

    changeWeather() {
      const isHot = this.state.isHot
      this.setState({isHot: !isHot})
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

三、精简constructor里对方法的this绑定语句

在constructor里对自定义方法调用.bind(this),目的是把方法里的this指向类的实例。此时自定义的changeWeather方法是定义在类的原型上,如果把它变成一个方法再赋值给changeWeather,那么这个changeWeather方法就变成了实例属性了。也就是把changeWeather() {}改成changeWeather = function() {}。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
    }
    
    state = {isHot: false, wind: '微风'}

    render() {
      const {isHot, wind} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
    }

    changeWeather = function() {
      const isHot = this.state.isHot
      this.setState({isHot: !isHot})
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

此时它仍然会报错this为undefined,因为现在的做法只是把changeWeather从原型挪到实例上,它给onClick绑定的时候,this仍然是undefined的。但只需要使用箭头函数,那么this的指向问题就迎刃而解:

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    constructor(props) {
      super(props)
    }
    
    state = {isHot: false, wind: '微风'}

    render() {
      const {isHot, wind} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
    }

    changeWeather = () => {
      const isHot = this.state.isHot
      this.setState({isHot: !isHot})
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

四、删除constructor构造器

constructor构造器最大的作用就是初始化state以及改变自定义方法的this指向,然而目前这两个问题都不需要在constructor里处理,也就是说constructor可以去掉了。

<script type="text/babel">
  // 1.创建组件
  class Weather extends React.Component {
    // 初始化状态
    state = {isHot: false, wind: '微风'}

    render() {
      const {isHot, wind} = this.state
      return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
    }

    changeWeather = () => {
      const isHot = this.state.isHot
      this.setState({isHot: !isHot})
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>

2.3 组件三大核心属性2: props

2.3.1 props的基本使用

概要总结

1、创建组件传参

2、使用props读取传入参数

一、创建组件传参

在创建组件的时候,对组件直接传参,组件从React继承下来的实例属性props能接收到参数。

<script type="text/babel">
  // 1.创建组件
  class Persion extends React.Component {
    render() {
      return (
        <ul>
          <li>姓名:tom</li>
          <li>性别:女</li>
          <li>年龄:18</li>
        </ul>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test'))
</script>

二、组件内部使用props读取传参属性

组件不需要做任何处理,直接使用this.props读取属性渲染即可。

<!-- 准备好一个"容器" -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>

<script type="text/babel">
  // 1.创建组件
  class Persion extends React.Component {
    render() {
      return (
        <ul>
          <li>姓名:{this.props.name}</li>
          <li>性别:{this.props.sex}</li>
          <li>年龄:{this.props.age}</li>
        </ul>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Persion name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
  ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test2'))
  ReactDOM.render(<Persion name="老刘" age="30" sex="女"/>, document.getElementById('test3'))
</script>

三、优化props代码

<!-- 准备好一个"容器" -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>

<script type="text/babel">
  // 1.创建组件
  class Persion extends React.Component {
    render() {
      const {name, sex, age} = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Persion name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
  ReactDOM.render(<Persion name="tom" age="18" sex="女"/>, document.getElementById('test2'))
  ReactDOM.render(<Persion name="老刘" age="30" sex="女"/>, document.getElementById('test3'))
</script>

2.3.2 批量传递props

概要总结

1、使用扩展运算符批量传递props

2、回顾扩展运算符对对象的使用

一、使用扩展运算符传递批量参数

const p = {name: '老刘', age: 30, sex: '女'}

ReactDOM.render({...p}/>, document.getElementById('test3'))

<script type="text/babel">
  // 1.创建组件
  class Persion extends React.Component {
    render() {
      const {name, sex, age} = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
  const p = {name: '老刘', age: 30, sex: '女'}
  ReactDOM.render(<Persion {...p}/>, document.getElementById('test3'))
</script>

二、关于扩展运算符

1、扩展运算符无法直接展开对象

扩展运算符可以直接对数组进行扩展,但是对象不行。

<script type="text/babel">
  let person = {name: 'tom', age: 18}
  console.log(...person)
</script>

2、扩展运算符字面量展开对象

虽然扩展运算符无法直接展开对象,但可以在外层包裹一个花括号,即变成ES6语法使用字面量展开对象。

<script type="text/babel">
  let person = {name: 'tom', age: 18}
  console.log({...person})
</script>

注意:使用扩展运算符可以实现拷贝或者合并对象的功能,但这种既不是深拷贝也不是浅拷贝,它只能深拷贝第一层,第二层开始又是浅拷贝。

3、React使用扩展运算符展开对象

通过React库和babel的结合,可以使用扩展运算符展开对象。

<script type="text/babel">
  // 1.创建组件
  class Persion extends React.Component {
    render() {
      const {name, sex, age} = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
  const p = {name: '老刘', age: 30, sex: '女'}
  ReactDOM.render(<Persion {...p}/>, document.getElementById('test3'))
</script>

注意:这里的{...p}的花括号,代表的意思是告诉React这里面写的是js表达式,而不是扩展运算符的对象字面量的花括号,所以在React这里,它是没有{}而直接对对象使用扩展运算符,这种只能在React与babel结合才可以使用。

2.3.3 对props进行限制

概要总结

1、使用PropTypes限制传参的类型和必填

2、使用defaultProps设置参数默认值

一、引入prop-types.js

在React15版本,它是通过React.PropTypes.xxx来进行限制。但是这种方式是在React身上加上PropTypes属性,随着内容越来越多,会导致React核心对象越来越重,而且这个限制也不是一定需要,所以直接挂在React上不太合适。

在16的版本里,PropTypes已经不再从React里读取了,通过引入prop-types.js来使用PropTypes。

<script type="text/javascript" src="../js/prop-types.js"></script>

二、使用PropTypes进行限制

1、限制类型

<script type="text/babel">
  // 1.创建组件
  class Person extends React.Component {
    render() {
      const {name, sex, age} = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
  Person.propTypes = {
    name: PropTypes.string
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
  ReactDOM.render(<Person name="tom" age="18" sex="女"/>, document.getElementById('test2'))

  const p = {name: '老刘', age: 30, sex: '女'}
  ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))
</script>

这里限制了name属性为字符串类型,如果传了其它类型,它也能正常显示,但会在控制台报错:

const p = {name: 100, age: 30, sex: '女'}

假设定了string类型,结果传的是number类型,它会提示它希望得到string类型,实际上得到number类型。

注意:对于函数类型,它的属性并不是function,而是func。因为function属于关键字,它改名为func。

2、限制必传

使用isRequired即可限制必传,而且它可以叠加在其它限制的后面,例如:PropTypes.string.isRequired

Person.propTypes = {
  name: PropTypes.string.isRequired
}
// 2.渲染组件到页面
ReactDOM.render(<Person age="19" sex="男"/>, document.getElementById('test1'))

三、使用defaultProps设置默认值

Person.defaultProps = {
  sex: '不男不女',
  age: 18
}
ReactDOM.render(<Person name="jerry"/>, document.getElementById('test1'))

2.3.4 props的简写方式

概要总结

1、props属性不能修改

2、将propsType、defaultProps等类属性使用static放在类内部

一、props属性不能修改

props里的属性都是只读的,只可以拿来使用,不允许做修改。

<script type="text/babel">
  // 1.创建组件
  class Person extends React.Component {
    render() {
      const {name, sex, age} = this.props
      this.props.name = 'jack';    // 此行代码会报错,因为props是只读的
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
  Person.propTypes = {
    name: PropTypes.string
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'))
</script>

二、props的简写方式

这里定义了类之后,无论是对它进行类型的限制还是指定默认值,都是给类自身添加属性。

对类本身添加属性,可以在类的内部,使用static关键字进行添加:

<script type="text/babel">
  // 1.创建组件
  class Person extends React.Component {
    // 对标签属性进行类型、必要性的限制
    static propTypes = {
      name: PropTypes.string.isRequired,  // 限制name必传,且为字符串
      sex: PropTypes.string,  // 限制sex为字符串
      age: PropTypes.number,  // 限制age为数值
      speak: PropTypes.func  // 限制speak为函数
    }
    // 指定默认标签属性值
    static defaultProps = {
      sex: '男', // sex默认值为男
      age: 18 // age默认值为18
    }
    render() {
      const {name, sex, age} = this.props
      // props是只读的
      this.props.name = 'jack';   // 此行代码会报错,因为props是只读的
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }
</script>

2.3.5 类式组件中的构造器与props

概要总结

1、constructor的作用

2、constructor的使用场景

一、constructor的作用

官网描述:

通常,在React中,构造函数仅用于以下两种情况:

(1)通过给this.state赋值对象来初始化内部state。

(2)为事件处理函数绑定实例

这两个场景可以分别通过在类里初始化state以及使用箭头函数来解决,因此constructor可以省略。

二、constructor的使用场景

官网描述:

在React组件挂载之前,会调用它的构造函数。在为React.Component子类实现构造函数时,应在其他语句之前调用super(props)。否则,this.props在构造函数中可能会出现未定义的bug。

说明:constructor可以不写,如果写了之后,就必须要给super传入props,否则在constructor里使用this.props会undefined。但即使真的要在constructor里使用props,也根本没必要写this.props,直接用props就得了。

1、super传入props

<script type="text/babel">
  // 1.创建组件
  class Person extends React.Component {

    constructor(props) {
      super(props);
      console.log('constructor', this.props);
    }
    ......
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" />, document.getElementById('test1'))
</script>

2、super没传props

<script type="text/babel">
  // 1.创建组件
  class Person extends React.Component {

    constructor() {
      super();
      console.log('constructor', this.props);
    }
    ......
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" />, document.getElementById('test1'))
</script>

2.3.6 函数式组件使用props

概要总结

1、函数式组件接收props

2、函数式组件对props进行限制

一、函数式组件接收props

在类式组件中,只要在实例化组件的时候传入参数,类会自动收集到props里,通过this.props即可使用。而函数式组件是没有this的,但它是一个函数,可以通过接收参数的方式接收props。

<script type="text/babel">
  // 1.创建组件
  function Person(props) {
    const {name, sex, age} = props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age}</li>
      </ul>
    )
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" sex="女" age={18} />, document.getElementById('test1'))
</script>

二、函数式组件对props进行限制

在类式组件中,可以使用static关键字在类的内部进行限制。在函数式组件只能放在外部,对函数名添加属性进行限制。

<script type="text/babel">
  // 1.创建组件
  function Person(props) {
    const {name, sex, age} = props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age}</li>
      </ul>
    )
  }

  // 对标签属性进行类型、必要性的限制
  Person.propTypes = {
    name: PropTypes.string.isRequired,  // 限制name必传,且为字符串
    sex: PropTypes.string,  // 限制sex为字符串
    age: PropTypes.number,  // 限制age为数值
  }
  // 指定默认标签属性值
  Person.defaultProps = {
    sex: '男', // sex默认值为男
    age: 18 // age默认值为18
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Person name="jerry" sex="女" age={18} />, document.getElementById('test1'))
</script>

2.4 组件三大核心属性3: refs与事件处理

2.4.1 字符串形式的ref

概要总结

1、给DOM节点添加ref属性

2、通过this.refs获取对应的dom节点

需求:自定义组件,功能说明如下:

1、点击按钮,提示第一个输入框中的值

2、当第2个输入框失去焦点时,提示这个输入框中的值

一、编写html并定义事件

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showData = () => {
      console.log(this)
    }
    render() {
      return (
        <div>
          <input type="text" placeholder="点击按钮提示数据"/> 
          <button onClick={this.showData}>点我提示左侧的数据</button> 
          <input type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

二、给DOM节点添加ref属性

在React中获取dom节点有两种方式:第一种就是用原生js的方法获取,这显然是不合理的;第二种是给dom节点添加一个ref属性。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showData = () => {
      console.log(this)
    }

    render() {
      return (
        <div>
          <input ref="input1" type="text" placeholder="点击按钮提示数据"/> 
          <button ref="button100" onClick={this.showData}>点我提示左侧的数据</button> 
          <input ref="input2" type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

React提供了一个内置的refs属性,它跟props一样,以键值对的形式收集所有的dom节点的ref属性,键名为ref的值,值为dom节点。

三、通过this.refs获取对应的dom节点

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showData = () => {
      const {input1} = this.refs
      alert(input1.value)
    }

    // 展示右侧输入框的数据
    showData2 = () => {
      const {input2} = this.refs
      alert(input2.value)
    }

    render() {
      return (
        <div>
          <input ref="input1" type="text" placeholder="点击按钮提示数据"/> 
          <button ref="button100" onClick={this.showData}>点我提示左侧的数据</button> 
          <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

2.4.2 回调形式的ref

概要总结

1、使用箭头函数传入ref属性

2、通过this来获取对应的dom节点

一、字符串形式的ref将被淘汰

官方说明:react.docschina.org/docs/refs-a…

二、ref属性传入箭头函数

1、ref传入箭头函数

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    render() {
      return (
        <div>
          <input ref={() => {}} type="text" placeholder="点击按钮提示数据"/> 
          <button ref="button100" onClick={this.showData}>点我提示左侧的数据</button> 
          <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

<input ref={() => {}} />

回调函数要满足3个条件:定义函数;自己没有调用;函数最终被执行。

在这里ref使用箭头函数之后,可以测试组件内部是否执行它:

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    render() {
      return (
        <div>
          <input ref={c => console.log(c)} type="text" placeholder="点击按钮提示数据"/> 
          <button ref="button100" onClick={this.showData}>点我提示左侧的数据</button> 
          <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

事实证明,React内部会帮我们执行ref里的函数,而且还传入了dom节点,因此它的第一个参数就是所在的dom。

2、定义变量存储ref返回dom节点

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showData = () => {
      const {input1} = this
      alert(input1.value)
    }

    // 展示右侧输入框的数据
    showData2 = () => {
      const {input2} = this
      alert(input2.value)
    }

    render() {
      return (
        <div>
          <input ref={c => this.input1 = c} type="text" placeholder="点击按钮提示数据"/> 
          <button ref="button100" onClick={this.showData}>点我提示左侧的数据</button> 
          <input ref={c => this.input2 = c} onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

把ref所在的节点挂在了实例自身上,并取一个名字叫input1和input2。那么在函数获取这两个dom节点直接在this里获取即可。

2.4.3 回调ref中调用次数的问题

概要总结

1、ref使用内联函数的执行次数问题

2、使用类定义函数替代内联函数

一、官方说明

官方说明:react.docschina.org/docs/refs-a…

所谓内联函数指的是在标签里传入函数,例如: this.input1 = c} type="text"/>。我们发现,在这里实际上并没有文档所说第一次获取了null,第二次才获取DOM元素,这是因为它说的是在更新过程中才会发生,而我们这是属于render函数初始化的第一次调用,因此能正常获取DOM元素。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showInfo = () => {
      const {input1} = this
      alert(input1.value)
    }

    render() {
      return (
        <div>
          <input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/>
          <button onClick={this.showInfo}>点我提示输入的数据</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

二、观察ref在更新过程的执行次数

更新过程指的是通过setState改变数据的时候产生的更新。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    state = {
      isHot: true
    }

    // 展示左侧输入框的数据
    showInfo = () => {
      const {input1} = this
      alert(input1.value)
    }

    changeWeather = () => {
      const {isHot} = this.state
      this.setState({isHot: !isHot})
    }

    render() {
      const {isHot} = this.state
      return (
        <div>
          <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
          <input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/><br/><br/>
          <button onClick={this.showInfo}>点我提示输入的数据</button>
          <button onClick={this.changeWeather}>点我切换天气</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

上面的例子通过按钮切换天气,实现更新的过程。此时会发现,每次更新ref的函数会执行两次,第一次传入为null,第二次才是DOM节点:

官方说明:这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。

为了保证代码渲染到页面上,一定要先调用一次render函数。在第一次调用的时候,发现里面有函数式的ref,那么它帮我们调用。因此第一次调用是能拿到它对应的dom节点。

在更新状态的时候,会驱动页面显示。驱动页面显示无非是通过重新执行render函数,它发现里面有函数式的ref,注意它并不是直接执行,而是重新赋一个新的函数再执行,之前的函数执行完就释放掉了。它不确定之前的函数接收了什么,做了什么操作,它为了保证这个函数完美的被清空,因此会传入一个null。然后紧接着又调用第二次,才把当前的dom节点传入。

三、ref使用类函数替代内联函数

官方说明:这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。

简单来说,就是不要用内联函数,把函数定义在类里面,然后传给ref即可:

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    state = {
      isHot: true
    }

    // 展示左侧输入框的数据
    showInfo = () => {
      const {input1} = this
      alert(input1.value)
    }

    changeWeather = () => {
      const {isHot} = this.state
      this.setState({isHot: !isHot})
    }

    saveInput = (c) => {
      this.input1 = c
      console.log('@', c)
    }

    render() {
      const {isHot} = this.state
      return (
        <div>
          <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
          {/*<input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/><br/><br/>*/}
          <input ref={this.saveInput} type="text"/><br/><br/>
          <button onClick={this.showInfo}>点我提示输入的数据</button>
          <button onClick={this.changeWeather}>点我切换天气</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

<input ref={this.saveInput} type="text"/>。使用这种方式,无论render函数执行多少次,ref所绑定的函数始终没有变化,从而解决多次执行的问题。

2.4.4 createRef的使用

概要总结

1、使用createRef创建ref容器

2、创建多个createRef对应多个ref

注意:React.createRef方法必须要在16.3版本以后才能使用

一、React.createRef

React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点。

简单理解为,React.createRef调用了之后,React会在实例上创建一个容器,它能拿到所在元素的ref存进去,我们在获取ref的时候,在这个ref容器里拿就可以了。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 展示左侧输入框的数据
    showInfo = () => {
      const {input1} = this
      alert(input1.value)
    }

    render() {
      return (
        <div>
          <input ref={(c) => {this.input1 = c;console.log('@', c)}} type="text"/>
          <button onClick={this.showInfo}>点我提示输入的数据</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

二、使用多个createRef

React.createRef只能存一个ref,如果有多个ref则需要调用多个React.createRef方法来创建多个ref容器进行存储。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    /*
    * React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点
    * */
    myRef = React.createRef()
    myRef2 = React.createRef()

    // 展示左侧输入框的数据
    showData = () => {
      alert(this.myRef.current.value)
    }

    // 展示右侧输入框的数据
    showData2 = () => {
      alert(this.myRef2.current.value)
    }

    render() {
      return (
        <div>
          <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/> 
          <button onClick={this.showData}>点我提示左侧的数据</button> 
          <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点左侧的数据"/>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

2.4.5 react中的事件处理

概要总结

1、React使用的是自定义(合成)事件,而不是使用的原生DOM事件

2、React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)

3、通过event.target得到发生事件的DOM元素对象

需求:

1、通过OnXxx属性指定事件处理函数(注意大小写)

(1)React使用的是自定义(合成)事件,而不是使用的原生DOM事件 —— 为了更好的兼容性

(2)React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —— 为了高效

2、通过event.target得到发生事件的DOM元素对象 —— 不要过度使用ref

一、React自定义合成事件

React把原生js的事件重新封装了一遍,目的就是为了更好的兼容性。

二、React事件通过事件委托最外层元素处理

假设在一个ul下需要绑定所有的li的点击事件,显然把点击事件委托给父级元素ul更加高效。

三、使用event.target获取DOM元素

实际上我们绑定点击事件的时候,它都会给每个事件传入原生的event对象。如果只是获取本元素的值或者属性,可以通过event.target进行获取,而没必要滥用ref。

<script type="text/babel">
  // 1.创建组件
  class Demo extends React.Component {
    // 创建ref容器
    myRef = React.createRef()
    myRef2 = React.createRef()

    // 展示左侧输入框的数据
    showData = () => {
      alert(this.myRef.current.value)
    }

    // 展示右侧输入框的数据
    showData2 = (event) => {
      alert(event.target.value)
    }

    render() {
      return (
        <div>
          <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/> 
          <button onClick={this.showData}>点我提示左侧的数据</button> 
          <input onBlur={this.showData2} type="text" placeholder="失去焦点左侧的数据"/>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Demo />, document.getElementById('test1'))
</script>

这里已经可以通过event.target.value就能替代ref的使用。

2.5 收集表单数据

2.5.1 非受控组件与受控组件

概要总结

1、非受控组件:页面中所有输入类的dom,包括input、checkbox、radio等等,属于现用现取的,也就是需要用到的时候才去获取dom节点的值

2、受控组件:页面中所有输入类的dom,随着输入就可以维护到状态里,等需要用到的时候直接从状态里取出来

需求:定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息

一、定义有表单的组件

<script type="text/babel">
  // 1.创建组件
  class Login extends React.Component {

    handleSubmit = () => {
      
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input type="text" name="username"/>
          密码:<input type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

二、非受控组件

我们在提交表单的时候,需要获取表单的每一项数据,可以通过ref的形式获取每一个输入框的值。

<script type="text/babel">
  // 1.创建组件
  class Login extends React.Component {

    handleSubmit = event => {
      event.preventDefault()    // 阻止表单提交
      const {username, password} = this
      alert(`你输入的用户名是:${username.value}, 你输入的密码是:${password.value}`)
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input ref={c => this.username = c} type="text" name="username"/>
          密码:<input ref={c => this.password = c} type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

非受控组件:页面中所有输入类的dom,包括input、checkbox、radio等等,属于现用现取的,也就是需要用到的时候才去获取dom节点的值。

三、受控组件

想要获取表单每一项,除了给每一项设置ref来获取以外,也可以给它们绑定onChange事件,在事件里把它们的值存在state里。在提交表单的时候,就不需要获取dom节点来取值了,直接从state里拿值即可。

<script type="text/babel">
  // 1.创建组件
  class Login extends React.Component {

    state = {
      username: '',   // 用户名
      password: ''    // 密码
    }

    // 保存用户名到状态中
    saveUsername = event => {
      this.setState({username: event.target.value})
    }

    // 保存密码到状态中
    savePassword = event => {
      this.setState({password: event.target.value})
    }

    // 表单提交的回调
    handleSubmit = event => {
      event.preventDefault()    // 阻止表单提交
      const {username, password} = this.state
      alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input onChange={this.saveUsername} type="text" name="username"/>
          密码:<input onChange={this.savePassword} ref={c => this.password = c} type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

受控组件:页面中所有输入类的dom,随着输入就可以维护到状态里,等需要用到的时候直接从状态里取出来。

总结:受控组件相比非受控组件更好一些,毕竟它没有设置ref,在性能上会更为优化。

2.5.2 高阶函数_函数柯里化

概要总结

1、高阶函数

(1)若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数

(2)若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数

常见的高阶函数有:Promise、setTimeout、arr.map()等等。

2、函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式

3、使用函数柯里化的方式,解决React绑定事件传参问题

需求:

定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息

一、优化受控组件

对于受控组件而言,它在每一个输入项绑定一个onChange事件来把值存储在state状态中。但如果输入项非常多,这显然是过于臃肿且不合理。此时我们可以尝试让所有输入项绑定同一个方法,然后根据类型作为参数传到方法里进行区分:

<script type="text/babel">
  // 1.创建组件
  class Login extends React.Component {

    state = {
      username: '',   // 用户名
      password: ''    // 密码
    }

    // 保存表单数据到状态中
    saveFormData = event => {
      this.setState({password: event.target.value})
    }

    // 表单提交的回调
    handleSubmit = event => {
      event.preventDefault()    // 阻止表单提交
      const {username, password} = this.state
      alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
          密码:<input onChange={this.saveFormData('password')} ref={c => this.password = c} type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

注意:所有输入项的onChange绑定同一个saveFormData事件之后,需要传入输入项的类型作为参数。但这样一来会产生以下问题

(1)onChange={this.saveFormData('username')}这句代码的意思是把执行saveFormData方法的返回值作为onChange的回调。它的返回值明显是没有的,也就是undefined,这就相当于把undefined赋给onChange去绑定

(2)在onChange里传参就意味着已经自执行了,因此在初始化的时候它就会先把方法执行

(3)由于在绑定事件上传参了,因此把React自带的event参数给覆盖了,所以事件方法里的event已经不再是原来的event了,而是自己传过去的参数

二、绑定事件返回一个函数

React绑定事件它需要的是一个函数,当我们想传参的时候,它就变成了执行函数,把函数的返回值作为事件的绑定。例如:

onChange={this.saveFormData}这样写是正确的,React会帮我们传入一个event作为参数。但如果写成onChange={this.saveFormData('username')},那意思就是去调用saveFormData方法,把它的返回值赋给onChange进行绑定。之所以不成功原因是saveFormData没有返回值。

既然又想自己传参又想正常绑定,那么可以在saveFormData里返回一个函数,这样既能满足自己传参,也能满足React的事件绑定:

saveFormData = dataType => {
  return () => {
    console.log('@')
  }
}

onChange={this.saveFormData('username')}它是将saveFormData的返回的函数进行事件绑定,因此实际上它所绑定的正是那个return () => {},所以React会帮我们把event传到这个匿名函数里:

saveFormData = dataType => {
  return (event) => {
    console.log(dataType, event.target.value)
  }
}

目前已经拿到输入项的类型以及它的值,最后一步把它们的值存进state状态里即可。

saveFormData = dataType => {
  return (event) => {
    this.setState({[dataType]: event.target.value})
  }
}

完整代码:

<script type="text/babel">
  // 1.创建组件
  class Login extends React.Component {

    state = {
      username: '',   // 用户名
      password: ''    // 密码
    }

    // 保存表单数据到状态中
    saveFormData = dataType => {
      return (event) => {
        this.setState({[dataType]: event.target.value})
      }
    }

    // 表单提交的回调
    handleSubmit = event => {
      event.preventDefault()    // 阻止表单提交
      const {username, password} = this.state
      alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
          密码:<input onChange={this.saveFormData('password')} ref={c => this.password = c} type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

三、高阶函数与函数柯里化

1、高阶函数

(1)若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数

(2)若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数

常见的高阶函数有:Promise、setTimeout、arr.map()等等。

2、函数柯里化

通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式。在上述的saveFormData方法已经使用了函数柯里化:

saveFormData = dataType => {
  return (event) => {
    this.setState({[dataType]: event.target.value})
  }
}

2.5.3 不用柯里化的写法

概要总结

1、绑定事件使用箭头函数,通过箭头函数里执行自定义的方法,可以实现同时传自定义和event参数

2、自定义的方法不需要返回一个函数,取消柯里化的写法

需求:

定义一个包含表单的组件:输入用户名密码后,点击登录提示输入信息

一、同时传入自定义参数与React的event参数

1、绑定事件使用箭头函数

对于React绑定事件而言,它目的只有一个,它要的就是一个函数,不管是直接把类定义的函数给它,还是调用了函数之后再返回一个函数给它都可以。

我们可以在绑定事件的时候,直接给予一个箭头函数,先满足了它的需求:

<input onChange={() => {}} type="text" name="username"/>

在这里React会给绑定的函数传入event的,所以在这个箭头函数里我们就能获取到event:

<input onChange={(event) => {}} type="text" name="username"/>

而在箭头函数里就可以执行我们自己定义的函数了,可以传自己想要的参数,并且把event也顺带传过去:

<input onChange={(event) => {this.saveFormData('username', event)}} type="text" name="username"/>

二、不用函数柯里化

既然在React事件绑定的时候已经能传入自定义参数和event参数,那就没必要再返回一个函数了,直接把数据存到状态里即可:

saveFormData = (dataType, event) => {

this.setState({[dataType]: event.target.value})

}

完整代码:

<script type="text/babel">
  /*
  * 高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数
  *     1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数
  *     2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数
  *     常见的高阶函数有:Promise、setTimeout、arr.map()等等
  * 函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式
  * */
  // 1.创建组件
  class Login extends React.Component {

    state = {
      username: '',   // 用户名
      password: ''    // 密码
    }

    // 保存表单数据到状态中
    saveFormData = (dataType, event) => {
      this.setState({[dataType]: event.target.value})
    }

    // 表单提交的回调
    handleSubmit = event => {
      event.preventDefault()    // 阻止表单提交
      const {username, password} = this.state
      alert(`你输入的用户名是:${username}, 你输入的密码是:${password}`)
    }

    render() {
      return (
        <form action="http://www.atguigu.com" onSubmit={this.handleSubmit}>
          用户名:<input onChange={event => this.saveFormData('username', event)} type="text" name="username"/>
          密码:<input onChange={event => this.saveFormData('password', event)} type="password" name="password"/>
          <button>登录</button>
        </form>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Login />, document.getElementById('test1'))
</script>

2.6 组件的生命周期

2.6.1 引出生命周期

概要总结

1、componentDidMount:组件完成挂载生命周期

2、componentWillUnmount:组件卸载前生命周期

需求:定义组件实现以下功能:

1、让指定的文本做显示/隐藏的渐变动画

2、从完全可见,到彻底消失,耗时2s

3、点击“不活了”按钮从界面中卸载组件

一、卸载组件

在React中提供了一个方法,叫做unmountComponentAtNode,它的意思很明确,就是指定dom节点卸载组件。

<div id="test1"></div>
<script type="text/babel">
  // 1.创建组件
  class Life extends React.Component {

    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }

    render() {
      return (
        <div>
          <h2>React学不会怎么办?</h2>
          <button onClick={this.death}>不活了</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Life />, document.getElementById('test1'))
</script>

二、实现透明度渐变效果

1、在render函数添加计时器

从完全可见,到彻底消失,耗时2s。思路就是改变元素的透明度,从1变为0。假设每次减少0.1透明度,那就是200毫秒变化一次。

<script type="text/babel">
  // 1.创建组件
  class Life extends React.Component {
    state = {opacity: 1}
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }
    render() {
      setInterval(() => {
        // 获取原状态
        let {opacity} = this.state
        // 减少0.1
        opacity -= 0.1
        if (opacity <= 0) {
          opacity = 1
        }
        // 设置新的透明度
        this.setState({opacity})
      }, 200)
      return (
        <div>
          <h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
          <button onClick={this.death}>不活了</button>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Life />, document.getElementById('test1'))
</script>

目前我们把定时器放在了render函数里,让React渲染组件的时候,执行render函数开启定时器。

注意:这里引发了一个无限的递归,也就是死循环。因为render函数的调用次数是1+n,组件初始化的时候调用一次,然后每次改变状态的时候它会调用。但是如果在render函数里改变状态,那就是一个死循环。

2、componentDidMount:组件完成挂载生命周期

componentDidMount从单词意义上来说,就是组件完成挂载。它跟render函数是兄弟。render函数的调用时机是初始化渲染和状态更新之后,而componentDidMount是在组件挂载完毕之后触发,只调用1次。

此时把render函数里的定时器,移到componentDidMount生命周期里执行即可:

<script type="text/babel">
  // 1.创建组件
  class Life extends React.Component {
    state = {opacity: 1}
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }
    // 组件挂载完毕
    componentDidMount() {
      setInterval(() => {
        // 获取原状态
        let {opacity} = this.state
        // 减少0.1
        opacity -= 0.1
        if (opacity <= 0) {
          opacity = 1
        }
        // 设置新的透明度
        this.setState({opacity})
      }, 200)
    }
    // 初始化渲染、状态更新之后
    render() {
      return (
        <div>
          <h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
          <button onClick={this.death}>不活了</button>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Life />, document.getElementById('test1'))
</script>

3、componentWillUnmount:组件卸载前生命周期

当计时器在运行的时候,如果卸载组件,它会发出以下报错:

它的意思是:我不能在已经被卸载的组件执行React状态的更新。此时可以通过componentWillUnmount生命周期,在组件卸载前把定时器去除掉即可:

<script type="text/babel">
  // 1.创建组件
  class Life extends React.Component {
    state = {opacity: 1}
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }
    // 组件挂载完毕
    componentDidMount() {
      this.timer = setInterval(() => {
        // 获取原状态
        let {opacity} = this.state
        // 减少0.1
        opacity -= 0.1
        if (opacity <= 0) {
          opacity = 1
        }
        // 设置新的透明度
        this.setState({opacity})
      }, 200)
    }
    // 组件将要卸载
    componentWillUnmount() {
      clearInterval(this.timer)
    }
    // 初始化渲染、状态更新之后
    render() {
      return (
        <div>
          <h2 style={{opacity: this.state.opacity}}>React学不会怎么办?</h2>
          <button onClick={this.death}>不活了</button>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Life />, document.getElementById('test1'))
</script>

2.6.2 生命周期(旧)_组件挂载流程

概要总结

1、组件挂载流程

(1)constructor:构造器

(2)componentWillMount:组件将要挂载的钩子

(3)render:render函数

(4)componentDidMount:组件挂载完毕的钩子

(5)componentWillUnmount:组件将要卸载的钩子

一、组件挂载流程

挂载流程的顺序是:constructor => componentWillMount => render => componentDidMount => componentWillUnmount

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 组件将要挂载的钩子
    componentWillMount() {
      console.log('Count---componentWillMount')
    }

    // 组件挂载完毕的钩子
    componentDidMount() {
      console.log('Count---componentDidMount')
    }

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

2.6.3 生命周期(旧)_setState流程

概要总结

1、setState流程

(1)shouldComponentUpdate:组件是否应该被更新的钩子

(2)componentWillUpdate:组件将要更新的钩子

(3)render:render函数

(4)componentDidUpdate:组件更新完毕的钩子

setState流程:

一、shouldComponentUpdate:组件是否应该被更新的钩子

setState更新状态之后,它会先执行shouldComponentUpdate钩子。这个组件的意思是:组件是否应该被更新。它是一个阀门,如果shouldComponentUpdate钩子返回了false,那么后面的钩子不会再执行,也就不会帮我们更新。所以每次执行setState的时候,它都要先去执行shouldComponentUpdate钩子判断是否应该更新。

shouldComponentUpdate钩子如果不写,React会给我们补上它,并且永远返回true。如果自己写了,那就以自己写的为准,并且一定要返回一个boolean值,否则会报错:

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {
    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }
    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }
    shouldComponentUpdate() {
      console.log('Count---shouldComponentUpdate')
    }
    render() {
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
        </div>
      )
    }
  }
  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

显然shouldComponentUpdate一定要返回一个boolean值,如果返回true,则正常往下执行:

shouldComponentUpdate() {
  console.log('Count---shouldComponentUpdate')
  return true
}

如果返回false,则不往下执行:

shouldComponentUpdate() {
  console.log('Count---shouldComponentUpdate')
  return false
}

二、componentWillUpdate:组件将要更新的钩子

经过shouldComponentUpdate钩子之后,下一个钩子执行的就是componentWillUpdate,它的意思就是组件将要被更新。

三、componentDidUpdate:组件更新完毕的钩子

经过componentWillUpdate钩子之后,那么组件就会执行render函数去更新,更新完毕之后再调componentDidUpdate钩子,它的意思就是组件更新完毕。

四、setState流程

setState流程的顺序是:shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 控制组件更新的"阀门"
    shouldComponentUpdate() {
      console.log('Count---shouldComponentUpdate')
      return true
    }

    // 组件将要更新的钩子
    componentWillUpdate() {
      console.log('Count---componentWillUpdate')
    }

    // 组件更新完毕的钩子
    componentDidUpdate() {
      console.log('Count---componentDidUpdate')
    }

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

2.6.4 生命周期(旧)_forceUpdate流程

概要总结

1、forceUpdate流程

(1)componentWillUpdate:组件将要更新的钩子

(2)render:render函数

(3)componentDidUpdate:组件更新完毕的钩子

forceUpdate流程:

一、forceUpdate:强制更新

forceUpdate与setState的区别在于,它比setState少走了一个shouldComponentUpdate钩子。它不需要经过shouldComponentUpdate钩子决定要不要更新,它直接就往下执行更新流程。它可以在没有状态更新的情况下,强制更新或者刷新一下。

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 强制更新按钮的回调
    force = () => {
      this.forceUpdate()
    }

    // 控制组件更新的"阀门"
    shouldComponentUpdate() {
      console.log('Count---shouldComponentUpdate')
      return false
    }

    // 组件将要更新的钩子
    componentWillUpdate() {
      console.log('Count---componentWillUpdate')
    }

    // 组件更新完毕的钩子
    componentDidUpdate() {
      console.log('Count---componentDidUpdate')
    }

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

此时在shouldComponentUpdate钩子里返回false,它没有办法改变状态。但调用this.forceUpdate()方法之后,它依然会执行componentWillUpdate以下的更新钩子流程。

2.6.5 生命周期(旧)_父组件render流程

概要总结

1、父组件render流程

(1)componentWillReceiveProps:组件将要接收新的props的钩子

(2)shouldComponentUpdate:组件是否应该被更新的钩子

(3)componentWillUpdate:组件将要更新的钩子

(4)render:render函数

(5)componentDidUpdate:组件更新完毕的钩子

父组件render流程:

一、父子组件

这里创建一个A与B组件,通过A组件调用B组件形成父子组件。

<script type="text/babel">
  class A extends React.Component {
    // 初始化状态
    state = {carName: '奔驰'}

    changeCar = () => {
      this.setState({carName: '奥拓'})
    }

    render() {
      return (
        <div>
          <div>我是A组件</div>
          <button onClick={this.changeCar}>换车</button>
          <B carName={this.state.carName} />
        </div>
      )
    }
  }

  class B extends React.Component {
    render() {
      return (
        <div>我是B组件,接收到的车是:{this.props.carName}</div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<A />, document.getElementById('test1'))
</script>

二、componentWillReceiveProps:组件将要接收新的props钩子

1、componentWillReceiveProps钩子在第二次接收props才执行

componentWillReceiveProps钩子,直接翻译就是组件将要接收参数,这个钩子显然是在子组件接收参数的时候调用的。

<script type="text/babel">
  class A extends React.Component {
    // 初始化状态
    state = {carName: '奔驰'}

    changeCar = () => {
      this.setState({carName: '奥拓'})
    }

    render() {
      return (
        <div>
          <div>我是A组件</div>
          <button onClick={this.changeCar}>换车</button>
          <B carName={this.state.carName} />
        </div>
      )
    }
  }

  class B extends React.Component {
    componentWillReceiveProps() {
      console.log('B---componentWillReceiveProps')
    }  
    render() {
      return (
        <div>我是B组件,接收到的车是:{this.props.carName}</div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<A />, document.getElementById('test1'))
</script>

这里子组件B显然已经接收到A的参数,而且已经显示在页面上,但是并没有执行componentWillReceiveProps钩子。

注意:componentWillReceiveProps钩子对于第一次传入props是不会执行的,从第二次以后传入才会执行。

当父组件A点击换车按钮,改变了状态。对于B组件而言,这是属于第二次传入props,那么它就会执行componentWillReceiveProps钩子。

2、componentWillReceiveProps钩子接收props参数

<script type="text/babel">
  class A extends React.Component {
    ......
  }

  class B extends React.Component {
    componentWillReceiveProps(props) {
      console.log('B---componentWillReceiveProps', props)
    }  
    render() {
      return (
        <div>我是B组件,接收到的车是:{this.props.carName}</div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<A />, document.getElementById('test1'))
</script>

2.6.6 总结生命周期(旧)

概要总结

1、初始化阶段:由ReactDom.render()触发--初次渲染

2、更新阶段:由组件内部this.setState()或父组件render触发

3、卸载组件:由ReactDOM.unmountComponentAtNode()触发

一、初始化阶段:由ReactDom.render()触发--初次渲染

1、constructor()

2、componentWillMount()

3、render()

4、componentDidMount() =====> 常用

一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

二、更新阶段:由组件内部this.setState()或父组件render触发

1、shouldComponentUpdate()

2、componentWillUpdate()

3、render() =====> 必须使用的一个

4、componentDidUpdate()

三、卸载组件:由ReactDOM.unmountComponentAtNode()触发

1、componentWillUnmount() =====> 常用

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

2.6.7 对比新旧生命周期

概要总结

1、升级React到最新版本

2、即将废弃三个生命周期:componentWillMount、componentWillUpdate、componentWillReceiveProps

3、新增两个生命周期:getDerivedStateFromProps、getSnapshotBeforeUpdate

一、升级React版本为17.0.1

官网地址:zh-hans.reactjs.org/docs/cdn-li…

二、即将废弃三个生命周期:componentWillMount、componentWillUpdate、componentWillReceiveProps

1、新版本的警告信息

在新的版本中,是可以使用旧版本的生命钩子的,但是会提出警告信息。

(1)componentWillMount has been renamed, and is not recommended for use.

翻译:componentWillMount钩子被重命名了,而且不被推荐使用了。

(2)Rename componentWillMount to UNSAFE_componentWillMount

翻译:componentWillMount改名为UNSAFE_componentWillMount

(3)In React 18.x, only the UNSAFE_ name will work.

翻译:在18版本里,只有写UNSAFE_这种形式才能工作。

说明:现在componentWillMount没有加UNSAFE_,仍然能正常调用,但在18版本后就必须要加UNSAFE_才能用了。

2、给componentWillMount、componentWillUpdate、componentWillReceiveProps前面加上UNSAFE_

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 卸载组件按钮的回调
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }

    // 强制更新按钮的回调
    force = () => {
      this.forceUpdate()
    }

    // 组件将要挂载的钩子
    UNSAFE_componentWillMount() {
      console.log('Count---componentWillMount')
    }

    // 组件将要更新的钩子
    UNSAFE_componentWillUpdate() {
      console.log('Count---componentWillUpdate')
    }
    
    ......

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

这3个生命钩子加上UNSAFE_之后,警告已经消失了。

总结:所有带will的钩子,在新版本里都希望你加上UNSAFE_,除了componentWillUnmount钩子。

3、详述加上UNSAFE_原因

官方文档:zh-hans.reactjs.org/blog/2018/0…

综上所述:React在为以后铺路,上面用了"预计"、"未来版本"这些词,其实就是提前打好预防针,预测这3个钩子会出问题。现在提高这3个钩子的使用成本,必须加上UNSAFE_前缀。在实际的开发中,这3个并不是核心的钩子,也有不少人说React即将要废弃这3个钩子。

官网标明在未来的版本已经给它们开启废弃的告警,我们看到的警告就是它们开启的废弃警告。在17版本以后就必须要加上UNSAFE_前缀,但实际上我们没有加前缀还是能使用,所以React在进行一些旧东西的变化时候,它是非常小心翼翼的。

三、新增两个生命周期:getDerivedStateFromProps、getSnapshotBeforeUpdate

1、对比挂载流程

旧版本挂载流程:constructor => componentWillMount => render => componentDidMount

新版本挂载流程:constructor => getDerivedStateFromProps => render => componentDidMount

总结:新版本挂载流程,对比旧版本而言,把componentWillMount生命钩子改成getDerivedStateFromProps生命钩子

2、对比更新流程

旧版本更新流程:componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate

新版本更新流程:getDerivedStateFromProps => shouldComponentUpdate => render => getSnapshotBeforeUpdate => componentDidUpdate

总结:新版本更新流程,对比旧版本而言,把componentWillReceiveProps生命钩子改成getDerivedStateFromProps生命钩子,废弃componentWillUpdate生命钩子,在render函数之后增加了getSnapshotBeforeUpdate生命钩子。

2.6.8 getDerivedStateFromProps

概要总结

1、getDerivedStateFromProps使用方法

2、getDerivedStateFromProps接收参数

3、getDerivedStateFromProps注意事项

4、使用constructor代替getDerivedStateFromProps

一、getDerivedStateFromProps使用方法

getDerivedStateFromProps生命钩子,直译就是从props得到一个派生的状态。

1、添加getDerivedStateFromProps生命钩子

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 卸载组件按钮的回调
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }

    // 强制更新按钮的回调
    force = () => {
      this.forceUpdate()
    }

    getDerivedStateFromProps() {
      console.log('getDerivedStateFromProps')
    }
    
    ......

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

这里并没有执行getDerivedStateFromProps生命钩子,反而提示一个警告:getDerivedStateFromProps方法不能给实例使用,请定义它为一个静态方法。

解决方案:在getDerivedStateFromProps前面加一个static关键字。

2、static getDerivedStateFromProps

static getDerivedStateFromProps() {
  console.log('getDerivedStateFromProps')
}

这次getDerivedStateFromProps生命钩子执行成功了,但又提示另一个告警:它必须返回一个状态对象或者null。

3、getDerivedStateFromProps的状态对象

在React里,状态就是state。而getDerivedStateFromProps要求返回的状态对象,这个状态对象实际上就是state,而且state会以这个状态对象为准,会把自己定义的state覆盖掉。

例如state与getDerivedStateFromProps返回的状态对象同时存在一个count属性:

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    static getDerivedStateFromProps() {
      console.log('getDerivedStateFromProps')
      return {count: 108}
    }
    
    ......

    render() {
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

此时状态里的count属性以getDerivedStateFromProps返回的状态对象为准,而且它无法更新这个count状态属性。因为每次更新是必须执行getDerivedStateFromProps生命钩子,而getDerivedStateFromProps又返回了一个固定的count状态值,因此无论怎么setState都无法改变。

二、getDerivedStateFromProps接收参数

1、props

getDerivedStateFromProps接收的第一个参数就是props,props参数就是在调用组件的时候传入的参数:

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    static getDerivedStateFromProps(props) {
      console.log('getDerivedStateFromProps', props)
      return props
    }
    
    ......
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>

它把接收到的props作为状态对象来返回,它就已经是当成状态来使用。简单来说,这个state不是自己写的,是从props传递过来的,所以就叫做从props得到一个派生的状态。

2、state

getDerivedStateFromProps接收的第二个参数是state,这个state是我们自己定义的state:

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    static getDerivedStateFromProps(props, state) {
      console.log('getDerivedStateFromProps', props, state)
      return props
    }
    
    ......
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>

我们可以在这里进行一些判断,例如传递过来的props的某一个属性,与state里的某一个属性进行对比,然后更新state值,最后返回一个null。那么state就可以根据props进行一些初始化的改变。

三、getDerivedStateFromProps注意事项

官方文档:zh-hans.reactjs.org/docs/react-…

四、使用constructor代替getDerivedStateFromProps

如果需要state完全取决于props,其实是可以在constructor里进行操作。constructor接收到props之后,把它赋值给state就可以了,因此getDerivedStateFromProps可以忽略。

2.6.9 getSnapshotBeforeUpdate

概要总结

1、getSnapshotBeforeUpdate使用方法

2、getSnapshotBeforeUpdate的作用

3、getSnapshotBeforeUpdate使用场景

一、getSnapshotBeforeUpdate使用方法

getDerivedStateFromProps生命钩子,直译就是在更新之前获取快照。

1、添加getSnapshotBeforeUpdate生命钩子

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      console.log('Count---constructor')
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 加1按钮的回调
    add = () => {
      // 获取原状态
      const {count} = this.state
      // 更新状态
      this.setState({count: count + 1})
    }

    // 卸载组件按钮的回调
    death = () => {
      ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
    }

    // 强制更新按钮的回调
    force = () => {
      this.forceUpdate()
    }

    getSnapshotBeforeUpdate() {
      console.log('getSnapshotBeforeUpdate')
    }
    
    ......

    render() {
      console.log('Count---render')
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

在更新的时候,已经执行了getSnapshotBeforeUpdate生命钩子,但提示一个警告:它必须返回一个快照值或者null。

2、componentDidUpdate接收参数

componentDidUpdate生命钩子能接收3个参数的,前两个参数是prevProps、prevState,意思是上一个props和上一个state。

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    // 组件更新完毕的钩子
    componentDidUpdate(prevProps, prevState) {
      console.log('Count---componentDidUpdate', prevProps, prevState)
    }
    
    ......

    render() {
      const {count} = this.state
      return (
        <div>
          <h2>当前求和为:{count}</h2>
          <button onClick={this.add}>点我+1</button>
          <button onClick={this.death}>卸载组件</button>
          <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count />, document.getElementById('test1'))
</script>

此时状态里的count属性值已经是1了,而prevState输出的count却是0,证明这确实是上一个state的状态值。

3、getSnapshotBeforeUpdate的快照值

官方文档:zh-hans.reactjs.org/docs/react-…

文档明确说明,getSnapshotBeforeUpdate的返回值会传给componentDidUpdate生命钩子,这正是对应componentDidUpdate的第三个参数:

<script type="text/babel">
  // 1.创建组件
  class Count extends React.Component {

    // 构造器
    constructor(props) {
      super(props);
      // 初始化状态
      this.state = {count: 0}
    }

    getSnapshotBeforeUpdate() {
      console.log('getSnapshotBeforeUpdate')
      return 'atguigu'
    }
    
    // 组件更新完毕的钩子
    componentDidUpdate(prevProps, prevState, snapshotValue) {
      console.log('Count---componentDidUpdate', prevProps, prevState, snapshotValue)
    }    
    ......
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Count count={199}/>, document.getElementById('test1'))
</script>

二、getSnapshotBeforeUpdate的作用

如果componentDidUpdate生命钩子执行完毕,那就意味着组件完成更新了,而在getSnapshotBeforeUpdate环节是在更新之前。在更新的时候,页面肯定要发生一些变化,一旦完成更新,那更新之前的东西就获取不到了。我们可以利用getSnapshotBeforeUpdate的快照就可以在它马上更新之前获取一些信息,就像亲戚聚餐合影拍照留念一样,而组件就在更新之前来个快照是同一个道理。

至于getSnapshotBeforeUpdate的快照值是什么,你想返回什么就是什么,根据需求返回即可。

三、getSnapshotBeforeUpdate使用场景

场景:

(1)每隔1s推送一条新闻,出现新闻列表滚动条

(2)手动滚动到某一条新闻时,不被新推送的新闻影响滚动条的位置,也就是说要把滚动条固定在这里

<script type="text/babel">
  // 1.创建组件
  class NewsList extends React.Component {

    state = {newsArr: []}

    componentDidMount() {
      setInterval(() => {
        // 获取原状态
        const {newsArr} = this.state
        // 模拟一条新闻
        const news = '新闻' + (newsArr.length + 1)
        this.setState({newsArr: [news, ...newsArr]})
      }, 1000)
    }

    render() {
      return (
        <div className="list">
          {
            this.state.newsArr.map((n, index) => {
              return <div key={index} className="news">{n}</div>
            })
          }
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<NewsList/>, document.getElementById('test'))
</script>

这里实现了1s推送一条新闻,接下来可以使用getSnapshotBeforeUpdate来动态计算推送新闻前后的滚动条差值,然后再进行对滚动条的高度进行控制,实现固定滚动条的效果。

<script type="text/babel">
  // 1.创建组件
  class NewsList extends React.Component {

    state = {newsArr: []}

    componentDidMount() {
      setInterval(() => {
        // 获取原状态
        const {newsArr} = this.state
        // 模拟一条新闻
        const news = '新闻' + (newsArr.length + 1)
        this.setState({newsArr: [news, ...newsArr]})
      }, 1000)
    }

    getSnapshotBeforeUpdate() {
      return this.refs.list.scrollHeight
    }

    componentDidUpdate(prevProps, prevState, height) {
      this.refs.list.scrollTop += this.refs.list.scrollHeight - height
    }

    render() {
      return (
        <div className="list" ref="list">
          {
            this.state.newsArr.map((n, index) => {
              return <div key={index} className="news">{n}</div>
            })
          }
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<NewsList/>, document.getElementById('test'))
</script>

思路:

(1)在getSnapshotBeforeUpdate钩子把当前的滚动条的总高度作为快照传给componentDidUpdate钩子

(2)在componentDidUpdate的列表高度减去getSnapshotBeforeUpdate传过来的高度,得到的是一条新闻记录的高度

(3)把一条新闻记录的高度加到当前滚动条的scrollTop上,这样就可以保持滚动条的位置不变了

2.6.10 总结生命周期(新)

概要总结

1、初始化阶段:由ReactDom.render()触发--初次渲染

2、更新阶段:由组件内部this.setState()或父组件render触发

3、卸载组件:由ReactDOM.unmountComponentAtNode()触发

一、初始化阶段:由ReactDom.render()触发--初次渲染

1、constructor()

2、getDerivedStateFromProps

3、render()

4、componentDidMount() =====> 常用

一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

二、更新阶段:由组件内部this.setState()或父组件render触发

1、getDerivedStateFromProps

2、shouldComponentUpdate()

3、render() =====> 必须使用的一个

4、getSnapshotBeforeUpdate

5、componentDidUpdate()

三、卸载组件:由ReactDOM.unmountComponentAtNode()触发

1、componentWillUnmount() =====> 常用

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

2.7 虚拟DOM与DOM Diffing算法

2.7.1 DOM的diffing算法

概要总结

1、验证diffing算法

2、key的作用

3、存在输入类的DOM

4、用index作为key可能会引发的问题

一、验证diffing算法

<script type="text/babel">
  class Time extends React.Component {

    state = {
      date: new Date()
    }

    componentDidMount() {
      setInterval(() => {
        this.setState({
          date: new Date()
        })
      }, 1000)
    }
    render() {
      return (
        <div>
          <h1>hello</h1>
          <input type="text" />
          <span>现在是:{this.state.date.toTimeString()}</span>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Time />, document.getElementById('test1'))
</script>

在这个案例中,date时间属性每隔1s更新一次,也就意味着1s执行一次render函数。render函数里有和里面的3个标签,每次执行render就意味着它就会生成整个的虚拟DOM。

在<div>标签中,有3个子标签,分别是、、。在生成出来新的虚拟DOM之后,它就会跟旧的虚拟DOM做一个比较,发现实际上发生改变的只有标签,所以、对应的真实DOM没有发生变化,就只有的真实DOM产生改变。

注意:它对比的最小粒度是标签,它并不能精确到标签里的某个内容的变化。例如现在是:{this.state.date.toTimeString()},它的比较只能检测到标签发生了改变,而不能精确到里面的时间发生改变。但是在标签内既有内容又有子标签,在改变内容的时候,子标签是不会发生改变的。

render() {
  return (
    <div>
      <h1>hello</h1>
      <input type="text" />
      <span>
        现在是:{this.state.date.toTimeString()}
        <input type="text" />
      </span>
    </div>
  )
}

二、key的作用

/**
 经典面试题:
   1). react/vue中的key有什么作用?(key的内部原理是什么?)
   2). 为什么遍历列表时,key最好不要用index?

   1. 虚拟DOM中key的作用:
       1). 简单的说: key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用

       2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后React进行【新的虚拟DOM】与【旧的虚拟DOM】的diff比较,比较规则如下:
              a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
                    (1).若虚拟DOM中内容没变,直接使用之前的真实DOM
                    (2).若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
              b. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
                    根据数据创建新的真实DOM,随后渲染到页面

   2. 用index作为key可能会引发的问题:
       1). 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
                会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低

       2). 如果结构中还包含输入类的DOM:
                会产生错误DOM更新 ==> 界面有问题
       3). 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

   3. 开发中如何选择key?:
       1). 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
       2). 如果确定只是简单的展示数据,用index也是可以的
 * */

案例:在数组前插入一条数据:

<script type="text/babel">
  class Person extends React.Component {

    state = {
      persons: [
        {id: 1, name: '小张', age: 18},
        {id: 2, name: '小李', age: 19}
      ]
    }

    add = () => {
      const {persons} = this.state
      const p = {id: persons.length + 1, name: '小王', age: 20}
      this.setState({persons: [p, ...persons]})
    }

    render() {
      return (
        <div>
          <h2>展示人员信息</h2>
          <button onClick={this.add}>添加一个小王</button>
          <ul>
            {
              this.state.persons.map((personObj, index) => {
                return <li key={index}>{personObj.name} --- {personObj.age}</li>
              })
            }
          </ul>
        </div>
      )
    }
  }

  // 2.渲染组件到页面
  ReactDOM.render(<Person />, document.getElementById('test1'))
</script>

1、使用index索引值作为key

(1)初始数据

{id: 1, name: '小张', age: 18}

{id: 2, name: '小李', age: 19}

(2)初始的虚拟DOM

小张 ---- 18

小李 ---- 19

(3)更新后的数据

{id: 3, name: '小王', age: 20}

{id: 1, name: '小张', age: 18}

{id: 2, name: '小李', age: 19}

(4)更新后的虚拟DOM

小王 ---- 20

小张 ---- 18

小李 ---- 19

2、旧虚拟DOM中找到了与新虚拟DOM相同的key(索引值)

我们可以发现,在新的虚拟DOM与旧的虚拟DOM当中,存在key为0和1是相同的,但通过对比发现内容是不同的,因此它们需要重新生成新的真实DOM来替换。

注意:在这里其实只是添加了一条数据,原来的两条数据是没有变化,按理是可以复用的,但由于key使用了index,而新数据偏偏又是从最前面插入,导致所有的索引值错乱,所以就产生了两条没有必要的真实DOM更新。如果数据量庞大,则会产生严重的效率问题。

3、使用id唯一标识作为key

(1)初始数据

{id: 1, name: '小张', age: 18}

{id: 2, name: '小李', age: 19}

(2)初始的虚拟DOM

小张 ---- 18

小李 ---- 19

(3)更新后的数据

{id: 3, name: '小王', age: 20}

{id: 1, name: '小张', age: 18}

{id: 2, name: '小李', age: 19}

(4)更新后的虚拟DOM

小王 ---- 20

小张 ---- 18

小李 ---- 19

4、旧虚拟DOM中找到了与新虚拟DOM相同的key(id唯一标识)

我们可以发现,在新的虚拟DOM与旧的虚拟DOM当中,存在key为1和2是相同的,而且内容也相同,因此可以复用。

注意:使用id作为key,无论新数据从哪里插入,只要是key相同内容都没有发生变化,这里只需要新建一条新的真实DOM,旧的可以完全复用。性能比index索引值作为key要高得多。

三、存在输入类的DOM

render() {
  return (
    <div>
      <h2>展示人员信息</h2>
      <button onClick={this.add}>添加一个小王</button>
      <h3>使用index(索引值)作为key</h3>
      <ul>
        {
          this.state.persons.map((personObj, index) => {
            return <li key={index}>{personObj.name} --- {personObj.age}<input type="text" /></li>
          })
        }
      </ul>
      <hr/>
      <h3>使用id(数据的唯一标识)作为key</h3>
      <ul>
        {
          this.state.persons.map((personObj, index) => {
            return <li key={personObj.id}>{personObj.name} --- {personObj.age}<input type="text" /></li>
          })
        }
      </ul>
    </div>
  )
}

当存在输入类的DOM的时候,使用index作为key会产生错乱的现象,而id为key就没问题。

1、使用index索引作为key

(1)初始的虚拟DOM

小张 ---- 18

小李 ---- 19

(2)更新后的虚拟DOM

小王 ---- 20

小张 ---- 18

小李 ---- 19

首先它拿key为0的新旧虚拟DOM做一个对比,对比后发现里面的内容并不一样,但是大家里面都有一个相同的,那么它们就会直接复用。但是万万没想到旧的真实DOM里面残留着之前的输入信息,因此React直接复用就会产生这种错乱的情况。以此类推后面的节点也会产生同样的错乱问题。

2、使用id唯一标识作为key

在这种情况下,如果使用id唯一标识作为key,就完美的避免这种问题了。

(1)初始的虚拟DOM

小张 ---- 18

小李 ---- 19

(2)更新后的虚拟DOM

小王 ---- 20

小张 ---- 18

小李 ---- 19

首先它拿key为0的新旧虚拟DOM做一个对比,对比后发现内容是一样的,因此是整个标签复用,只有key为3的节点需要重新生成,因此不会产生错乱问题。

四、用index作为key可能会引发的问题

1、若对数据进行:逆序添加、逆序删除等破坏顺序的操作

会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低

2、如果结构中还包含输入类的DOM

会产生错误DOM更新 ==> 界面有问题

3、注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的

2.8 代码地址

gitee.com/huang_jing_…