Skip to main content

JSX 本质与 createElement

一、JSX 的定义

JSX 即 Javascript XML,它是 JavaScript 的语法扩展。React 使用 JSX 来替代常规的 JS,遇到 < 时 JSX 就当做 HTML 解析,遇到 { 就当 JS 解析。

下面是一段 JSX 代码:

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

二、JSX 的用法

1、在 JSX 中嵌入表达式

下面声明了一个名为 name 的变量,然后在 JSX 中使用它,并将它包裹在大括号中:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

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

在 JSX 语法中,可以在大括号内放置任何有效的 JavaScript 表达式。例如,2 + 2user.firstNameformatName(user) 都是有效的 JavaScript 表达式。

2、JSX 中指定属性

可以通过使用引号,来将属性值指定为字符串字面量:

const element = <div tabIndex="0"></div>;

也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:

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

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

例如,JSX 里的 class 变成了 className,而 tabindex 则变为 tabIndex

3、JSX 属性扩散

当要给组件设置多个属性,可以不用一个个写下这些属性,或有时甚至不知道这些属性的名称,可以用 {...props}

var component = <Component {...props} />;

4、JSX 防止注入攻击

可以安全地在 JSX 当中插入用户输入内容:

const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

三、JSX 的本质

1、基本用法的转换

再回来看 JSX 代码:

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

浏览器无法直接执行 JSX 代码,因为浏览器只认识 JavaScript。JSX 代码需要经过 JavaScript 编译器转化为 JavaScript 代码之后,才能够被浏览器执行。

通过 Babel,上述 JSX 代码被转换成了下面的 JavaScript 代码:

const element = /*#__PURE__*/ React.createElement("h1", null, "Hello, world!");

可以看出,JSX 本质上是 React.createElement 的语法糖。

2、JSX style 转换

const styleObj = { color: 'red', fontSize: '20px' };
const elem = <div className="main" style={styleObj}></div>;

转化为:

const styleObj = {
color: 'red',
fontSize: '20px'
};
const elem = /*#__PURE__*/React.createElement("div", {
className: "main",
style: styleObj
});

3、JSX 子组件转换

const elem = <div id="main">
<Input name={name} />
<List list={list} />
</div>;

注意如果是组件,React.createElement 的第一个参数是组件名称,组件名称是需要大写的。

上面代码转换为:

const elem = /*#__PURE__*/ React.createElement(
"div",
{
id: "main"
},
/*#__PURE__*/ React.createElement(Input, {
name: name
}),
/*#__PURE__*/ React.createElement(List, {
list: list
})
);

4、JSX 事件的转换

const elem = <div id="main" onClick={this.clickHandler}></div>;

转换为:

const elem = /*#__PURE__*/ React.createElement("div", {
id: "main",
onClick: (void 0).clickHandler
});

5、JSX 列表渲染的转换

const elem = <ul>
{this.state.list.map((item, index) => {
return <li key={item.id}>{item.title}</li>
}
)}
</ul>;

转换为:

const elem = /*#__PURE__*/ React.createElement(
"ul",
null,
(void 0).state.list.map((item, index) => {
return /*#__PURE__*/ React.createElement(
"li",
{
key: item.id
},
item.title
);
})
);

以上的转换都可以看出,JSX 本质上是 React.createElement 的语法糖。

四、React.createElement

1、基本用法

来看一段复杂点的 JSX 代码:

const element = (
<div className="container">
<span>Hello</span>
<span>World</span>
</div>
)

转化后的 JavaScript 如下:

const element = /*#__PURE__*/ React.createElement(
"div",
{
className: "container"
},
/*#__PURE__*/ React.createElement("span", null, "Hello"),
/*#__PURE__*/ React.createElement("span", null, "World")
);

再结合 React.createElement 的语法:

React.createElement(
type,
[props],
[...children]
)
  1. 第一个参数 type 为标签类型;
  2. 第二个参数 props 为标签属性的集合,例如 id,className;
  3. 之后的参数为其子元素。

2、React.createElement 源码

  1. 二次处理 key、ref、self、source 四个属性值;
  2. 遍历 config,筛选出可推进 props 里的属性;
  3. 提取子元素,推入 childArray(即:props.children)数组;
  4. 格式化 defaltProps;
  5. 结合以上数据作为入参,发起 ReactElement 调用。

可以看出,React.createElement 中并未涉及到算法或者复杂的 DOM 操作,它的每一个步骤都在格式化数据,就像是开发者和 ReactElement 调用之间的一个“转换器”、一个数据处理层。最后通过调用 ReactElement 实现元素的创建。

下面是 React.createElement 的源码

/**
* 创建 React Element
* type 用于标识节点的类型,可以是 html 标签字符串,也可以是 react 组件类型或 Reactfragment 类型
* config 组件所有的属性都会以键值对的形式存储在 config 对象中
* children 子元素、子节点
* 1. 分离 props 属性和特殊属性
* 2. 将子元素挂载到 props.children 中
* 3. 为 props 属性赋默认值 (defaultProps)
* 4. 创建并返回 ReactElement
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;

// props 变量用于储存元素属性的键值对集合
const props = {};

// React 内部为了实现某些功能而存在的属性
let key = null;
let ref = null;
let self = null;
let source = null;

// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
// 将 config.ref 属性提取到 ref 变量中
ref = config.ref;
// 在开发环境中
if (__DEV__) {
// 如果 ref 属性的值被设置成了字符串形式就报一个提示
// 说明此用法在将来的版本中会被删除
warnIfStringRefCannotBeAutoConverted(config);
}
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
// 将 config.key 属性中的值提取到 key 变量中
key = '' + config.key;
}

self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着把 config 里面的属性一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
// 如果当前遍历到的属性是对象自身属性
// 并且在 RESERVED_PROPS 对象中不存在该属性
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
// 将满足条件的属性添加到 props 对象中 (普通属性)
props[propName] = config[propName];
}
}
}

/**
* 将第三个及之后的参数挂载到 props.children 属性中
* 如果子元素是多个 props.children 是数组
* 如果子元素是一个 props.children 是对象
*/

// 由于从第三个参数开始及以后都表示子元素,所以减去前 2 个参数的结果就是子元素的数量
const childrenLength = arguments.length - 2;
// 如果抛去 type 和 config 只剩一个参数,意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给 props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
// i + 2 的原因是实参集合的前两个参数不是子元素
childArray[i] = arguments[i + 2];
}
// 如果是开发环境
if (__DEV__) {
// 如果 Object 对象中存在 freeze 方法
if (Object.freeze) {
// 调用 freeze 方法冻结 childArray 数组,防止 React 核心对象被修改
Object.freeze(childArray);
}
}
// 最后把这个数组赋值给 props.children
props.children = childArray;
}

