zoukankan      html  css  js  c++  java
  • 从零开始的react入门教程(二),从react组件说到props/state的联系与区别

    壹 ❀ 引

    从零开始的react入门教程(一)一文中,我们搭建了第一个属于自己的react应用,并简单学习了jsx语法。jsx写法上与dom标签高度一致,当然我们也知道,本质上这些react元素都是React.createElement()的语法糖,通过编译,bable会将其还原成最原始的样子,比如如下代码效果相同:

    <div class="echo"></div>
    // 等同于
    React.createElement(
      'div',
      {className: 'echo'}
    )
    

    至少从书写上,jsx为我们提供了极大便利。在文章结尾,我们也敲到了react元素并非组件,它可能是一个单一的标签,也可能是一个代码块,在react中有专门的方式来创建组件,那么本文就从组件说起。

    贰 ❀ 组件

    贰 ❀ 壹 函数组件

    react中的组件分为函数组件class组件,两者有一定区别,但都非常好理解。函数组件很简单,比如我们现在想复用一段简单的dom结构,但它的文本内容可能会不同,这种情况我们想到的就是文本内容是一个变量,这样就能做到dom复用的目的了,所以函数组件就是做了这样一件事:

    // 这是一个函数组件,它接受一些props,并返回组合后的dom结构
    function Echo(props) {
      return <div>听风是风又叫{props.name}</div>;
    };
    
    ReactDOM.render(<Echo name="听风是风" />, document.getElementById("root"));
    

    需要注意的是,函数组件的函数名是大写的(class组件也是如此),我们在render中使用了组件Echo,并传递了一个name属性,所有在组件上传递的属性都会被包裹在props对象中,所以通过props参数我们能访问到每一个传递给组件的属性。通过打印props可以看到它是一个对象:

    function Echo(props) {
      console.log(props);
      return <div>听风是风又叫{props.name}</div>;
    };
    

    传递的数据格式除了字符,数字,它当然也支持对象传递,比如下面这个例子运行结果与上方相同:

    const myName = {
      name:'echo'
    };
    function Echo(props) {
      console.log(props);
      return <div>听风是风又叫{props.name.name}</div>;
    };
    // 
    ReactDOM.render(<Echo name= {myName}/>, document.getElementById("root"));
    

    我们来解读下props.name.name,首先我们是将myName作为name的值传递给了组件,所以要访问到myName得通过props.name拿到,之后才是name取到了具体的值。其次需要注意的是传递对象需要使用{}包裹,如果不加花括号会有错误提示,jsx这里只支持加引号的文本或者表达式,而{myName}就是一个简单的表达式。

    我们在上文中,也有将react元素赋予给一个变量的写法,比如:

    const ele = <div>我的名字是听风。</div>
    ReactDOM.render(ele, document.getElementById("root"));
    

    其实组件也能像这样赋予给一个变量,所以看到下面这样的写法也不要奇怪:

    function Echo(props) {
      return <div>我的名字是{props.name}</div>;
    };
    // 这里将组件赋予给了一个变量,所以render时直接用变量名
    const ele = <Echo name="听风是风"/>
    ReactDOM.render(ele, document.getElementById("root"));
    

    同理,react元素可以组合成代码块,组件同样可以组合成一个新组件,比如:

    function Echo(props) {
      return <div>我的名字是{props.name}</div>;
    };
    function UserList() {
      return (
        // 注意,只能有一个父元素,所以得用一个标签包裹
        <div>
          <Echo name="echo" />
          <Echo name="听风" />
        </div>
      );
    };
    ReactDOM.render(<UserList />, document.getElementById("root"));
    

    在这个例子中,组件Echo作为基础组件,重新构建出了一个新组建UserList,所以到这里我们可以发现,组件算自定义的react元素,它可能是由react元素组成,也可能是由组件构成。

    贰 ❀ 贰 class组件

    除了上面的函数组件外,我们还可以通过class创建组件,没错,就是ES6的class,看一个简单的例子:

    class Echo extends React.Component {
      render() {
        return <div>我的名字是{this.props.name}</div>;
      }
    };
    ReactDOM.render(<Echo name="听风" />, document.getElementById("root"));
    

    由于ReactDOM.render这一块代码相同,我们把目光放在class创建上,事实上这里使用了extends继承了Component类,得到了一个新组件Echo。由于此时的Echo并不是函数,也不能接受函数形参,但事实上我们可以通过this.props直接访问到当前组件所传递的属性,让人心安的是,与函数组件相比,我们同样有方法获取外部传递的props。

    extends中的render方法是固定写法,它里面包含的是此组件需要渲染的dom结构,如果你了解过ES6的class类,除了render固有方法外,其实我们可以在这个类中自定义任何我们想要的属性以及方法,比如:

    const o = {
      a: 1,
      b: 2,
    };
    class Echo extends React.Component {
      // 这是一个自定义方法
      add(a, b) {
        return a + b;
      }
      // 这是固定方法
      render() {
        return <div>{this.add(this.props.nums.a, this.props.nums.b)}</div>;
      }
    }
    ReactDOM.render(<Echo nums={o} />, document.getElementById("root"));
    

    看着似乎有点复杂,我们来解释做了什么,首先我们在外部定义了一个包含2个数字的对象o,并将其作为nums属性传递给了组件Echo,在组件内除了render方法外,我们还自定义了一个方法add,最终渲染的文本由此方法提供,所以我们在返回的标签中调用了此方法。前面说了可以通过this.props访问到外部传递的属性,所以这里顺利拿到了函数的两个实参并参与了计算。

    那么到这里我们知道,除了一些组件固有方法属性外,我们也可以定义自己的方法用于处理渲染外的其它业务逻辑。

    举个很常见的情景,在实际开发中,有时候我们处理的组件结构会相对庞大和复杂,这时候我们就能通过功能拆分,将一个大组件在内部拆分成单个小的功能块,比如下面这段代码:

    class Echo extends React.Component {
      handleRenderTop() {
        return "我是头部";
      }
      // 自定义的render方法
      renderTop() {
        return <div>{this.handleRenderTop()}</div>;
      }
      handleRenderMiddle() {
        // dosomething
      }
      renderMiddle() {
        return <div>我是中间部分</div>;
      }
      handleRenderBottom() {
        // dosomething
      }
      renderBottom() {
        return <div>我是底部</div>;
      }
      // 官方提供的固定render方法
      render() {
        return (
          <div>
            {this.renderTop()}
            {this.renderMiddle()}
            {this.renderBottom()}
          </div>
        );
      }
    }
    ReactDOM.render(<Echo />, document.getElementById("root"));
    

    在上述代码中,假设这个组件结构和逻辑比较复杂,通过拆分我们将其分为了上中下三个部分,并创建了对应的处理方法,最终在render中我们将其组成在一起,这样写的好处是可以让组件的结构更清晰,也利于后期对于代码的维护。当然这里也只是提供了一种思路和可能性,具体做法还需要自行探索。

    其实在class组件中除了固有render方法外,还有ES6的constructor,以及组件生命周期函数,这些都是固定写法,不过我们现在不急,后面会展开说明。

    肆 ❀ props与State

    肆 ❀ 壹 基本概念与区别

    与vue双向数据绑定不同,react提供的是单向数据流,我们可以将react的数据流动理解成一条瀑布,水流(数据)从上往下流动,传递到了瀑布中的每个角落(组件),而这里的水流其实就是由props和State构成,数据能让看似静态的组件换发新生,所以现在我们来介绍下组件中的数据props与State,并了解它们的关系以及区别。

    先说props,其实通过前面的例子我们已经得到,props就是外部传递给组件的属性,在函数组件中,可以直接通过形参props访问,而在class组件中,我们一样能通过this.props访问到外部传递的属性。

    那么什么是State呢,说直白点,State就是组件内部定义的私有属性,这就是两者最直观的区别。

    State在react中更官方的解释是状态机,状态的变化会引起视图的变化,所以我们只需要修改状态,react会自动帮我们更新视图。比如下面这个例子:

    class Echo extends React.Component {
      constructor(props) {
        // 参照ES6,extends时,constructor内使用this必须调用super,否则报错
        super(props);
        this.state = { name: "echo" };
      }
      render() {
        return (
          <div>
            我的名字是{this.state.name},年龄{this.props.age}
          </div>
        );
      }
    }
    ReactDOM.render(<Echo age="27" />, document.getElementById("root"));
    

    在上述例子中,外部传递的age就是props,所以在内部也是通过this.props访问,而内部定义的属性则是通过this.state声明,一个在里一个在外,它们共同构成了组件Echo的数据。

    上述例子中constructor内部调用了super方法,这一步是必要的,如果你想在继承类的构造方法constructor中使用this,你就一定得调用一次,这也是ES6的规定,简单复习下ES6的继承:

    class Parent {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    class Child extends Parent {
      constructor(x, y, z) {
        // 本质上就是调用了超类
        super(x, y);
        this.z = z; // 正确
      }
    
      say() {
        console.log(this.x, this.y, this.z);
      }
    }
    
    const o = new Child(1, 2, 3);
    console.log(o);
    o.say(); //1,2,3
    

    首先我们定义了一个父类Parent,在它的构造方法中定义了x,y两个属性,之后我们通过extends让Child类继承了Parent,并在Child的构造方法中执行了super,这里本质上其实就是调用了父类Parent的构造函数方法,只是在执行superthis指向了Child实例,这也让Child实例o顺利继承了Parent上定义的属性。我们可以输出实例o,可以看到它确实继承了来着Parent的属性。

    super本意其实就是超类,所有被继承的类都可以叫超类,也就是我们长理解的父类,它并不是一个很复杂的概念,这里就简单复习下。

    肆 ❀ 贰 不要修改props以及如何修改props

    在上文中,我们介绍了props与State的基本作用与区别,一个组件可以在内部定义自己需要的属性,也可以接受外部传递的属性。事实上,比如父子组件结构,父组件定义的State也能作为props传递给子组件使用,只是对于不同组件它的含义不同,这也对应了上文瀑布的比喻,水流由props与State构成就是这个意思了。

    我们说State是私有属性,虽然它可以传递给其它组件作为props使用,但站在私有的角度,我虽然大方的给你用,那你就应该只使用而不去修改它,这就是props第一准则,props应该像纯函数那样,只使用传递的属性,而不去修改它(想改也改不掉,改了就报错)。

    为什么这么说,你想想,react本身就是单向数据流,父传递数据给子使用,如果在子组件内随意修改父传递的对象反过来影响了父,那这不就乱套了吗。

    那么问题来了,如果父传了属性给子,子真的要改怎么办?也不是没办法,第一我们可以在父提供props同时,也提供一个修改props的方法过去给子调用,子虽然是调用点,但本质执行的是父的方法,这是可行的。第二点,将传递进来的props复制一份,自己想怎么玩就怎么玩,也不是不可以,比如:

    function Child(props) {
      // 拷贝一份自己玩
      let num = props.num;
      num++;
      return <div>{num}</div>;
    }
    
    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { num: 1 };
      }
      render() {
        return (
          <div>
            <Child num={this.state.num} />
          </div>
        );
      }
    }
    ReactDOM.render(<Parent />, document.getElementById("root"));
    

    当然如果子组件也是class组件也可以,还是这个例子,只是修改了Child部分:

    class Child extends React.Component {
      constructor(props) {
        super(props);
        // 将传递进来的props赋予子组件的state
        this.state = {
          num:props.num
        }
      }
      render() {
        return <div>{++this.state.num}</div>;
      }
    }
    

    再或者直接赋值成this上的一条属性:

    class Child extends React.Component {
      constructor(props) {
        super(props);
      }
      // 将传递进来的props赋予给this
      num = this.props.num;
      render() {
        return <div>{++this.num}</div>;
      }
    }
    

    以上便是复制一份的常规操作,我们再来看看父提供修改方法的做法:

    class Child extends React.Component {
      render() {
        return (
          <div>
            <div>{this.props.num}</div>
            <button type="button" onClick={() => this.props.onClick()}>
              点我
            </button>
          </div>
        );
      }
    }
    
    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { num: 1 };
      }
      // 传递给子组件使用的方法
      handleClick() {
        // 拷贝了state中num
        let num_ = this.state.num;
        // 自增
        num_ += 1;
        // 更新state中的num
        this.setState({ num: num_ });
      }
      render() {
        return (
          <div>
            <Child num={this.state.num} onClick={() => this.handleClick()} />
          </div>
        );
      }
    }
    ReactDOM.render(<Parent />, document.getElementById("root"));
    

    我们来解释下这段代码,我们在Parent中定义了state,其中包含num变量,以及定义了handleClick方法,在

    <Child num={this.state.num} onClick={() => this.handleClick()} />这行代码中,我们将state中的numhandleClick分别以numonClick这两个变量名传递进去了。

    对于事件定义react有个规则,比如我们传递给子组件的变量名是on[Click],那么具体方法定义名一般以handle[click]来命名,简单点说,on[event]与handle[event]配对使用,event就是代表你事件具体含义的名字,有一个统一的规则,这样也利于同事之间的代码协作。

    在子组件内部,我们通过props能访问到传递的num与onClick这两个属性,我们将其关联到dom中,当点击按钮就会执行父组件中的handleClick方法。有同学可能注意到handleClick中更新num的操作了,按照我们常规理解,直接this.state.num++不就行了吗,很遗憾,这是react需要注意的第二点,我们无法直接修改state,比如如下行为都不被允许:

    // 直接修改不允许
    this.state.num = 2;
    // 同理,这也是直接修改了state,也不被允许
    this.setState({ num: this.state.num++ });
    

    官方推荐做法,同样也是将state中你要修改的部分拷贝出来,操作完成,再利用setState更新。

    如果你了解过vue,在深入响应式原理一文中,也有类似的要求,比如请使用Vue.set(object, propertyName, value)去更新某个对象中的某条属性,而不是直接修改它,否则你的修改可能并不会触发视图更新,其实都是差不多的道理,这里就顺带一提了。

    OK,到这里我们对于这一块知识点做个小结,props与state构成了react单向数据流的数据部分,同为属性,只是一个私有一个是从外部传递的而已。其次,props只读不可修改,若要修改请使用类似拷贝的折中方法,state除了拷贝外还得通过setState重新赋值。前面也说了,props就是外部传递的state,所以两者都不能直接修改也不是不无道理,记住这点就好了。

    伍 ❀ 总

    现在是凌晨1点半(封面图也是应景了),其实写到这里,第二部分知识我想说的也都差不多了,看了眼篇幅,四千多字,再长一些知识点可能也有点多了,所以这篇就先介绍到这里。怎么说呢,关于文章的编写我心里其实还是会有遗憾的,我毕竟只是个初学者,实战项目经验还远远不足,很多东西还不能从根源去解释清楚,比如setState可能是异步行为,所以不要用state变化作为你部分逻辑的执行判断条件,举个例子:

    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          bol: true,
        };
      }
      // 这是生命周期函数
      componentDidMount(){
        for (let i = 0; i < 5; i++) {
          if (this.state.bol) {
            console.log(i);
            this.setState({ bol: false });
          }
        }
        console.log(this.state.bol);//true
      }
      // 这也是生命周期函数
      componentDidUpdate(){
        console.log(this.state.bol);//false
      }
    
      render() {
        return null;
      }
    }
    ReactDOM.render(<Parent />, document.getElementById("root"));
    

    我们预期的是在输出i为0之后,就修改bol状态,之后循环无法再次进入这段代码,但很遗憾,for循环会完整执行完毕并输出0-1-2-3-4,直到在生命周期componentDidUpdate中我们才捕获到修改成功的状态。遗憾的是我目前的经验还不足以将这块知识吃透,没吃透的东西我不会写,所以这里算留个坑吧,之后一定会单独写一篇文章介绍state的问题,把这块弄情况,那么这篇文章就先说到这里了,本文结束。

  • 相关阅读:
    .NET简谈设计模式之(适配器模式)
    .NET简谈组件程序设计之(手动同步)
    .NET简谈组件程序设计之(初识远程调用)
    .NET简谈组件程序设计之(初识.NET线程Thread)
    .NET映射设计(Model与UIControl之间的模型关系)
    .NET简谈事务本质论
    .NET简谈组件程序设计之(异步委托)
    向5.4致敬吧 无为而为
    SQL 2005 分析服务基于角色的动态授权 无为而为
    文思创新深圳招聘biztalk 无为而为
  • 原文地址:https://www.cnblogs.com/echolun/p/13850068.html
Copyright © 2011-2022 走看看