Reactjs 踏坑指南2: JSX&&组件

JSX 知识准备

JSX 并不是一门全新的语言,仅仅是一个语法糖,允许开发者在javascript中编写XML语言。这样使用JavaScript来构建组件以及组件之间关系的应用,在代码层面显得更加清晰,而不再是使用JavaScript操作DOM来创建组件以及组件之间的嵌套关系。

作为是React的核心部分,JSX使用XML标记的方式直接声明页面。在JavaScript代码里直接写XML的语法,每一个XML标签都会被JSX转换工具转换成纯JavaScript代码。HTML直接写到JavaScript中不加任何分号,这就是JSX的语法奥义

JSX环境准备

JSX必须借助ReactJS环境才能运行。在编写JSX代码之前,需要先加载ReactJS文件,比react.js.光有ReactJs还不行,还需要加载JSX解析器。下面大家一起准备好一个最基础的ReactJS环境

  • 创建一个test.html文件

  • <head>标签中引入如下文件

      <script src="http://cdn.bootcss.com/react/15.2.0/react.js"></script>
      <script src="http://cdn.bootcss.com/react/15.2.0/react-dom.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
    

这样环境就搭建好了,然后按照惯例,双手奉上一个Hello World 程序。

  <body>
    <div id="example"></div>
    <script type="text/babel">
      var HelloComponent = React.createClass({
        render: function(){
          return <span>Hello World !</span>
        }
      });
      
      ReactDOM.render(
        <HelloComponent />,
        document.getElementById('example')
      );
    </script>
  </body>

运行结果

如上面代码,我们在JavaScript中书写HTML标签时,不再像以前那样作为字符串用引号引起来,而是想在XML文件中书写一样,直接写即可。

除了<span>Hello World !</span>这种直接使用的HTML标签外,还有一个<HelloComponent />标签。这个是ReactJS创建的组件类标签,通过这种方式,把创建的HelloComponent组件引用进来。ReactJS约定,自定义的组件标签首字母一定要大些,用来区分是组件标签还是HTML标签。

注意几点

  • script 标签的 type 属性为 text/babel,这是React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是在页面中直接使用 JSX 的地方,都要加上 type="text/babel"。
  • 一共用了三个库: react.js 、react-dom.js 和 browser.min.js ,它们必须首先加载。其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能, browser.min.js的作用是将 JSX 语法转为 JavaScript 语法。
  • 将 JSX 语法转为 JavaScript 语法,这一步很消耗时间。现在前端项目,都会使用前端工程化,不会直接在html页面中直接写js代码,写好的js代码都会使用工具进行编译压缩等。这样的话,我们的jsx也会通过编译直接转化成js语法,让浏览器直接使用。

JSX执行JavaScript表达式

JSX使用{}来执行JavaScript表达式。

var arr = [
 <h1>Hello world!</h1>,
 <h2>React is awesome</h2>,
];
ReactDOM.render(
 <div>{arr}</div>,
 document.getElementById('example')
);

JSX定义属性&&样式使用

HTML中可以通过标签上的属性来改变当前元素的样式,当然,这在JSX中也是可以的,只不过JSX在使用行内样式的时候是有缺陷的。。使用{{}}而不是引号的方式,如下

React.render(
    <div style={{color:'red',marginTop:'20px'}}>
        xxxxx
    </div>,
    document.body
);

注:直接在标签上使用style属性时,要写成style={{}}是两个大括号,外层大括号是告知jsx这里是js语法,和真实DOM不同的是,属性值不能是字符串而必须为对象,需要注意的是属性名同样需要驼峰命名法。即margin-top要写成marginTop,属性之间以逗号间隔。还有class 属性需要写成 className ,for 属性需要写成 htmlFor ,这是因为 class 和 for 是 JavaScript 的保留字。

JSX中的延展属性

在JSX中我们可以使用ES6中的最新语法...来遍历对象

  var HelloMessage = React.createClass({
    render: function() {
      return <h1>Hello {this.props.name}  get {this.props.votes} votes</h1>;
    }
  });

  var Lucy = {
    name: "feng",
    votes: "23"
  }

  ReactDOM.render(
  <HelloMessage {...Lucy} />,
    document.getElementById('example')
  );

JSX中的事件绑定

