React教程详解一(props、state、refs、生命周期)

文章略长,耐心读完,受益匪浅哦~

 目录

前言

简介

JSX

面向组件编程

state

props

refs

组件生命周期


前言

简介

React框架由Facebook开发,和Vue框架一样,都是用于构建用户界面的JavaScript库;

它有如下三个特点:

  1. 采用组件化模式,声明式编程,提高开发效率及组件复用率
  2. 在React Native中可以使用React语法进行移动端开发
  3. 使用虚拟DOM和优秀的Diffing算法,尽量减少与真实DOM的交互;

如下是引入react的简单尝试:

// test.html
<body>
  <!-- 引入react核心库 -->
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <!-- 引入react-dom库,用于支持react操作DOM -->
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <!-- 引入babel,用于将jsx解析为js -->
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <div id="test"></div>
  <!-- 记住这里的类型是text/babel,下面写的是jsx语法 -->
  <script type="text/babel">
    // 创建虚拟DOM
    const VDOM = (
      <div>
        <h1>React初尝试</h1>
      </div>
    )
    ReactDOM.render(VDOM, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>
</body>

JSX

JSX(JavaScript XML)是react定义的一种类似于XML的js扩展语法,用于简化创建虚拟DOM的过程;下面列出一些简单语法:

  • 定义虚拟DOM时,不要写引号;
  • 标签中混入JS表达式时要用{ };
  • 样式的类名指定要用className;
  • 内联样式,要用style={{key: value;...;}}的方式,其中最外层{}表示是js表达式,内层{}表示个对象;
  • 样式的名称要用小驼峰命名,如fontSize
  • 只有一个根标签
  • 标签必须闭合
  • 标签首字母若为小写,则会将标签转为html中同名标签;若首字母为大写,则意味着是个组件

在使用中,利用babel将jsx编译成js使用;

补充,由于只有一个跟标签,所以可以使用<Fragment></Fragement>标签或者<></>进行包裹,它俩的区别是Fragment组件可接收标签属性key,其余标签不接受;<>啥标签属性也不接受~

面向组件编程

在react中,组件分为函数式组件和类式组件;

  • 函数式组件

函数式组件即为在创建虚拟DOM时使用函数定义;

// test.html
<body>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <div id="test"></div>
  <script type="text/babel">
    // 函数式组件,名为test组件
    function Test() {
      console.log(this) // undefined,babel编译后开启了严格模式
      // 返回虚拟DOM
      return (
      <div>
        <h1>React初尝试</h1>
      </div>
    )
    }
    ReactDOM.render(<Test/>, document.getElementById('test')) // 渲染test组件到页面
  
  </script>
</body>

 执行ReactDOM.render之后,发生了什么?

  1. React解析组件标签,找到Test组件
  2. 发现Test组件是用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后渲染到页面上

在无hook之前,函数式组件中只有props这一种属性;

  • 类式组件

类式组件即为在创建虚拟DOM时使用类定义;

// test.html
<body>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <div id="test"></div>
  <script type="text/babel">
    // 创建类式组件,皆要继承于React.Component
    class Demo extends React.Component {
      render() {
        return (
          <div>
            <h1>React初尝试</h1>
          </div>
        )
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>
</body>

其中render函数是定义在Demo组件的原型对象对象上,供实例使用;

执行了ReactDom.render之后发生了什么?

  1. React解析组件标签,找到Demo组件;
  2. 发现组件是类式组件,随后React自动new出来该类的实例,并通过该实例调用原型上的render方法;
  3. 将render方法返回的虚拟DOM转为真实DOM,随后渲染到页面上

类式组件上有三大属性:state,props,refs,下面分别进行介绍;

state

state是由多个key-value组成的对象;通过更新组件的state来更新页面(重新渲染页面);

state中的数据不能被直接更改,要通过调用setState({key: value})方法来更改;

如下案例为通过改变state中的workDay属性渲染页面显示工作日/休息日:

<body>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <div id="test"></div>
  <script type="text/babel">
    class Demo extends React.Component {
      // 构造函数传参为props,在react中创建类组件时,若写构造函数,则要写super,否则会引起内部bug
      constructor(props) {
        super(props)
        this.state = {workDay: true} // 设置状态
        // 未避免调用changeDay方法时this指向为undefined,因此利用bind将this指向变为实例对象,并将改变this指向后的函数赋值给实例对象的change属性
        this.change = this.changeDay.bind(this)
      }
      render() {
        return (
          <div>
            <h1 onClick={this.change}>今天是{this.state.workDay? '工作日': '休息日'}</h1>
          </div>
        )
      }
      // 该方法原本是给Demo类的实例对象使用的,若作为事件的回调函数,则this指向为undefined(babel为严格模式)
      changeDay() {
        this.setState({workDay: !this.state.workDay}) // 要利用setState方法修改state值
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>
</body>

 如注释所说,若利用类组件方法创建组件,若在类中写了构造函数,则接收props参数的同时要写上super(props),以防止报错~

在构造函数中可以创建state对象,用以控制页面显示~

在定义事件函数时,若写在类中作为普通函数(则该方法如changeDay,只有作为类的实例对象被调用时,this才有值,即为该实例对象),作为事件被调用时,此时this为undefined。为解决此问题,可以在构造函数中改变this指向并赋值给另一属性,则此时的this就是类的实例对象,则可以正常被使用~

可以看到Demo实例上有change方法,原型对象上有changeDay方法;

备注:此处看不懂的要去复习ES6哦 JavaScript高级教程(面向对象编程)_迷糊的小小淘的博客-CSDN博客

修改state属性时,不能采用直接赋值的形式,要调用原型对象中的方法setState实现;

当然,state方法还有简单写法(不通过构造函数的方式),因为在继承类中,可省略构造函数constructor,同时若在类中直接写赋值语句,也可以将该属性给每个实例对象使用~所以可以将state直接写在类中,将事件回调函数用属性赋值的方式进行定义;

<script>
    class Person {
      constructor(name, age) {
        this.name = name
        this.age = age
      }
      workerType = '程序媛'
      say = function(){
        console.log(`我的工作是${this.workerType}`)
      }
    }
    let person = new Person('人类', 100)
    console.log(person)
    person.say()
</script>

因此用在此例中如下:

  <script type="text/babel">
    // 创建类式组件,皆要继承于React.Component
    class Demo extends React.Component {
      state = {workDay: true} // 设置状态
      change = () => {
        console.log(this); // 此处一定要写成箭头函数形式, this的指向才为实例对象
        this.setState({workDay: !this.state.workDay})
      }
      render() {
        return (
          <div>
            <h1 onClick={this.change}>今天是{this.state.workDay? '工作日': '休息日'}</h1>
          </div>
        )
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>

修改state不能采用直接赋值的方法,要使用setState方法,该方法有两种使用方式:

setState(stateChange, [callback])  // 方法一 对象式更新状态
// stateChange为状态改变的对象[该对象可以体现出状态的更改]
// callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
setState(updater, [callback])  // 方法二 函数式更新状态
// updater为返回状态改变的对象[该对象可以体现出状态的更改]的函数,其参数可接收到state和prop
// callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用

 两种改变state状态的方式,都可以接收到一个回调函数,该回调函数是异步的,在该回调里才能拿到state的最新状态~

同时,该回调还有特殊之处:即使利用shouldComponentUpdate组织了状态/视图的更新,该回调函数依然会被触发执行;

关于state的总结如下:

  • state可定义在构造函数中,也可利用赋值语句方法赋值;
  • state不可直接修改,需要调用原型对象中的setState方法(对象式方法与组件式方法);
  • 类组件中的render方法this指向组件实例对象(由react自动创建出来),组件初始化时会被渲染一次,随后在每次组件更新时被调用;
  • 类组件中的构造函数只会被调用一次;
  • 类组件中普通函数定义改变this指向有两种方法:

                ① 在构造函数中利用bind等方法改变this指向;

                ② 利用箭头函数+函数赋值的方式

props

        prop用于存放组件中的标签属性;props用于存放所有组件中的标签属性;

<script type="text/babel">
    class Demo extends React.Component {
      render() {
        return (
          <ul>
            <li>{this.props.name}</li>
            <li>{this.props.age}</li>
            <li>{this.props.grade}</li>
          </ul>
        )
      }
    }
    ReactDOM.render(<Demo name="小红" age="18" grade="100"/>, document.getElementById('test')) // 渲染虚拟DOM到页面
    const properties = {name: '小明', age: '19', grade: "90"}
    ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面
  </script>

复用组件时,利用props可传入不同的标签属性用于展示~

可以限制属性的类型,默认值及是否必输特性,需引入prop-types库;通过给类组件的propTypes和defaultProps进行赋值;

注意:

自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types 库 代替。

<body>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <!-- 引入prop-types库 -->
  <script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>

  <div id="test"></div>
  <div id="test1"></div>
  <script type="text/babel">
    class Demo extends React.Component {
      render() {
        const {name, age, grade} = this.props
        return (
          <ul>
            <li>{name}</li>
            <li>{age}</li>
            <li>{grade}</li>
          </ul>
        )
      }
    }
    Demo.propTypes = {
      name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项
      age: PropTypes.number, // 设置age为数字类型
      grade: PropTypes.number // 设置grade为数字类型
    }
    Demo.defaultProps = {
      age: 18, // 设置age默认值为18
      grade: 90 // 设置grade默认值为90
    }
    ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面
    const properties = {name: '小明', age: 19}
    ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面
  </script>
</body>

当然,也可以通过static关键字对其进行限制;

<script type="text/babel">
    class Demo extends React.Component {
      static propTypes = {
        name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项
        age: PropTypes.number, // 设置age为数字类型
        grade: PropTypes.number // 设置grade为数字类型
      }
      static defaultProps = {
        age: 18, // 设置age默认值为18
        grade: 90 // 设置grade默认值为90
      }
      render() {
        const {name, age, grade} = this.props
        return (
          <ul>
            <li>{name}</li>
            <li>{age}</li>
            <li>{grade}</li>
          </ul>
        )
      }
    }
    ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面
    const properties = {name: '小明', age: 19}
    ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面
  </script>

函数式组件也可以使用props,通过给函数传参的方式拿到props:

<body>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <!-- 引入prop-types库 -->
  <script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>

  <div id="test"></div>
  <div id="test1"></div>
  <script type="text/babel">
    function Demo(props) {
      const {name, age, grade} = props
      return <ul>
              <li>{name}</li>
              <li>{age}</li>
              <li>{grade}</li>
            </ul>
    }
    Demo.propTypes = {
      name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项
      age: PropTypes.number, // 设置age为数字类型
      grade: PropTypes.number // 设置grade为数字类型
    }
    Demo.defaultProps = {
      age: 18, // 设置age默认值为18
      grade: 90 // 设置grade默认值为90
    }
    ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面
    const properties = {name: '小明', age: 19}
    ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面
  </script>
</body>

对props的总结如下:

  • 组件标签的所有属性都保存在props中
  • props属性是可读的,不能对其修改(会报错)
  • 可以利用propTypes以及defaultProps对属性类型、是否必输、默认值进行设置

refs

利用refs来表示某标签节点本身,避免操作dom获得标签;有三种形式:

  • 通过字符串赋值方式
<input ref="input">{name}</input>
<script type="text/babel">
    class Demo extends React.Component {
      state = {age: 17}
      addAge = () => {
        const newage  = Number(this.refs.span.innerText) + 1 // 利用this.refs.span可以拿到span节点
        this.setState({age: newage})
      }
      render() {
        return (
          <div>
            <span ref="span">{this.state.age}</span><br/>
            <button onClick={this.addAge}>加一</button>
          </div>
        )
      }
    }
    ReactDOM.render(<Demo name="小红" age={10}/>, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>

该方法使用简单,但是要少用(有bug,以后可能会被弃用);

  • 回调函数方式的ref定义

可以在定义ref使用回调函数,传入的参数恰恰是该节点本身;将该参数赋值给其它变量获取即可;

<input ref={ (ele) => {this.input1 = ele} }>{name}</input>
 <script type="text/babel">
    class Demo extends React.Component {
      state = {age: 17}
      addAge = () => {
        const newage  = Number(this.span.innerText) + 1 // 利用this.refs.span可以拿到span节点
        this.setState({age: newage})
      }
      render() {
        return (
          <div>
            <span ref={(ele) => {this.span = ele}}>{this.state.age}</span><br/> 
            <button onClick={this.addAge}>加一</button>
          </div>
        )
      }
    }
    ReactDOM.render(<Demo name="小红" age={10}/>, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>

利用回调函数方式定义ref时,在更新过程中会被执行两次:一次传入参数为null,第二次才是正常的节点元素,这是因为在每次更新时会创建一个新的函数实例,所以React清空旧的ref并设置新的,当然该差别在开发中没啥影响;见官网说明:Refs and the DOM – React

  • createRef创建ref容器

Refs是使用React.createRef()创建的,并通过ref属性附加到React元素。在构造组件时,通过将Refs分配给实例属性,以便可以在整个组件中引用它们;创建一个标记一个,React.createRef()与标签是一对一关系;使用时须通过.current拿到该节点元素;

 <script type="text/babel">
    class Demo extends React.Component {
      state = {age: 17, age1: 19}
      myRef = React.createRef() // 创建标记加的span节点
      myRef1 = React.createRef() // 创建标记减的span节点
      addAge = () => {
        const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点
        this.setState({age: newage})
      }
      subAge = () => {
        const newage  = Number(this.myRef1.current.innerText) - 1 // 利用this.myRef1.current可以拿到span节点
        this.setState({age1: newage})
      }
      render() {
        return (
          <div>
            <span ref={this.myRef}>{this.state.age}</span><br/>
            <button onClick={this.addAge}>加一</button><br/>
            <span ref={this.myRef1}>{this.state.age1}</span><br/> 
            <button onClick={this.subAge}>减一</button>
          </div>
        )
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
 </script>

对于refs的总结:

有三种方式使用refs

  • 字符串形式的ref
<input ref="input">{name}</input>
  • 回调形式的ref
<input ref={ (ele) => {this.input1 = ele} }>{name}</input>
  • 使用React.createRef()创建
myRef = React.createRef()
<input ref={ this.myRef }></input>

节点元素需通过this.myRef.current拿到;

在开发中,要尽量减少对ref的使用,可以通过event.target拿到发生事件的DOM元素;

上述三种方式对于不同目标来说,所取到的值也分别有所不同:

对于元素标签设置ref,则可以取到该元素标签对应的dom元素;

而对于类组件来说,设置ref可以拿到该类组件的实例对象,后续可以使用该对象中的内容;

对应函数组件来说,设置ref会报错,需要配合React.forwordRef使用(在不使用hook之前)

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
import React from 'react';
const Test = React.forwardRef(function Test(props, ref){
  return <div ref={ref}>xiaoxiao</div>
})
class Vote extends React.PureComponent {
   render() {
    return (
      <>
        <Test ref={ele => this.tip = ele}></Test>
      </>
    )
   }
  componentDidMount() {
    console.log(this.tip, 'this.tip是啥');
  }
}

export default Vote

组件生命周期

组件生命周期是指组件对象从创建到死亡会经历一些特定阶段。又称为组件生命钩子; 

  图片来源React lifecycle methods diagram

 上图展示了reactV16.4以后所有的生命周期函数,

  • 组件挂载时,依次会执行类的构造函数construtor、getDerivedStateFromProps、render函数及componentDidMount

组件挂载通过ReactDOM.render()触发;

static getDerivedStateFromProps(props, state)

 getDerivedStateFromProps会在调用render方法之前被调用,它应返回一个对象来更新state,若返回null则表示不更新任何内容(不写该函数表示返回null)。该函数适用于state的值在任何时候都取决于props。

componentDidMount()方法会在组件挂载后(插入到DOM树中)立即调用。一般在此阶段发送网络请求、开启定时器、订阅消息等;

<script type="text/babel">
    class Demo extends React.Component {
      state = {age: 17}
      myRef = React.createRef() // 创建标记加的span节点
      addAge = () => {
        const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点
        this.setState({age: newage})
      }
      render() {
        return (
          <div>
            <span ref={this.myRef}>{this.state.age}</span><br/>
            <button onClick={this.addAge}>加一</button><br/>
          </div>
        )
      }
      static getDerivedStateFromProps() {
        console.log('getDerivedStateFromProps')
        return {age: 21} 
// 此处返回state对象中的age为21时,则不论点多少次按钮,age都不会被改变; 若返回null,则age会被修改
      }
      componentDidMount() {
        console.log('componentDidMount') // 在此处设置定时器,发送请求,订阅消息等
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>
  • 组件更新时,依次执行getDerivedStateFromProps、shouldComponentUpdate、render、getSnapShotBeforeUpdate、componentDidUpdate

组件更新由组件内部setState或者父组件更新触发;

shouldComponentUpdate()返回布尔值,用以判断React组件的输出是否受当前state或props更改的影响,默认值是true,表示state每次发生变化组件都会重新渲染;该方法在首次渲染或使用forceUpdate()时不会被调用;但是官网建议慎用哦~

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate()方法在最近一次渲染输出(提交到DOM节点)之前调用。它使得组件能在发生更改之前从DOM中捕获一些信息,如滚动位置。此方法的任何返回值都将作为参数传递给componentDidUpdate();该方法并不常用;

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate()方法会在组件更新后立即调用。首次渲染不会执行该方法。如果组件实现了getSnapshotBeforeUpdate()方法,则它的返回值将作为第三个参数传递给componentDidUpdate(),否则该参数为undefined;

  <script type="text/babel">
    class Demo extends React.Component {
      state = {age: 17}
      myRef = React.createRef() // 创建标记加的span节点
      addAge = () => {
        const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点
        this.setState({age: newage})
      }
      render() {
        return (
          <div>
            <span ref={this.myRef}>{this.state.age}</span><br/>
            <button onClick={this.addAge}>加一</button><br/>
          </div>
        )
      }
      static getDerivedStateFromProps(props, state) {
        console.log('getDerivedStateFromProps')
        return props
      }
      componentDidMount() {
        console.log('componentDidMount')
      }
      shouldComponentUpdate() {
        console.log('shouldComponentUpdate')
        return true
        // 此处设为false则点击按钮不会被更新,下面两个方法就不会被执行,点击true则正常更新
      }
      getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log('getSnapshotBeforeUpdate')
        return 123
      }
      componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('componentDidUpdate')
        return 456
      }
    }
    ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面
  </script>
  • 组件卸载时,执行componentWillUnmount()

组件卸载通过ReactDOM.unmountComponentAtNode触发

componentWillUnmount()用在组件卸载及销毁之前直接调用,一般在此阶段执行必要的清理操作,如清除定时器、取消网络请求或清除在componentDidMount()中创建的订阅;

下图展示了常用的声明周期函数,在日常开发中经常被使用。

图片来源React lifecycle methods diagram

备注:旧版本中有几个弃用的声明周期函数,如 componentWillUnmount() 、componentWillUpdate() 、componentWillReceiveProps(),了解即可~


To be continue~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷糊的小小淘

整理不易,赏点动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值