path:packages/react/src/ReactElement.js
ReactElement 下面导出了多个方法:
createElement
用于创建并返回一个新的ReactElement 元素createFactory
用于返回固定类的 createElement 方法 【已废弃】cloneElement
克隆一个元素isValidElement
验证一个对象是否为 ReactElement 。cloneAndReplaceKey
使用给定 key 返回一个新的 ReactElement 类型
ReactElement
ReactElement
是用来创建 React 元素的工厂方法。
ReactElemen
不再遵循类的模式,并未从 ReactElement 模块导出,因此不能使用 new
来调用它,并且无法使用 instanceOf
检查。 取而代之,可以检测 $$typeof
字段与 Symbol.for('react.element')
是否匹配来判断是否是一个 React 元素
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
if (__DEV__) {
// do something
}
return element;
};
复制代码
从上面的源码中可以看到 React 元素是一个拥有以下属性的普通的对象:
-
$$typeof
这个标记允许我们唯一地将其标识为 React 元素,其值为
REACT_ELEMENT_TYPE
。我们可以通过判断对象的
$$typeof
是否等于REACT_ELEMENT_TYPE
来判断其是否为一个 React 元素。 -
type
: -
key
: -
ref
: -
props
: -
_owner
:
createElement
React.createElement
函数在 React 中具有举足轻重的地位,我们的组件最后都会编译成它。
React.createElement
使用给定的 type
创建并返回的新的 React 元素,参见 React 官方文档。
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;
// 不为 undefined 和 null
if (config != null) {
// config 是否含有有效的 ref 属性
if (hasValidRef(config)) {
ref = config.ref;
}
// config 是否含有有效的 key 属性
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 剩余的属性被添加到一个新的 props 对象中
// RESERVED_PROPS 包含四个属性 ref、key、__self、__source
// 这里就是拷贝除这四个属性之外的其他属性
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// children 可以是多个参数,这些参数被转移到新分配的 props 对象上。
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
// do somthing
}
props.children = childArray;
}
// 解析默认 props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
// do something
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
复制代码
示例
来看看一个示例:
React.createElement('div');
复制代码
返回一个对象,对象如下:
可以看到句代码创建了一个普通的 javascript 对象。这时候,产生了疑问:
- DOM 层级如此之多,信息之复杂,React 又是如何实现的?
React 如何实现 DOM 层级
来看下面的 Hello
组件:
function Hello(props) {
function sayHi() { return (<div>Hi</div>); }
function handleClick(e) {
console.log('click');
}
return (
<div>
{ sayHi() }
Hello { props.name }
<p className="title" onClick={ handleClick }>React</p>
<p className="content">React is cool!</p>
<MyFooter className="footer">
<div className="concat">concat</div>
<div className="info">company info</div>
</MyFooter>
</div>
);
}
复制代码
MyFooter
组件:
function MyFooter(props) {
return (
<div className="footer">
{ props.children }
</div>
);
}
复制代码
我们采用了函数式组件的方式,他们将分别被编译为:
// Hello 组件
function Hello(props) {
function sayHi() {
return React.createElement("div", null, "Hi");
}
function handleClick(e) {
console.log('click');
}
return React.createElement("div", null, sayHi(), "Hello ", props.name, React.createElement("p", {
className: "title",
onClick: handleClick
}, "React"), React.createElement("p", {
className: "content"
}, "React is cool!"), React.createElement(MyFooter, {
className: "footer"
}, React.createElement("div", {
className: "concat"
}, "concat"), React.createElement("div", {
className: "info"
}, "company info")));
}
// MyFooter 组件
function MyFooter(props) {
return React.createElement("div", {
className: "footer"
}, props.children);
}
复制代码
首先,从上面的代码我们可以看出下面几点:
-
组件都会被编译成
React.createElement
不论是自定义组件还是原始的 html 标签,都会被编译器编译。 -
React.createElement
方法的参数个数是可变的,在上面源码分析中,我们已经看到从第三个参数开始的所有参数会打包为一个数组,存入 React 元素的props
属性的children
中。 -
不论从组件的哪一级部分开始划分,其子元素都是通过函数参数传递进父级的,并最后都会存放于
props
属性的children
中。 -
在处理
children
时,编译器会很智能地区分字符串、{} 表达式和 JSX,不同的部分都会被转换成一个React.createElement
的参数,因此在上面的代码中,会产生 6 个参数:sayHi()
"Hello"
props.nameReact.createElement(...)
React.createElement(...)
React.createElement(...)
-
自定义组件的
type
参数值是组件的直接引用,而不是组件名的字符串。MyFooter
组件的type
属性是一个函数,是它本身,而不是"MyFooter"
字符串。 -
只要是写在 JSX 上的属性,都被当做
config
的一部分传递给了React.createElement()
,包括事件,例如这里的onClick
再看看生成的 JavaScript 对象:
所谓的层级结构就是通过 React 元素的 props
属性中的 children
属性层层嵌套得来的。
createFactory
此辅助函数已废弃,建议使用 JSX 或直接调用 React.createElement()
来替代它。
cloneAndReplaceKey
使用新的 key
克隆一个 React 元素。
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);
return newElement;
}
复制代码
cloneElement
克隆一个旧的 react 元素,并返回一新的个 React 元素,参见 React 官方文档。
export function cloneElement(element, config, children) {
invariant(
!(element === null || element === undefined),
'React.cloneElement(...): The argument must be a React element, but you passed %s.',
element,
);
let propName;
// 复制原始 props
const props = Object.assign({}, element.props);
// 提取保留名称
let key = element.key;
let ref = element.ref;
// Self 被保存,因为 owner 被保存。
const self = element._self;
// 保存 Source,因为 cloneElement 不太可能被 transpiler 定位,
// 而原始源 source 可能是真正 owner 的更好指示器。
const source = element._source;
// Owner 将被保留,除非 ref 被覆盖
let owner = element._owner;
if (config != null) {
if (hasValidRef(config)) {
// Silently steal the ref from the parent.
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// 其余属性覆盖现有的 props
let defaultProps;
if (element.type && element.type.defaultProps) {
defaultProps = element.type.defaultProps;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
if (config[propName] === undefined && defaultProps !== undefined) {
// Resolve default props
// 解析默认 props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
}
}
}
}
// Children 可以是多个参数,这些参数被转移到新分配的 props 对象上。
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return ReactElement(element.type, key, ref, self, source, owner, props);
}
复制代码
cloneElement
方法会拷贝对旧 React 元素的属性进行拷贝,并使用新传入的 config
和 children
参数对这些属性进行更新。然后使用这些新的属性作为参数调用 ReactElement
构造方法,生成并返回新的 React 元素。
isValidElement
通过判断对象的 $$typeof
属性与 Symbol.for('react.element')
是否相同来,验证一个对象是否为 React 元素。
通过 React.createElement
方法创建的对象,都含有一个值完全相同的 $$typeof
属性,标识其为一个 React 元素。
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
复制代码
其他
RESERVED_PROPS
保留属性
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
复制代码
hasValidRef
hasValidRef
判断一个对象中是否含有 ref
属性
function hasValidRef(config) {
if (__DEV__) {
// do something
}
return config.ref !== undefined;
}
复制代码
hasValidKey
hasValidKey
判断一个对象中是否含有 key
属性
function hasValidKey(config) {
if (__DEV__) {
// do something
}
return config.key !== undefined;
}
复制代码
遗留问题
- 普通的 javascript 对象(React 元素)是如何变成了我们页面的 DOM 结构的
写文章的时候再阅读 React 的官方文档,发现这份文档制作很用心,循序渐进,由浅入深。
借用文档的一句话:
我们将在下一章节中探讨如何将 React 元素渲染为 DOM。