深入浅出 React -- JSX

905 阅读9分钟

什么是 JSX

JSX 是一个 JavaScript 的语法扩展。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能

在 React 中,JSX 仅仅是 React.createElement(component, props, ...children) 函数的语法糖

如下 JSX 代码:

<MyButton color="blue" shadowSize={2}>
	Clicke Me
</MyButton>

会编译为:

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

React 必须在作用域内

由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内

如果不使用打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中

用户自定义的组件必须以大写字母开头

以小写字母开头的元素代表一个 HTML 内置组件,比如 <div> 或者 <span> 会生成相应的字符串 'div' 或者 'span' 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 <Foo /> 会编译为 React.createElement(Foo)

JSX 语法

在 JSX 中嵌入表达式

在 JSX 语法中,你可以在打括号内放置任何有效的 JavaScript 表达式

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
}

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
)

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

JSX 也是一个表达式

在编译后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

JSX 特定属性

在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。

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

因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。

使用 JSX 指定子元素

假如一个标签里面没有内容,你可以使用 /> 来闭合标签,就像 XML 语法一样:

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

JSX 标签里能够包含很多子元素:

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

JSX 防止注入攻击

React DOM 在渲染所有输入内容之前,默认会进行转义

所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

JSX 表示对象

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用

以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
)
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
)

JSX 中的 Props

JavaScript 表达式作为 Props

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

字符串字面量

以下两个 JSX 表达式是等价的:

<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

当你将字符串字面量赋值给 prop 时,它的值是未转义的

以下两个 JSX 表达式是等价的:

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

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

Props 默认值为 true

以下两个 JSX 表达式是等价的:

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

属性展开

可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。

以下两个组件是等价的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

JSX 中的子元素

包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。

有几种不同的方法来传递子元素:

字符串字面量

<MyComponent>Hello world!</MyComponent>

编译为:

<div>This is valid HTML &amp; JSX at the same time.</div>

JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。

因此以下的几种方式都是等价的:

<div>Hello World</div>

<div>
  Hello World
</div>

<div>
  Hello
  World
</div>

<div>

  Hello World
</div>

JSX 子元素

<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>

JavaScript 表达式作为子元素

JavaScript 表达式可以被包裹在 {} 中作为子元素。

以下表达式是等价的:

<MyComponent>foo</MyComponent>

<MyComponent>{'foo'}</MyComponent>

这对于展示任意长度的列表非常有用。例如,渲染 HTML 列表:

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

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}

函数作为子元素

你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX。

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

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}

布尔类型、Null 以及 Undefined 将会忽略

false, null, undefined, and true 是合法的子元素。但它们并不会被渲染。

以下的 JSX 表达式渲染结果相同:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeadertrue 时,才会渲染 <Header /> 组件:

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

值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

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

要解决这个问题,确保 && 之前的表达式总是布尔值:

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

反之,如果你想渲染 falsetruenullundefined 等值,你需要先将它们转换为字符串

<div>
  My JavaScript variable is {String(myVariable)}.
</div>

JSX 的本质:JavaScript 的语法扩展

React 官网给出的一段定义:

JSX 是一个 JavaScript 的语法扩展。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能

那么 “JSX 语法时如何在 JavaScript 中生效的

JSX 语法是如何在 JavaScript 中生效的:认识 Babel

JSX 定位是 JavaScript 的“扩展”,这就直接决定了浏览器不会天然支持 JSX。那么,JSX 的语法是如何在 JavaScript 中生效的呢?React 官网给出了答案:

JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。

“编译” 这个动作,是由 Babel 来完成的

什么是Babel

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。 —— Babel 官网

例如箭头函数:

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});

类似的,Babel 也具备将 JSX 语法转换为 JavaScript 代码的能力

看看开头的示例:

我们写的 JSX 其实写的就React.createElement,虽然它看起来有点像 HTML,但也只是“看起来像”而已。JSX 的本质是 React.createElement 这个 JavaScript 调用的语法糖,这也呼应了 React 官方给出的“JSX 充分具备 JavaScript 的能力”这句话。