JSX支持事件的绑定,语法类似于HTML中事件的绑定,不同的是这里事件的名称必须按照驼峰式,下面给个例子大家参考一下。

var HelloComponent = React.createClass({
    testClick: function (str) {
      alert('hello ' +  str)
    },
    render: function() {
      return (
          <p
          onClick={this.testClick.bind(this, 'feng')} style={{olor:'#ff6600',width:'200px', height:'100p'}}
          >
          Click me
          </p>
          )
        
    }
  })

  
  ReactDOM.render(
  <HelloComponent  />,
    document.getElementById('example')
  );

知晓了JSX的基本使用之后,开始介绍ReactJS的组件的概念和使用方法

组件初步

ReactJS的基础就是实例化,即按功能封装成一个又一个的组件,各个组件维护自己的状态和UI,当状态改变时,会自动重新渲染整个组件,多个组件一起协作构成了整个ReactJS应用。

以一个简单的Hello World开始

<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="http://cdn.bootcss.com/react/15.2.0/react.js"></script>
    <script src="http://cdn.bootcss.com/react/15.2.0/react-dom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
<head>
<body>
    <div id="example"></div>
    <script type="text/babel">
          var HelloMessage = React.createClass({
            render: function() {
                  return <div>Hello,{this.props.name}</div>
            }
          })
          ReactDOM.render(<HelloMessage name="World" />, document.getElementById('example'))
    </script>
</body>

运行结果:

运行结果

审查元素发现DOM结构如下:

上面的代码中,我们调用React提供的工厂方法来创建组件,其中:

  • React是全局对象,ReactJS的所有顶级API都挂在这个对象下。
  • React.createClass是ReactJS用来创建组件类的方法,这个对象必须包含一个render方法和若干可选的生命周期(详情请见下文)方法,其中
  • render是一个组件级的API,是React.createClass内部最常用的API,该方法主要用来返回组件结构
  • React.render(这里我用的是ReactDOM.render)是最常用的方法,用于将模板转换成HTML语言,并将转换后的DOM结构插入到制定的DOM节点上
  • 简而言之就是:先通过React.createClass来创建一个组件类,调用组件的render方法输出组件的DOM结构,最后调用React.render将组建插入到id为example的节点上

这里我们需要保证render函数是纯函数:即同样的输入始终返回相同的输出,并且执行过程中没有副作用(和DOM交互或者发Ajax请求)。但一个组件要和DOM交互或者发Ajax请求需求是很正常的,那么就要用到其他生命周期方法了。

组件的状态与属性

组件本质上是状态机,输入确定,输出一定确定。组件把状态与结果一一对应起来,组件中有state与prop(状态与属性)。

  • 属性(props)是由父组件传递给子组件的;
  • 状态(state)是子组件内部维护的数据,当状态发生变化的同时,组件也会进行更新。当状态发生转换时会触发不同的钩子函数(详见下文中生命周期),从而让开发者有机会做出相应。

props属性的用法

