React组件基础

261 阅读10分钟

React组件基础

React的元素与组件

React的元素与组件本质上是同一个东西,不过出现的形式不一样。

React元素:直接用const div = React.createElement创建的虚拟DOM

React组件:使用函数的形式返回React元素,const Div = ()=>React.createElement。注意组件名第一个字母要大写,这是开发者的约定俗成。

组件分类

  1. 类组件

    1. 以class的模式声明的React组件,叫做类组件。

      class myComponent extends React.Component{
          constructor(){
          }
          render(){
              return (<h1> this is myComponent</h1>)
          }
      }
      
    2. 类组件的使用方法

      首先要确保引入了组件

      // 直接使用
      ReactDOM.render(<myComponent />, document.querySeletor('#app'))
      // 组件嵌套
      function App() {
        return (
          <div className="App">
            这是爸爸,下面有一个儿子和一个孙子
            <Son />
          </div>
        );
      }
      class Son extends React.Component {
        render() {
          return (
            <div className="Son">
               这是儿子,下面还有一个孙子
              <Grandson />
            </div>
          );
        }
      }
      
  2. 函数组件

    1. 以函数的方式声明的React组件,在使用上比类组件更简洁,所以更受欢迎

      function myComponent(){
          return (<h1> this is myComponent</h1>)
      }
      
    2. 函数组件的使用方法

      和类组件的方法相同。

  3. React.createElement的逻辑

    1. <div /> <myComponent />会被翻译成什么呢?

      <div />会被翻译成 React.createElement('div')

      <myComponent />则会被翻译成React.createElement(myComponent)

      可以通过 babel翻译查询 来查看

    2. React.createElement的逻辑

      1. 如果传入的是一个字符串,则会创建一个该字符串对应的标签的React元素,如React.createElement('div')
      2. 如果传入的是一个函数,则会调用该函数,获取其返回值。如:函数组件。
      3. 如果传入的是一个类,则会通过new创建一个该类的实例(这会导致执行constructor)作为组件对象,然后调用对象的render方法,获取返回值。
  4. 组件的外部数据props

    1. 怎么传递外部数据?

      在使用组件的时候直接传递。

      <Son messageForSon="儿子你好" />
      // 完整实例
      function App() {
        return (
          <div className="App">
            爸爸
            <Son messageForSon="儿子你好" />
          </div>
        );
      }
      
    2. 类组件中使用props

      通过**this.props.[props名]**获取外部传入的数据,注意React中JS语句要使用{}包裹。

      class Son extends React.Component {
        render() {
          return (
            <div className="Son">
              我是儿子,我爸对我说「{this.props.messageForSon}」;
              <Grandson messageForGrandson="孙子你好" />
            </div>
          );
        }
      }
      
    3. 函数组件中使用props

      函数组件在声明的时候第一个参数默认就是props外部数据组成的对象,所以直接[第一个参数名].[props名]即可调用到外部参数。但是我们一般都把这第一个参数名写成props,更加语义化。

      function Grandson(haha){ // 这里使用haha作为形参,一般会叫props。
          return (
          	<div className="Grandson">
            		我是孙子,我爸对我说「{haha.messageForGrandson}」
          	</div>
          )
      }
      
  5. 内部数据state

    1. 类组件中的state

      class Son extends React.Component {
        constructor() {
          super();
          // 声明和初始化state
          this.state = {
            n: 0,
            m: 0
          };
        }
        add() {
          // 修改state
          // 思考:this.state.n += 1 为什么不行
          this.setState({ n: this.state.n + 1 });
        }
        render() {
          return (
            <div className="Son">
              // 直接在模板html中渲染state
              儿子 n: {this.state.n}
              <button onClick={() => this.add()}>+1</button>
              <Grandson />
            </div>
          );
        }
      }
      
      1. 声明和直接使用state

        使用类组件时,在声明组件的过程中,constructor里可以通过this.state = { ...}初始化内部数据,并且可以直接通过{this.state.[stateName]}渲染这些数据。

      2. 修改state

        1. 新手写法:类组件中的state需用通过类组件对象的内置方法this.setState({产生变化的数据}/函数)来修改,如:this.setState({n:this.n + 1})。这个API调用时,会新创建一个对象来覆盖掉原来的state对象。这个新创建的对象中变化的数据使用变化后的值,而没有变化的数据沿用原来的值,但是我们使用该API时只需要写变化的数据。

        2. 经验写法this.setState({产生变化的数据}/函数)这个方法是异步的,如果直接使用:this.setState({n:this.n + 1})来更新数据的话,那么我们不能马上更新 n ,必须等同步任务结束后才会更新 n ,忽视这点容易出现获取的数据都是旧的数据的问题。因此有经验的开发者会这样写:

          this.setState( (state) =>{  // 函数的第一个参数就是state对象
              	const n = state.n + 1 ;
              	// ↓之间的部分是对新数据的操作内容
              	console.log(n);
              	// ↑
              	return {n}  
          	} 
          )
          

          这样实际上我们将更新和操作新数据的步骤一起执行了,就不会出现旧数据的问题。

        3. 错误写法一:不使用this.setState,直接this.state.xxx = ...。React并不像Vue一样监听了所有的数据,直接this.state.xxx = ...是不会触发界面UI的更新的,因为React根本不知道数据发生了改变。

        4. 错误写法二:没有新创建一个对象传递给this.setState,如:

          this.state.n += 1 
          setState(this.state)
          

          注意:React强烈反对直接修改state的数据,而是推荐创建一个新的对象覆盖之前的state,因为后续的操作可能会覆盖前面的修改。

    2. 函数组件中的state

      1. const Grandson = () => {
          // 声明state
          const [n, setN] = React.useState(0);
          return (
            <div className="Grandson">
              // 直接使用state
              孙子 n:{n}
              <button onClick={() => setN(n + 1)}>+1</button>
            </div>
          );
        };
        
      2. 简单的声明和直接使用state

        函数组件的state声明过程很独特:const Div = ()=>{ const [n,setN] = React.useState(0);return (..)};我们先声明一个数组,[n,setN] 这就是我们state中的其中一个数据的2个接口,n是这个数据的读接口setN是这个数据的写接口React.useState(0)这里0就是这个数据的初始化的值。

      3. 复杂的声明

        如果state中有多个数据如何声明呢?

        // 方法一:多次声明
        const Div1 = ()=>{ 
            const [n,setN] = React.useState(0)
            const [m,setM] = React.useState(0)
        }
        
        // 方法二:一次性声明(这里不再有读写接口的概念)
        // state:包含所有state数据的对象
        // setState:能够更新所有state的接口,和类组件的this.setState类似
        // state、setState只是个名字可以自定义,但是最好还是使用这两个
        const Div2 = ()=>{ 
           const [state,setState] = React.useState({n:0,m:0})
        }
        
      4. 读写state

        函数组件中所有的数据都有读接口和写接口。

        1. 简单声明下使用state:

          // 读:直接使用读接口
          // 写:使用如:setN( n+1 )
          const Div1 = ()=>{ 
              const [n,setN] = React.useState(0)
              const add = ()=>{
                  // setN的参数是一句简单的JS语句
                  setN(n+1) // 这既用到了写接口,又用到了读接口
              }
          }
          
        2. 复杂声明下使用state:

          const Div2 = ()=>{ 
             const [state,setState] = React.useState({n:0,m:0})
             const add = ()=>{
                 // 这里的参数是一个新的state的对象了
                  setState({
                      // 特别注意这个操作
                      ...state;
                      // state.n读取n的数据
                      n:state.n+1
                  })
              }
          }
          

          复杂声明下,读取数据需要通过对象的形式state.[stateName],而更新state不再是简单声明下的直接修改,而是和类组件一样通过传递一个新的对象来覆盖旧的state,但是这里又有一个非常大的区别:注意...state这个操作。函数组件的‘setState’并没有像类组件一样对没有变化的数据沿用旧值这一操作,即传入的参数对象就是最新的state,如果新对象中没有但旧对象中有的数据,值都为undefined

          所以...state这个操作或者Object.assign把之前的state中的所有数据拷贝到新的对象中,防止数据的丢失。

      5. 类组件与函数组件关于操作state的对比以及注意点

        1. 类组件中的大多操作都涉及this,所以要清楚this的指向,而函数组件则很少涉及到this。
        2. 类组件的this.setState对没有变化的属性会自动沿用旧值,而函数组件的setState则会完全覆盖旧state对象,所以要记得...state或者Object.assign
        3. 类组件的沿用旧值的功能只在第一层数据生效。
        constructor(){
            super()
            this.state = {
                // obj是第一层数据,而obj的值则是第二层数据
                obj:{
                    name:'jack',
                    age:18
                }
            }
        }
        

        在这个例子下,this.setState({obj:{ name:'ben' }})会改变this.state.obj.namethis.state.obj.age没有改变,但是由于沿用旧值的功能只在第一层数据生效,this.state.obj.age并不会沿用旧值而是变成了undefined,整个this.state.obj的值就是{ name:'ben' }

        所以非常不建议依赖React的旧值沿用的功能,而是统统使用...语法来保证数据的稳定。

    3. 以数据为基础比较React与Vue

    4. 事件绑定

      1. 函数组件事件绑定

        const Div =  ()=>{
            const [n,setN] = React.useState(0)
            // 设置处理函数
            const add = ()=>{
                setN( n+1 )
            }
            return (
                <div>
                	{n}
                    // 函数名绑定
                    <button onClick={add}> +1 </button>
                    // 匿名函数绑定
                    <button onClick={()=>{setN(n-1)}}> -1 </button>
                </div>
            )
        }
        

        函数组件的事件绑定比较简单,可以直接使用匿名函数绑定事件,也可以通过事先声明的函数绑定事件。

      2. 类组件的事件绑定

        React对事件函数的调用是 button.onclick.addN.call(null,...)进行调用的,所以要注意this的指向。

        这里我们探究类组件的事件绑定的写法演进以及注意点

        1. 正常写法以及拓展

          class Son extends React.Component {
            constructor() {
              super();
              this.add = ()=>{...}
            }
            render() {
              return (
                <div className="Son">
                  <button onClick={() => this.add()}>+1</button>
                </div>
              );
            }
          }
          

          constructor中声明处理函数,并以onClick={() => this.add()}的匿名函数的方式传递给<button>,那么如果我们直接onClick={this.add}可以吗?

          我们知道函数中的this指向的是当前函数运行的环境

          onClick={() => this.add()}这里我们在匿名的箭头函数中调用事件函数,此时this.add()的this指向的是组件的实例,即:组件实例.add()所以add函数调用时其内部的this指向这个实例。

          onClick={this.add}中的this指向的是组件实例这点没有问题,但是之后就会出现2种情况:①执行函数以 function(){} 的形式 定义在constructor中;②执行函数以箭头函数的形式定义在constructor中。

          ①如果执行函数以 function(){} 的形式 定义在constructor中,那么onClick={this.add}事件触发时相当于是直接执行这个function,所以函数内部的this指向全局变量window

          ②如果执行函数以箭头函数的形式定义在constructor中,那么这里就涉及到箭头函数的一个特性:箭头函数的this指向在声明的时候就已经确定。所以其内部的this的指向就是组件实例。

          根据上面的推导,我们运行下列的代码可以发现onClick={this.addM}会报错,原因是Cannot read property 'm' of undefined这是因为this指向了window。解决方式:onClick={this.addM.bind(this)},但是这个函数名过于复杂,差评!

          class Son extends React.Component {
            constructor() {
              super();
              this.state = {n: 0,m: 0};
              this.addN = ()=>{
                this.setState({n:this.state.n + 1})
              }
              this.addM = function () {
                this.setState({m:this.state.m + 1})
              }
            }
            render() {
              return (
                <div className="Son">
                   // 成功运行
                  <button onClick={this.addN}>n+1</button>
                   // 运行报错
                  <button onClick={this.addM}>m+1</button>
                </div>
              );
            }
          }
          
        2. 最佳写法以及拓展

          其实最佳写法在上面的推演中已经出现了:

          class Son extends React.Component {
            constructor() {
              super();
              this.state = {n: 0};
              this.addN = ()=>{
                this.setState({n:this.state.n + 1})
              }
            }
            render() {
              return (
                <div className="Son">
                  <button onClick={this.addN}>n+1</button>  
                </div>
              );
            }
          }
          

          上面这种就是类组件绑定的事件的最佳方式:我们可以直接通过声明的函数名进行绑定,不会出现this的指向错误,以及过于复杂的函数名(this.addM.bind(this))。很多开发者在这个基础上还希望不要将函数放到constructor中,为此React特地提供了一个语法糖:

          class Son extends React.Component {
            constructor() {...}
            
            // 语法糖
            addN = ()=>{...}
            
            render() {
              return (
                <div className="Son">
                  <button onClick={this.addN}>n+1</button>  
                </div>
              );
            }
          }
          

          这个语法糖的效果就和声明在constructor中的效果一样,不过省略了this,以及不需要放在constructor中。注意:是addN = ()=>{...}的形式,而不是addN(){...}的形式。

          addN = ()=>{...}addN(){...} 的区别在于,前者是语法糖,实际上addN是声明在constructor中的,是组件实例的自有属性;而后者则是属于这个组件class的原型上的函数,而且函数的定义其实相当于addN:function(){...}的形式,其内部的this也指向的window。

        3. 总结:

          用函数组件吧。。。