10

十二、深入理解JSX

从根本上讲,JSX就是提供了一个React.createElement(component, props, ...children)函数的语法糖。就像下面的JSX代码:

<MyButton color="blue" shadow={2}>
    Click Me
</MyButton>

经过编译后为:

React.createElement(
    MyButton,
    {color: 'blue', shadow: 2},
    'Click Me'
)

如果一个标签没有子元素的话,你可以使用/>来自动闭合。例如:

<div className="sidebar" />

经过编译后为:

React.createElement(
    'div',
    {className: 'sidebar'},
    null
)

如果你想测试一些特定的JSX是如何转换成JavaScript的话,你可以试试在线Babel编译器

指定React元素类型

JSX标记的第一部分决定了React元素的类型。

首字母大写的类型表示JSX标记指的为React组件。 这些标签被编译为对指定变量的直接引用,因此如果使用JSX <Foo />表达式,Foo必须在当前的作用域内。

React必须在作用域内

由于JSX编译的本质是对React.createElement的调用,因此React库也必须始终在JSX代码的作用域中。
例如,虽然CustomButton没有直接引用React,但是这两个导入的模块在这段代码中也还是很有必要的:

import React from 'react';
import ReactDOM from 'react-dom';

function WarningButton(props) {
    // return React.createElement(CustomButton, {color: 'red'}, null);
    return <CustomButton color="red" />
}

如果不使用JavaScript打包工具并将React通过script标签引入,那么它就会作为一个全局变量React

对JSX类型使用『点』表示符

您还可以使用JSX中的点表示符来引用React组件。 如果您有一个模块会导出很多React组件的话,使用这种方法就会十分方便。 例如,如果MyComponents.DatePicker是一个组件,您可以直接从JSX使用它:

import React from 'react';
import ReactDOM from 'react-dom';

const MyComponents = {
    DatePicker(props) {
        return <div>这里有一个颜色为{props.color}的日期选择器</div>
    }
};

function BlueDataPicker(props) {
    return <MyComponents.DatePicker color="blue" />
}

ReactDOM.render(
    <BlueDataPicker />,
    document.getElementById('root')
);

用户自定义组件必须是首字母大写

当元素类型以是小写字母开头时,它指向一个内置组件,如<div><span>,并生成一个字符串'div''span'传递给React.createElement。 以大写字母开头的类型,如<Foo />编译为React.createElement(Foo),并且在当前作用域内寻找这个名称为Foo的已定义或已导入组件。

我们建议使用首字母大写命名组件。 如果你有一个以小写字母开头的组件,请在JSX中使用它之前请将它赋值给一个首字母大写的变量。

下面代码不会按预期运行:

import React from 'react';

//这是错误的,这个组件应该为首字母大写
function hello(props) {
    // 这是正确的,因为div是一个有效的html标签
    return <div>Hello {props.name}</div>;
}

function HelloWorld(props) {
    // 这是错误的,因为它是首字母小写,所以React认为<hello />是一个html标签
    return <hello name="zhangyatao" />
}

想要修复上面的问题,我们必须将hello重命名为Hello,通过<Hello />来使用该组件:

import React from 'react';

// 这是正确的
function Hello(props) {
    return <div>Hello {props.name}</div>;
}

function HelloWorld(props) {
    // 这是正确的
    return <Hello name="zhangyatao" />;
}

在运行的时候选择组件类型

不能将常规的javascript表达式用作React元素类型。 如果你想使用一个通用表达式来表示元素的类型,只需将它赋值给一个首字母大写的变量即可。
这通常出现在当你想基于同一个props渲染一个不同的组件的情况下:

import React from 'react';
import {Com1, Com2} from './Components';

const components = {
    myCom1: Com1,
    myCom2: Com2
}

function RunCom(props) {
    // 这是错误的,JSX的类型不能这么写
    return <components[props.comType] type={props.type} />;
}

想要解决上面的问题,只需要将它们赋值给一个首字母大写的变量即可:

import React from 'react';
import {Com1, Com2} from './Components';


const components = {
    myCom1: Com1,
    myCom2: Com2
}

function RunCom(props) {
    // 这是正确的,将它们赋值给一个首字母大写的变量
    const MyCom = components[props.comType];
    return <MyCom type={props.type} />;
}

JSX中的Props

在JSX中指定Props有以下几种不同的方法。

JavaScript表达式

你可以传递任何JavaScript表达式作为Props,用{}括住它们就可以使用。 例如,在这个JSX中:

<MyComponents foo={1 + 2 + 3 + 4} />

对于MyComponent来说,props.foo的值将为10,因为是通过表达式1 + 2 + 3 + 4计算得到的。

if语句和for循环在JavaScript中不是表达式,因此它们不能在JSX中直接使用。 相反,写完它们之后你可以把JSX放在里面。 例如:

function NumberDescriber(props) {
    let description;
    if (props.number % 2 === 0) {
        description = <strong>偶数</strong>
    } else {
        description = <strong>奇数</strong>
    }
    return <div>{props.number}是一个{description}.</div>;
}

字符串直接量

你可以传递一个字符串内容作为props。 这两个JSX表达式是等价的:

<MyComponent message="hi zhangyatao" />

<MyComponent message={'hi zhangyatao'} />

当你传递一个字符串直接量时,它的值是经过html转义的。 所以这两个JSX表达式是等价的:

<MyComponent message='&lt;3' />

<MyComponent message={'<3'} />

Props默认值为true

如果你没有给Props传入一个值,那么它的默认值为true,这两个JSX表达式是等价的:

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

一般来说,我们不建议使用它,因为它可以使用ES6对象的简写{foo},也就是{foo:foo}的简称会和{foo:true}混淆。 这种行为在这里只是方便它匹配到HTML行为。

Props传递

如果你有一个对象类似的数据作为props,并且想在JSX中传递它,你可以使用...作为一个“spread”运算符传递整个props对象。 这两个组件是等效的:

function App() {
    return <Greeting firstName="yatao" lastName="zhang" />;
}

function App() {
    const props = {firstName: 'yatao', lastName: 'zhang'};
    return <Greeting {...props} />;
}

当创建一个通用容器时,spread props很有用。
然而,他们也可以让你的代码变得有点凌乱,这样很容易使大量不相关的prps传递给那些不关心它们的组件。 建议您谨慎使用此语法。

JSX中的子元素和子组件

在包含开始标记和结束标记的JSX表达式中,这些标记之间的内容通过一种特殊的prop:props.children传递。 有几种不同的方式传递子组件:

字符串直接量

你可以在开始和结束标签之间放一个字符串,那么props.children就是那个字符串。 这对许多内置的HTML元素很有用。 例如:

function MyComponent(props) {
    return <div>{props.children}<div>; //=> <div>hello zhangyatao</div>
}

<MyComponent>Hello zhangyatao</MyComponent>

这是有效的JSX,并且MyComponent中的props.children将是字符串“Hello zhangyatao”。 HTML标签是不会经过转义的,所以你一般可以写JSX就像你写HTML一样:

<div>这是一个html标签 &amp; 同时也是个JSX</div>

JSX会删除行的开始和结尾处的空格。 它也会删除中间的空行。 与标签相邻的空行被会被删除;
在字符串文本中间出现的空行会缩合成一个空格。 所以这些都渲染相同的事情:

<div>hello zhangyatao</div>

<div>
    hello zhangyatao
</div>

<div>
    hello
    zhangyatao
</div>

<div>

hello zhangyatao
</div>

JSX子元素

你可以使用很多个JSX元素作为子元素。 这对需要嵌套的显示类型组件很有用:

<Dialog>
    <DialogHeader />
    <DialogBody />
    <DialogFooter />
</Dialog>

你可以将不同类型的子元素混合在一起,因此JSX子元素可以与字符串直接量一起使用。 这是JSX的另一种方式,就像一个HTML一样:

<div>
    这是一个列表
    <ul>
        <li>item 1</li>
        <li>item 2</li>
    </ul>
</div>

一个React组件不可能返回多个React元素,但是一个JSX表达式可以包含多个子元素,因此如果你想让一个组件渲染多个东西,你可以将它们统一放置在就像上面那样的div中。

Javascript表达式

您可以将任何JavaScript表达式放在{}中作为子组件传递。 例如,下面这些表达式是等价的:

function MyComponent(props) {
    return <div>{props.children}<div>; //=> <div>hi zhangyatao</div>
}

<MyComponent>hi zhangyatao</MyComponent>

<MyComponent>{'hi zhangyatao'}</MyComponent>

这通常用于渲染任意长度的JSX表达式列表。 例如,这将渲染一个HTML列表:

function Item(props) {
    return <li>{props.message}</li>;
}

function TodoList(props) {
    const todos = ['完成文档', '出去逛街', '打一局dota'];
    return (
        <ul>
            {todos.map(message => <Item key={message} message={message} />)}
        </ul>
    );
}

JavaScript表达式可以与其他类型的子元素混合使用。 这通常用于替换字符串模板:

function Hello(props) {
    return <div>Hello {props.name}</div>;
}

使用函数作为子元素

通常,插入JSX中的JavaScript表达式都最终返回为一个字符串、React元素、一个列表。

当然,props.children可以像任何其他props那样工作,它可以传递任何类型的数据,并不局限于那些告诉React应该如何渲染的东东。 例如,如果您有一个自定义组件,您可以将props.children作为一个回调函数:

import React from 'react';
import ReactDOM from 'react-dom';

function Repeat(props) {
    let items = [];
    let callback = props.children;
    var numTimes = props.numTimes;
    for(var i = 0 ; i < numTimes ; i++ ){
        items.push(callback(i));
    }
    return <div>{items}</div>;
}

function ListOfTenThings(props) {
    return (
        <Repeat numTimes={10}>
            {index => <div key={index}>这是列表中的第{index}项</div>}
        </Repeat>
    );
}
ReactDOM.render(
    <ListOfTenThings/>,
    document.getElementById('root')
);

传递给自定义组件的子元素可以是任何东西,只要在React在渲染之前,该组件将它们转换为可以理解的东西即可。 这种用法并不常见,如果你想扩展JSX的其他能力,可以通过这个例子了解下它的工作原理。

布尔值、null、undefined在渲染时会被自动忽略

falsenullundefinedtrue是有效的子元素,不过他们从根本上讲是不参与渲染的。 这些JSX表达式将渲染处相同的东西:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{true}</div>

这对于有条件地呈现React元素很有用。 如果showHeadertrue,那么这个JSX只渲染一个<Header />

<div>
    {showHeader && <Header />}
    <Content />
</div>

如果返回一些“假的”值就会收到一个警告,如数字0,不过React仍然会渲染。 例如,此代码将不会像您预期的那样工作,因为当props.messages是空数组时将打印0

<div>
    {props.messages.length && <Message messages={props.messages} />}
</div>

想要修复上面的问题,你要确定这个表达式在&&之前总返回布尔值:

<div>
    {props.messages.length > 0 && <Message messages={props.messages} />}
</div>

相反,如果你想要一个值如falsetruenullundefined出现在输出中,你必须先将它转换为字符串

import React from 'react';
import ReactDOM from 'react-dom';

function MyVariable(props) {
    const myVariable = false;
    // 如果这里不把false转换为字符串,这只会输出『我的javascript变量是』
    const convertedVar = String(myVariable);
    return (
        <div>
            我的javascript变量是{convertedVar}
        </div>
    );
}
ReactDOM.render(
    <MyVariable/>,
    document.getElementById('root')
);

张亚涛
5.3k 声望2.8k 粉丝

人首先应该接受现实,承认问题的存在,并且反思它。而不是首先就跳出来回避这个问题,找比我们更差的。质疑甚至抨击提出问题的人,并且一杆子打死。