/**
* 如果当前处理是组件
* 看组件身上是否有 defaultProps 属性
* 这个属性中存储的是 props 对象中属性的默认值
* 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined
* 如果为 undefined 就将默认值赋值给对应的 props 属性值
*/

// 处理 defaultProps
if (type && type.defaultProps) {
// 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量
const defaultProps = type.defaultProps;
// 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量
for (propName in defaultProps) {
// 如果 props 对象中的该属性的值为 undefined
if (props[propName] === undefined) {
// 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值
props[propName] = defaultProps[propName];
}
}
}

/**
* 在开发环境中 如果元素的 key 属性 或者 ref 属性存在
* 监测开发者是否在组件内部通过 props 对象获取了 key 属性或者 ref 属性
* 如果获取了就报错
*/

// 如果处于开发环境
if (__DEV__) {
// 元素具有 key 属性或者 ref 属性
if (key || ref) {
// 看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件
// 如果元素不是组件 就直接返回元素类型字符串
// displayName 用于在报错过程中显示是哪一个组件报错了
// 如果开发者显式定义了 displayName 属性 就显示开发者定义的
// 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
// 如果 key 属性存在
if (key) {
// 为 props 对象添加key 属性
// 并指定当通过 props 对象获取 key 属性时报错
defineKeyPropWarningGetter(props, displayName);
}
// 如果 ref 属性存在
if (ref) {
// 为 props 对象添加 ref 属性
// 并指定当通过 props 对象获取 ref 属性时报错
defineRefPropWarningGetter(props, displayName);
}
}
}
// 最后返回一个调用 ReactElement 执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
// 在 Virtual DOM 中用于识别自定义组件
ReactCurrentOwner.current,
props,
);
}

ReactElement 它只做一件事,就是“创建”,将传入的参数按照一定的规范,“组装”到 element 对象里,返回给 React.createElement

下面是 ReactElement 的源码

/**
* 接收参数 返回 ReactElement
*/
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
/**
* 组件的类型, 十六进制数值或者 Symbol 值
* React 在最终在渲染 DOM 的时候, 需要确保元素的类型是 REACT_ELEMENT_TYPE
* 需要此属性作为判断的依据
*/
$$typeof: REACT_ELEMENT_TYPE,

/**
* 元素具体的类型值 如果是元素节点 type 属性中存储的就是 div span 等
* 如果元素是组件 type 属性中存储的就是组件的构造函数
*/
type: type,
/**
* 元素的唯一标识
* 用作内部 vdom 比对 提升 DOM 操作性能
*/
key: key,
/**
* 存储元素 DOM 对象或者组件实例对象
*/
ref: ref,
/**
* 存储向组件内部传递的数据
*/
props: props,
/**
* 记录当前元素所属组件 (记录当前元素是哪个组件创建的)
*/
_owner: owner,
};

if (__DEV__) {
element._store = {};

Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});

Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});

Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});

if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}

return element;
};

可以看出,ReactElement 就是把入参按照一定的规范进行组成,最终得到一个 React 元素。

五、总结

  • JSX 是 JavaScript 的语法扩展,本质是 React.createElement 的语法糖,Babel 帮我们完成了这个转换过程;
  • React.createElement 的作用是进行数据转换,最终通过 ReactElement 方法创建出 React 元素;
  • ReactDOM.render 再将生成好的元素渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实 DOM。