props是一个对象,是组建用来接收外面传来的参数,组件内部是不允许修改自己的props属性的,只能够通过父组件来修改。具体的使用方法如下

  • 接收键值对数据

    <HelloWorld name= ? />

    1. 字符串:"XiaoFeng"

    2. 求值表达式 {123}、{"XiaoFeng"}

    3. 数组{[1,2,3]}

    4. 变量{variable}

    5. 函数求值表达式{function}(不推荐,如果需要函数可以单独把函数提取出来然后单独调用函数)

       var HelloWorld =React.createClass({
        render:function () {
             console.log(this.props)
            return <p>Hello,{this.props.name ? this.props.name : " World"}</p>;
        },
       });
       var HelloUniverse = React.createClass({
          getInitialState:function () {
              return {
                  name:''
              };
          },
          handleChange: function (event) {
              this.setState({name: event.target.value});
          },
          render: function () {
              return (<div>
              <HelloWorld name={this.state.name}></HelloWorld>
              <br/>
              <input type="text" onChange={this.handleChange} />
              </div>
              );
          }
       });
       ReactDOM.render(<HelloUniverse />,example);
      
  • setProps

    已过时,即将废弃

  • propTypes

    组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求。

      var MyTitle = React.createClass({
        propTypes: {
          title: React.PropTypes.string.isRequired,
        },
        render: function() {
           return <h1> {this.props.title} </h1>;
         }
      });
    

    上面的Mytitle组件有一个title属性。PropTypes 告诉 React,这个 title 属性是必须的,而且它的值必须是字符串。现在,我们设置 title 属性的值是一个数值。

      var data = 123;
      ReactDOM.render(
        <MyTitle title={data} />,
        document.body
      );
    

    这样一来,title属性就通不过验证了。控制台会显示一行错误信息

  • getDefaultProps

    getDefaultProps 方法可以用来设置组件属性的默认值

      var MyTitle = React.createClass({
        getDefaultProps : function () {
          return {
            title : 'Hello World'
          };
        },
      
        render: function() {
           return <h1> {this.props.title} </h1>;
         }
      });
      ReactDOM.render(<MyTitle />,document.body);
    
  • this.props.children

    this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点

      var NotesList = React.createClass({
        render: function() {
          return (
            <ol>
            {
              React.Children.map(this.props.children, function (child) {
                return <li>{child}</li>;
              })
            }
            </ol>
          );
        }
      });
      ReactDOM.render(
        <NotesList>
          <span>hello</span>
          <span>world</span>
        </NotesList>,example
      );
    

    上面代码的 NoteList 组件有两个 span 子节点,它们都可以通过 this.props.children 读取。这里需要注意, this.props.children 的值有三种可能:

    1. 如果当前组件没有子节点,它就是 undefined;
    2. 如果有一个子节点,数据类型是 object;
    3. 如果有多个子节点,数据类型就是 array

    React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。

state属性的用法

React把组件当成状态机,一旦用户交互导致状态发生变化,就会触发重新渲染UI。this.state会随着用户的交互而发生变化。

  • 工作原理

    常用的通知 React 数据变化的方法是调用 setState(data, callback)。这个方法会合并(merge) data 到 this.state,并重新渲染组件。渲染完成后,调用可选的 callback 回调。大部分情况下不需要提供 callback,因为 React 会负责把界面更新到最新状态。

  • getInitialState

    object getInitialState()

    getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。在组件挂载之前调用一次。返回值将会作为 this.state 的初始值。

  • setState

    setState(object nextState[, function callback])

    this.setState 方法用于修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。

    注意:

    1. 绝对不要直接改变 this.state,因为在之后调用 setState() 可能会替换掉你做的更改。把 this.state 当做不可变的。
    2. setState()不会立刻改变 this.state,而是创建一个即将处理的state转变。在调用该方法之后获取 this.state 的值可能会得到现有的值,而不是最新设置的值。
    3. 不保证 setState()调用的同步性,为了提升性能,可能会批量执行state转变和DOM 渲染。
    4. setState ( ) 将总是触发一次重绘,除非在 shouldComponentUpdate()中实现了条件渲染逻辑。如果使用可变的对象,但是又不能在shouldComponentUpdate()中实现这种逻辑,仅在新 state 和之前的 state 存在差异的时候调用 setState()可以避免不必要的重新渲染。

    常用的模式是创建多个只负责渲染数据的无状态(stateless)组件,在它们的上层创建一个有状态(stateful)组件并把它的状态通过 props 传给子级。这个有状态的组件封装了所有用户的交互逻辑,而这些无状态组件则负责声明式地渲染数据。

组件的生命周期

生命周期各阶段

在整个ReactJS的声明周期中,主要会经历这四个阶段:创建阶段、实例化阶段、更新阶段、销毁阶段。下面通过一段代码,来深入了解组件各个环节的运作流程

  var OneComponent = React.createClass({
    //1.创建阶段
    getDefaultProps: function() {
      //在创建类的时候被调用
      console.log('getDefaultProps');
      return {}
    },

    //2.实例化阶段
    getInitialState: function() {
      //获取this.state的默认值
      console.log('getInitialState');
      return {};
    },
    componentWillMount: function() {
      //在render之前调用此方法
      //业务逻辑的处理都应该放在这里,比如对state的操作等
      console.log('componentWillMount')
    },
    render: function() {
      //渲染并返回一个虚拟DOM
      console.log('render');
      return (
          <div> hello <b> {this.props.name} </b></div>
        )
    },
    componentDidMount: function() {
      //该方法发生在render方法之后
      //在该方法中,ReactJS会使用render方法返回的虚拟DOM对象来创建真实DOM结构
      console.log('componentDidMount');
    },

    //3.个更新阶段
    componentWillReceieveProps: function() {
      //该方法发生在this.props被修改或富组件调用setProps()方法之后
      console.log('componentWillRecieveProps');
    },
    shouldComponentUpdate: function() {
      //是否需要更新
      console.log('shouldComponentUpdate');
      return true;
    },
    componentWillUpadate: function() {
      //将要更新
      console.log('componentWillUpadate');
    },
    componentDidUpdate: function() {
      //更新完毕
      console.log('componentDidUpdate');
    },
    //4.销毁阶段
    componentWillUnmout: function() {
      //销毁时被调用
      console.log('componentWillUnmout');
    }
  })

  
  ReactDOM.render(
  <OneComponent  name="World "/>,
    document.getElementById('example')
  );