为什么选择 JSX

既然 JSX 等价于一次 React.createElement 调用,为什么不直接使用 React.createElement 来创建元素呢?

原因很简单,在效果一致的前提下,JSX 代码层次分明、嵌套关系清晰,而 React.createElement 代码混乱杂糅,不仅难以阅读,也难以编码

JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

React 官网也提出:

React 并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离

JSX 是如何映射为 DOM 的:阅读 createElement 源码

我们先大致理解 createElement 函数源码的作用:

//注意:react只写了3个参数,实际上,从第三个参数往后都是children
export function createElement(type, config, children) {
  let propName; // 用于存储后面需要用到的元素属性

  // Reserved names are extracted
  const props = {}; // 用于存储元素属性的键值对集合

  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  // 赋给标签的props不为空时
  // config 存储元素的属性
  if (config != null) {
    // 依次对 ref、key、self 和 source 属性赋值
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      // 防止是Number
      key = '' + config.key;
    }
    //__self、__source 暂时不知道是干啥用的属性
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      // 如果config中的属性不是标签原生属性,则放入props对象中
      if (

        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  // 子元素数量
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    // 依次将children push进array中
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    
    // 开发中写的this.props.children就是子元素的集合
    props.children = childArray;
  }

  // Resolve default props

  // 为传入的props设置默认值,比如:
  //class Comp extends React.Component{
  //  static defaultProps = {
  //     aaa: 'one',
  //     bbb: () => {},
  //     ccc: {},
  //   };
  //
  // }

  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      // 如果props数组中未设值,则设置默认值(注意:null也算设置了值)
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,  //'div'
    key,  //null
    ref,  //null
    self, //null
    source, //null
    ReactCurrentOwner.current, // null或Fiber
    props, // 自定义的属性、方法,注意:props.children=childArray
  );
}

参数:创建一个元素需要哪些信息

export function createElement(type, config, children)
  • type:用于表示节点的类型。它可以是标准 HTML 标签字符串,也可以是 React 组件类型或 React Fragment 类型
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中
  • children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的 “子节点” “子元素”

下面的示例可以帮助增进对 createElement 的理解:

React.createElement('div', {
  className: 'wrapper'
}, React.createElement('h1', {
  className: 'header'
}, 'header'), React.createElement('p', {
  className: 'content'
}, 'content'))

对应的 DOM 结构:

<div class="wrapper">
	<h1 class="header">header</h1>
  <p class="content">content</p>
</div>

createElement 分析

逻辑流程图

createElement 中并没有复杂的逻辑,它的每一步步骤几乎都是在格式化数据

createElement 就如同是开发者和 ReactElement 调用之间的一个 “转换器”,对数据进行处理

返回值:初识虚拟 DOM

createElement 执行到最后会 return 一个 ReactElement 的调用

下面是关于 ReactElement 的源码及解析:

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录创造该元素的组件
    _owner: owner,
  };

  if (__DEV__) {
  }

  return element;
};

ReactElement 只做了一件事,那就是创建ReactElement 把传入的参数按照一定的规范,“组装”进 element 对象,并将它返回给 React.createElement,最终 React.createElement 又将它返回到开发者

对于 ReactElement 对象实例,其本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是 “虚拟 DOM”(更准确地说,是虚拟 DOM 中的一个节点)

从虚拟 DOM 到真实 DOM 需要调用 ReactDOM.render 方法

在每个 React 项目的入口文件中,都有对 React.render 函数的调用。下面是 ReactDOM.render 的函数签名:

ReactDOM.render(
	// 需要渲染的元素(ReactElement)
  element,
  
  // 元素挂载的目标容器(真实 DOM)
  container,
  
  // 回调函数,用于处理渲染结束后的逻辑。可选
  [callback]
)

总结

JSX 经过 babel 转换为 React.createElement 函数,再调用 React.createElementReactElement 返回一个 element 对象(虚拟 DOM),最后通过 React.render 函数的调用,生产真实 DOM 节点并挂载到 “容器” 上。