全文共 3046 个字

JSX基础介绍

先看看一个最简单的例子:

const element = <h1>Hello, world!</h1>;

上面这段有趣的例子既不是标准的JavaScript也不是HTML,它就是我们接下来要介绍的JSX的语法,是一种JavaScript的扩展。在React中使用JSX描述一个UI是什么样子的,就好像HTML告诉浏览器我们看到的页面是什么样子。最开始接触JSX时会感觉它很像一种模板语言,但是除了提供模板能力之外,他拥有JavaScript所有的能力。

JSX用于产生React的组件,JSX最大的特色就是就是在JavaScript中嵌入和HTML表达式。我们先看下面这个例子:

function formatName(user) {
  //将参数合并成一个srting
  return user.firstName + ' ' + user.lastName;
}

//创建user对象
const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

//创建element对象,并用JSX语法标识为一个html内容。
//h1标签中会包含方法计算之后的内容
const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

ReactDOM.render(
  element,
  document.getElementById('root')
);

测试代码

这个例子将JSX语法分成了很多部分,element就是一个HTML的JSX表达式,HTML标签最好使用一组()括号包裹起来以避免分号导致的问题(分号可能会在编译时成为HTML内容的一部分)。ReactDOM是一个react工具,用于提供Dom渲染功能。ReactDOM.render 方法接受2个参数,一个是要渲染的JSX元素,另外一个是Dom对象,render会在这个Dom对象中添加由JSX定义的HTML。

JSX是一种丰富的表达式,他可以随意嵌套JavaScript和HTML使用,例如if、for等等,比如:

function getGreeting(user) {
  // 使用if来判断输入参数,根据判断结果来输出HTML内容
  if (user) {
    return <h1>Hello, {formatName(user)}</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

源生的HTML可以任意指定属性,同样在JSX中也有这个能力,例如:

const element = <div tabIndex="0"></div>;
//或
const element = <img src={user.avatarUrl}></img>;

也可以直接用 />表示一个HTML标签的闭环:

const element = <img src={user.avatarUrl} />;

当然也可以同时声明父元素和子元素:

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

需要注意的是:由于JSX更像JavaScript,在使用JSX语法时建议使用驼峰规范来命名。例如将标签上的"class"命名为"className"。

JSX天生具备防止注入攻击的能力。ReactDom在渲染之前会转义所有嵌入JSX中的值,所以他能确保没有任何特殊的内容被注入到最终的HTML代码中。在渲染之前,所有的东西都会转换成string类型,这将能有效的防止XSS攻击。

JSX对象

首先需要强调的是,JSX对象就是一个JavaScript对象,所有的JSX表达式最终都会转义成JavaScript。有两种方法可以创建JSX对象:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

上面2种创建JSX对象的方法结果都是一样的。使用React.createElement() 方法的好处是它会执行一些检查,以帮助我们编写无错误的代码。最终通过转义,他会输出这样一个结构:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world'
  }
};

官方将这个对象的结构称为React元素。React通过这个对象来控制浏览器对dom的渲染,最终显示我们想要的内容。

渲染React元素

前一小节提到的React元素是React的基本单元,React会由一个一个的基本单元组成,最终构建成一个有效的体系(组件化)。每一个元素用来描述想在屏幕上展示什么。

和Dom结构不同的是, React元素是一个纯粹的对象并且比创建一个Dom花费的资源更少。React会全局维护所有的元素,并在合适的时候更新到浏览器的Dom,这就是虚拟Dom管理机制。

将一个元素渲染成为Dom

从一个简单的div标签开始:

<div id="root"></div>

这是一个“根元素”,我们将通过ReactDom来管理他的所有子元素。一个RreactDom.render方法只能用来渲染一个Dom元素。如果想同时对多个元素进行渲染,可以使用互不关联的RreactDom.render方法来对不同的Dom元素进行操作。

下面的例子将一个JSX元素渲染到Dom中,完成后,会在页面中显示Hello world:

const element = <h1>Hello, world</h1>;
ReactDOM.render(
  element,
  document.getElementById('root')
);

测试代码

更新已被渲染的元素

React元素是不可变对象,一旦创建,将不再能够修改,包括其属性和子元素。更新UI的方法就是创建一个新的元素并用ReactDom.render()再次渲染他。如下面的例子:

//创建一个tick方法,用于执行重复方法
function tick() {
  //创建一个JSX对象
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

测试代码

上面代码中创建了一个tick方法,并使用setInterval让这个方法每1秒执行一次。tick中创建了一个用于显示时间的JSX对象,然后将其渲染到#root节点中。运行代码可以看到例子实现了一个时钟功能,每秒都会调用ReactDom.render动态修改时钟的数字。

需要强调的是:重复使用ReactDom.render方法来多次渲染Dom并不是React推崇的方法。后续的内容中会介绍更合理的方法。

React只执行必要的更新

ReactDom会将当前的元素与之前的元素进行比对,并且只会更新被改动部分的Dom以避免全局渲染和多次重复渲染。我们可以通过浏览器工具来验证最后一个例子——我们使用render方法创建了整个Dom结构,但是仅仅只有表示时间的文字部分发生了变动。

组件与属性

组件是React的重要概念,组件能让我们将整个页面的UI分解成独立、可复用、可继续分割的对象。从概念上来说,组件很像JavaScript的一个方法,他可以接受任意的参数输入(React中将这些参数称呼为属性——Props)并返回一个用于UI展示的React元素。

使用函数或类声明组件

在React中既可以使用function来声明一个组件,也可以使用ES6规范的class关键字来声明一个组件。下面的例子是使用function创建一个组件:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

例子中使用function声明了一个名为Welcome的组件,他只能接收一个参数用于描述组件的元素。在React中,我们将通过function创建的组件命名为“functional”,因为从字面上看它实际上就是一个JavaScript的函数。

下面的例子是使用ES6 class方式声明一个组件:

//ES6
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

//JavaScript 非官方内容
var Welcome = React.createClass({
   render: function () {
     return <h1>Hello, {this.props.name}</h1>;
   }
});
//

上面两种创建组件的方式,从React的角度来说是一样的。

与使用方法创建组件相比,使用ES6 class的方式创建组件有更多特性,后续篇幅会说明。

渲染一个组件

为了便于说明,我们先用<div>标签创建一个最简单的组件:

const element = <div />;

此时,element即可认为是一个组件,组件中只有一个div元素。根据这个定义,我们可以使用用户自定义的组件,比如使用上面的Welcome:

const element = <Welcome name="Sara" />;

当React发现element中有用户自定义的组件,它会使用JSX语法解析element并将标签上的属性转换成一个JSX对象,这个对象被称为“props”。

例如下面这个例子,我们经使用组件在屏幕上输出"Hello, Sara":

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

测试代码

看看发生了什么:

  1. 使用ReactDOM.render()方法渲染<Welcome name="Sara" />。
  2. React调用Welcome方法,并传递了一个参数:{name: 'Sara'}。
  3. 在Welcome组件中合并了参数,并返回一个<h1>Hello, Sara</h1>。
  4. ReactDom将<h1>Hello, Sara</h1>更新到浏览器的Dom树中。

需要注意的是,使用React组件时一定要将组件名称首字母大写。例如在html标签中<div>是一个标准的Dom,但是<Welcome>并不是一个标准的html标签,而是一个React组件。React通过判断组件名称的首字母加以区分。

组件组合

一个组件能够被其他的组件引用,就像使用普通的html标签一样。我们可以把组件抽象成各种抽象功能在任何地方使用,例如一个按钮、一个弹出框、一个表单。下面的例子展示了React组件的组合使用:

function Welcome(props) {//创建Welcome组件
  return <h1>Hello, {props.name}</h1>;
}

function App() {//创建App组件
  return (
    <div>
      <Welcome name="Sara" /> //使用Welcome组件
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

ReactDOM.render( //向Dom渲染App组件
  <App />,
  document.getElementById('root')
);

测试代码    

在例子中,首先创建了一个Welcome组件,然后在App组件中重复使用它,最后向浏览器渲染App。App组件中整合使用了Welcome组件。基于组件可以层层封装,建议在使用React开始新项目时先从封装一些小的组件开始,比如按钮、弹出框等,这会对后面开发高层次的业务逻辑时有巨大的帮助。

一个组件只能返回一个根元素,不能同时包含2个根元素。因此上面的例子中App组件中增加了一个<div>元素将Welcome组件包裹起来。

抽象提取组件

不必担心组件的分割粒度太小,开发组件时我们最好是通过多个层次的组件组合实现一个更高层次的组件。

看下面这个例子——封装一个Comment组件:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name} 
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

尝试代码

Comment组件很难维护,因为它内部的代码都前台和交织在一起,也无法实现代码复用。现在我们稍微修改组件中的Avator,将其提取成一个组件:

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Avator组件不需知道他是在哪被使用,它只关心输入的参数,并使用参数生成一个Dom结构。现在可以像下面这样声明一个Comment组件:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />//使用Avatar组件
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

我们再将UserInfo也提取成一个组件:

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} /> //使用Avatar组件
        {props.user.name}
      </div>
    </div>
  );
}

此时的Comment:

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} /> //使用UserInfo组件
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

测试代码

属性(props)只读

无论是使用函数(function)还是类(class)声明组件,它都不能通过修改props参数来改变值。例如下面这个sum方法:

function sum(a, b) {
  return a + b;
}

var param1 = 1,
    param2 = 2,
    result = sum(1, 2);

在第一次计算得到结果之后,无论怎么修改param1、param2的值,result都不会改变。

React相当的灵活自由,但是它有一条必须遵守的规则:

所有的React组件必须像上面的sum方法这样保证传入的属性(props)参数只读。