创建阶段:

该阶段主要发生在创建组件类的时候,即在调用React.createClass的时候。这个阶段只会触发一个getDefaultProps方法,该方法会返回一个对象,并缓存下来。然后与富组件制定的props对象合并,最后赋值给this.props作为该组件的默认属性。

实例化阶段

该阶段主要发生在实例化组件类的时候,也就是组件类被调用的时候。

  ReactDOM.render(
  <OneComponent  name="World "/>,
    document.getElementById('example')
  );

该组件被调用的时候,这个阶段会触发一系列的流程,具体的执行顺序如下所示。

  • getInitialState。初始化组件的state的值,其返回值会赋值给组件的this.state属性。

  • componentWillMount 在渲染(挂载)之前调用一次。

    此时this.refs对象为空。如果在此时调用this.setState()则会更新this.state对象,而render只会调用一次。

  • render 根据state的值,生成页面需要的虚拟DOM结构,并返回该结构

  • componentDidMount 在渲染之后调用一次。根据虚拟DOM结构而生成的真实DOM进行相应的处理,组件内部可以通过this.getDOMNode()来获取当前组件的节点,然后就可以像在web开发中那样操作里面的DOM元素了。

      componentDidMount () {
        const textbox = React.findDOMNode(this.refs.text)
        if (this.props.autoFocus) textbox.focus()
      }
    

更新阶段

主要发生在用户操作或者组件有更新的时候,此时会根据用户的操作行为进行相应的页面结构的调整。该阶段也会触发一系列的流程,具体的执行顺序如下所示。

  • componentWillReceiveProps 在将要接收props时调用。在该函数中,通常可以调用this.setState方法来完成对state的修改。

    props是父组件传递给资组建的。父组件发生render的时候子组件就会调用componentWillReceiveProps(不管props有没有更新,也不管父子组件之间有没有数据交换)。

      componentWillReceiveProps (nextProps) {
        if (this.props.disabled !== nextProps.disabled) {
      // disabled这个属性改变了
        }
      }
    
  • shouldComponentUpdate:该方法用来拦截新的props或state,然后根据事先设定好的判断逻辑,返回值决定组件是否需要update。

    组件挂载之后,每次调用setState后都会调用shouldComponentUpdate判断是否需要重新渲染组件。默认返回true,需要重新render。在比较复杂的应用里,有一些数据的改变并不影响界面展示,可以在这里做判断,优化渲染效率。

      shouldComponentUpdate (nextProps, nextState) {
          // 比较props或者states,返回true则更新照常,返回false则取消更新,且不会调用下面的两个生命周期函数
      }
    
  • componentWillUpdate :组件更新之前调用一次。当上一部中shouldComponentUpdate方法返回true时候,就可以在该方法中做一些更新之前的操作。

  • render 根据一系列的diff算法,声称需要更心动额虚拟DOM数据。实际表明,在render中,最好只做数据和模板的结合,而不进行state等逻辑的修改,这样组件结构更清晰。

  • componentDidUpdate 组件的更新已经同步到DOM中后出发。

    除了首次render之后调用componentDidMount,其它render结束之后都是调用componentDidUpdate。

销毁阶段

  • componentWillUnmount 会在组件即将从挂载点移去时调用,用来取出去除即将被销毁的DOM节点的引用,或者是清除计时器,取消监听的时间等等。

注意:

绝对不要在componentWillUpdate和componentDidUpdate中调用this.setState方法,否则将导致无限循环调用。

