前言
React特点的之一就是JSX,JSX是JavaScript的语法的扩展,使用JSX来开发UI内容。React开发不一定需要使用JSX,但是使用JSX会非常便捷。
实际上JSX是React.createElement函数的语法糖,使用JSX需要使用Babel来将JSX转移成createElement函数调用(React版本号是17.0.0)。
createElement具体执行逻辑
JSX的具体使用可以查看React中文官网文档对其的介绍,这里就不赘述了。使用React JSX方式来创建的简单的实例如下:
ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('app')
);
在代码中直接JSX,通过Babel来配合会将JSX转移成React.createElement函数调用,上面简单实例函数调用形式如下:
ReactDOM.render(
React.createElement('h1', null, 'Hello World'),
document.getElementById('app')
)
接下来就具体看看React.createElement函数的具体执行逻辑。
React.createElement函数的语法如下:
React.createElement(
type,
[props],
[...children]
)
React.createElement函数是用于创建并返回指定类型的新 React 元素。type表示元素名称,props表示组件的入参即传入组件的属性,children表示子组件。
createElement函数属于React核心功能,位于react core包中。createElement在源码中实际上是调用createElementWithValidation函数,具体如下:
var createElement$1 = createElementWithValidation;
exports.createElement = createElement$1;
createElementWithValidation函数的核心逻辑具体如下:
isValidElementType
该函数是用于校验createElement函数传入的type参数,其具体逻辑如下:
function isValidElementType(type) {
if (
typeof type === 'string' ||
typeof type === 'function'
) {
return true;
}
if (
type === exports.Fragment ||
type === exports.Profiler ||
type === exports.unstable_DebugTracingMode ||
type === exports.StrictMode ||
type === exports.Suspense ||
type === exports.unstable_SuspenseList ||
type === exports.unstable_LegacyHidden || enableScopeAPI ) {
return true;
}
// 其他校验逻辑type.$$typeof的校验
return false;
}
类型参数type可以是字符串、函数,也可以是React提供的一些内置组件类型。实际上类型参数type可以是:
- 标签名字符串:浏览器原生标签
- React 组件:class 组件或函数组件
- React fragment、React Profiler等内置组件
createElement核心逻辑
var element = createElement.apply(this, arguments);
对于React.createElement函数调用逻辑中核心逻辑是调用createElement函数。该函数的主要逻辑可以概括如下几点:
- 处理props选项以及其他相关参数
- 处理children选项,支持数组和多个参数形式
- 调用ReactElement函数
主要逻辑源代码如下:
function createElement(type, config, children) {
// 定义props对象
var props = {};
if (config != null) {
// 将定义的props复制到内部props对象中,其中不包括key、ref、self、source属性
for (propName in config) {
if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// 处理
if (type && type.defaultProps) {
// 相关处理逻辑
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
ReactElement函数实际上就是创建一个冻结的对象,其具体代码如下:
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner
};
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 元素,它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。
在React元素中有一个type属性,实际上就是createElement调用时的第一个参数,即该属性实际上就是保存对应的组件内容
ReactElement函数有一个重要的逻辑就是调用Object.freeze,对props和对象本身进行冻结,来避免对其再修改等操作,但是Object.freeze是浅层的冻结,如果对象内部存在引用类型是可以被修改的。
虽然props会被浅层冻结,但是不是完全冻结,还是可以更改内部对象的,但是React声明props的只读性即:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改
validatePropTypes
处理组件的propTypes做相关处理,这里逻辑次要可以略过。
总结
React.createElement函数的逻辑相对来说是简单清晰的,总结如下:
- 校验类型参数type的合法性,可以是代表标签的字符串、组件(class组件、函数组件)、一些内部组件
- 生成表示组件类型的对应React元素对象,并处理相关属性
- 子组件children选项的传参的数组、非数组方式处理
- props相关处理(key、ref、self、source不会被包含props,如果包含defaultProps也会立即处理),其中children会是props中特殊的一个属性
这里简单对比下Vue,实际上Vue中也存在一个createElement函数是用于创建虚拟DOM对象的,其createElement函数的传入参数与React这边基本相同。其中对于相同位置的props参数选项,Vue称之为数据对象,是创建组件非常重要的对象参数。不同于Vue数据对象的复杂性,React.createElement函数调用传入的props参数是非常清晰简单的:
所有关于组件的属性和事件都保存在React元素对象中的props属性中,而没有其他复杂的区分例如props、attrs、domProps、nativeOn等
当然Vue数据对象的复杂性也是由于Vue事件机制等决定的,其在解析阶段就区分了原生属性、自定义属性、原生事件、自定义事件。React这边对于这里的区分逻辑,理论上也是存在的,目前看来是在后续render处理阶段,这里先暂时不去关心之后会去细看。
无论是React.createElement还是Vue实例的createElement都是生成对应的虚拟DOM,但是这个过程Vue的处理是非常复杂的。