componentWillMount、componentDidMount和componentWillUpdate、componentDidUpdate可以对应起来。区别在于,前者只有在挂载的时候会被调用;而后者在以后的每次更新渲染之后都会被调用。

组件生命周期流程图

图片来自网络文章

组件之间的通信

先创建一个父类组件Parent,它内部调用一个叫做Child的子组件,并将接收到的外部参数name传递给子组件Child。

var Parent = React.createClass({
    handleClick: function() {
      this.refs.myTextInput.focus();
    },
    render: function() {
      return (
          <div onClick={this.handleClick}>
            <input type="text" ref="myTextInput" />
            Parent is:
              <Child name={this.props.name}></Child>
          </div>
        )
    }
});

在创建一个子类组件Child

  var Child = React.createClass({
    render: function() {
      return <span>{this.props.name}</span>
    }
  });

自后通过React.render方法将组件渲染到页面上

  ReactDOM.render(
    <Parent name="React" />,document.getElementById('example')
  );

运行结果

整个应用的功能是父组件接收传入的用户名称,并将用于名称传给子组件,最后再由子组件渲染显示到页面上。这些组件直接按是怎么通讯的呢?下面通过两方面介绍一下

  • 子组件调用父组件的方法。从上面的例子可以看出,子组件要拿到父组件的属性,需要通过this.props方法。所以,如果子组件想要调用父组件的方法,只需要父组件把要被调用的方法以属性的方式放在子组件上,子组件内部便可通过"this.props.被调用的方法"这样的方式拿到name属性的。然后,每次父组件修改了传入的name属性,子组件便会得到通知,然后自动获取新的name属性
  • 父组件调用子组件的方法。子组件调用父组件是通过prop属性,而反过来,父组件调用子组件通过的就是 ref 属性。子组件被设置了ref属性之后,父组件便可以通过this.ref.xxx来获取到子组件了,其中xx为子组件的ref值。

组件实践

1. 静态组件

var MyComponent=React.createClass({
  render: function() {
   return <h1 className="my-h1" style={{color:'red'}}>Hello world!</h1>;
 }
});
ReactDOM.render(
 <MyComponent />,
 document.getElementById('example')
);

运行结果

运行结果

2. 动态组件

var Counter = React.createClass({
    incrementCount: function(){
        this.setState({
            count: this.state.count + 1
        });
    },
    getInitialState: function(){
        return {
            count: 0
        }
    },
    render: function(){
        return (
            <div className="my-component">
                <h1>Count: {this.state.count}</h1>
                <button type="button" onClick={this.incrementCount}>{this.props.name}</button>
            </div>
        );
    }
});

ReactDOM.render(
    <Counter name="递增按钮" />,
    document.getElementById('mount-point')
);

运行结果:

递增按钮

参考内容

推荐阅读更多精彩内容

  • 按照惯例,先给ReactJS背书 React是一个Facebook开发的UI库,于2013年5月开源,并迅速的从最...
    艾伦先生阅读 3,070评论 1 12
  • 3. JSX JSX是对JavaScript语言的一个扩展语法, 用于生产React“元素”,建议在描述UI的时候...
    pixels阅读 2,608评论 0 24
  • 使用 create-react-app 快速构建 React 开发环境 项目的目录结构如下: React JSX ...
    majun00阅读 369评论 0 0
  • 以下内容是我在学习和研究React时,对React的特性、重点和注意事项的提取、精练和总结,可以做为React特性...
    科研者阅读 7,627评论 2 21
  • 自己最近的项目是基于react的,于是读了一遍react的文档,做了一些记录(除了REFERENCE部分还没开始读...
    潘逸飞阅读 2,822评论 1 10
  • 昨天被QiangSt问及为什么还没对象这件事,仔细思考和梳理了一下原因。 一方面由于工具理性的长期影响,使得社会上...
    履霜阅读 472评论 8 13
  • 懊恼是对过去的悔恨和对未来的迷茫。 你懊恼是因为你穷 穷是是什么?从字为会意,力在穴下,有劲使不出。本义:穷尽。...
    吾日阅读 650评论 0 1
  • 看完电影,情节且不去说他,因为魔兽世界可以通过两个小时呈现出来,但是这么漫长的魔兽历史,两个小时实在是太短了。作为...
    闲男阅读 534评论 0 51