react源码浅析(三):ReactElement

11次阅读

共计 9403 个字符,预计需要花费 24 分钟才能阅读完成。

react 相关库源码浅析
react ts3 项目
总览:
你将会明白:react 元素的 key 和 ref 为什么不会存在 props 上,并且传递,开发环境下与生产环境下处理 key 和 ref 的区别?…

内部方法
│ ├── hasValidRef —————————– 检测获取 config 上的 ref 是否合法
│ ├── hasValidKey —————————– 检测获取 config 上的 key 是否合法
│ ├── defineKeyPropWarningGetter —– 锁定 props.key 的值使得无法获取 props.key
│ ├── defineRefPropWarningGetter —– 锁定 props.ref 的值使得无法获取 props.ref
│ ├── ReactElement ———— 被 createElement 函数调用,根据环境设置对应的属性

向外暴露的函数
│ ├── createElement —————————- 生成 react 元素,对其 props 改造
│ ├── createFactory ————————————– react 元素工厂函数
│ ├── cloneAndReplaceKey —————————- 克隆 react 元素,替换 key
│ ├── cloneElement —————————– 克隆 react 元素,对其 props 改造
│ ├── isValidElement ——————————— 判断元素是否是 react 元素

hasValidRef
通过 Ref 属性的取值器对象的 isReactWarning 属性检测是否含有合法的 Ref,在开发环境下,如果这个 props 是 react 元素的 props 那么获取上面的 ref 就是不合法的,因为在 creatElement 的时候已经调用了 defineRefPropWarningGetter。生产环境下如果 config.ref !== undefined,说明合法。
function hasValidRef(config) {
// 在开发模式下
if (__DEV__) {
//config 调用 Object.prototype.hasOwnProperty 方法查看其对象自身是否含有 ’ref’ 属性
if (hasOwnProperty.call(config, ‘ref’)) {
// 获取‘ref’属性的描述对象的取值器
const getter = Object.getOwnPropertyDescriptor(config, ‘ref’).get;
// 如果取值器存在,并且取值器上的 isReactWarning 为 true,就说明有错误,返回 false,ref 不合法
if (getter && getter.isReactWarning) {
return false;
}
}
}
// 在生产环境下如果 config.ref !== undefined,说明合法;
return config.ref !== undefined;
}

hasValidKey
通过 key 属性的取值器对象的 isReactWarning 属性检测是否含有合法的 key,也就是如果这个 props 是 react 元素的 props 那么上面的 key 就是不合法的,因为在 creatElement 的时候已经调用了 defineKeyPropWarningGetter。逻辑与上同
function hasValidKey(config) {
if (__DEV__) {
if (hasOwnProperty.call(config, ‘key’)) {
const getter = Object.getOwnPropertyDescriptor(config, ‘key’).get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.key !== undefined;
}

defineKeyPropWarningGetter
开发模式下,该函数在 creatElement 函数中可能被调用。锁定 props.key 的值使得无法获取 props.key, 标记获取 props 中的 key 值是不合法的,当使用 props.key 的时候,会执行 warnAboutAccessingKey 函数,进行报错,从而获取不到 key 属性的值。
即如下调用始终返回 undefined:
props.key

给 props 对象定义 key 属性,以及 key 属性的取值器为 warnAboutAccessingKey 对象该对象上存在一个 isReactWarning 为 true 的标志,在 hasValidKey 上就是通过 isReactWarning 来判断获取 key 是否合法 specialPropKeyWarningShown 用于标记 key 不合法的错误信息是否已经显示,初始值为 undefined。
function defineKeyPropWarningGetter(props, displayName) {
const warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
warningWithoutStack(
false,
‘%s: `key` is not a prop. Trying to access it will result ‘ +
‘in `undefined` being returned. If you need to access the same ‘ +
‘value within the child component, you should pass it as a different ‘ +
‘prop. (https://fb.me/react-special-props)’,
displayName,
);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, ‘key’, {
get: warnAboutAccessingKey,
configurable: true,
});
}

defineRefPropWarningGetter
逻辑与 defineKeyPropWarningGetter 一致,锁定 props.ref 的值使得无法获取 props.ref, 标记获取 props 中的 ref 值是不合法的,当使用 props.ref 的时候,会执行 warnAboutAccessingKey 函数,进行报错,从而获取不到 ref 属性的值。
即如下调用始终返回 undefined:
props.ref

ReactElement
被 createElement 函数调用,根据环境设置对应的属性。
代码性能优化:为提高测试环境下,element 比较速度,将 element 的一些属性配置为不可数,for…in 还是 Object.keys 都无法获取这些属性,提高了速度。
开发环境比生产环境多了_store,_self,_source 属性,并且 props 以及 element 被冻结,无法修改配置。
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,

// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,

// Record the component responsible for creating this element.
_owner: owner,
};

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

// To make comparing ReactElements easier for testing purposes, we make
// the validation flag non-enumerable (where possible, which should
// include every environment we run tests in), so the test framework
// ignores it.
Object.defineProperty(element._store, ‘validated’, {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
// self and source are DEV only properties.
Object.defineProperty(element, ‘_self’, {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
// Two elements created in two different places should be considered
// equal for testing purposes and therefore we hide it from enumeration.
Object.defineProperty(element, ‘_source’, {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}

return element;
};

createElement
在开发模式和生产模式下,第二参数 props 中的 ref 与 key 属性不会传入新 react 元素的 props 上,所以开发模式和生产模式都无法通过 props 传递 ref 与 key。生产模式下 ref 与 key 不为 undefined 就赋值给新 react 元素对应的 ref 与 key 属性上,开发模式下获取 ref 与 key 是合法的(第二参数不是某个 react 元素的 props,其 key 与 ref 则为合法),则赋值给新 react 元素对应的 ref 与 key 属性上。
使用 JSX 编写的代码将被转成使用 React.createElement()
React.createElement API:
React.createElement(
type,
[props],
[…children]
)

type(类型) 参数:可以是一个标签名字字符串(例如 ‘div’ 或 ’span’),或者是一个 React 组件 类型(一个类或者是函数),或者一个 React fragment 类型。
仅在开发模式下获取 props 中的 ref 与 key 会抛出错误
props:将 key,ref,__self,__source 的属性分别复制到新 react 元素的 key,ref,__self,__source 上,其他的属性值,assign 到 type 上的 props 上。当这个 props 是 react 元素的 props,那么其 ref 与 key 是无法传入新元素上的 ref 与 key。只有这个 props 是一个新对象的时候才是有效的。这里就切断了 ref 与 key 通过 props 的传递。
children:当 children 存在的时候,createElement 返回的组件的 props 中不会存在 children,如果存在的时候,返回的组件的 props.children 会被传入的 children 覆盖掉。
参数中的 children 覆盖顺序
如下:
// 创建 Footer
class Footer extends React.Component{
constructor(props){
super(props)
}
render(){
return (
<div>
this is Footer {this.props.children}
</div>
)
}
}

// 创建 FooterEnhance
const FooterEnhance = React.createElement(Footer, null ,”0000000″);

// 使用 Footer 与 FooterEnhance
<div>
<Footer>aaaaa</Footer>
{FooterEnhance}
</div>

结果:
this is Footer aaaaa
this is Footer 0000000

可以看到:
第三个参数 children 覆盖掉原来的 children:aaaaa

由下面源码也可知道:

第三个参数 children 也可以覆盖第二参数中的 children,测试很简单。
第二个参数 props 中的 children 会覆盖掉原来组件中的 props.children

返回值的使用:如 {FooterEnhance}。不能当做普通组件使用。

源码
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};

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;

// 将 config 上有但是 RESERVED_PROPS 上没有的属性,添加到 props 上
// 将 config 上合法的 ref 与 key 保存到内部变量 ref 和 key
if (config != null) {
// 判断 config 是否具有合法的 ref 与 key,有就保存到内部变量 ref 和 key 中
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
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
// 将 config 上的属性值保存到 props 的 propName 属性上
for (propName in config) {
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.
// 如果只有三个参数,将第三个参数直接覆盖到 props.children 上
// 如果不止三个参数,将后面的参数组成一个数组,覆盖到 props.children 上
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__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}

// Resolve default props
// 如果有默认的 props 值,那么将 props 上为 undefined 的属性设置初始值
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 开发环境下
if (__DEV__) {
// 需要利用 defineKeyPropWarningGetter 与 defineRefPropWarningGetter 标记新组件上的 props 也就是这里的 props 上的 ref 与 key 在获取其值得时候是不合法的。
if (key || ref) {
//type 如果是个函数说明不是原生的 dom 标签,可能是一个组件,那么可以取
const displayName =
typeof type === ‘function’
? type.displayName || type.name || ‘Unknown’
: type;
if (key) {
// 在开发环境下标记获取新组件的 props.key 是不合法的, 获取不到值
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
// 在开发环境下标记获取新组件的 props.ref 是不合法的, 获取不到值
defineRefPropWarningGetter(props, displayName);
}
}
}
// 注意生产环境下的 ref 和 key 还是被赋值到组件上
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}

createFactory
返回一个函数,该函数生成给定类型的 React 元素。用于将在字符串或者函数或者类转换成一个 react 元素,该元素的 type 为字符串或者函数或者类的构造函数
例如:Footer 为文章的类组件
console.log(React.createFactory(‘div’)())
console.log(React.createFactory(Footer)())

返回的结果分别为:
$$typeof:Symbol(react.element)
key:null
props:{}
ref:null
type:”div”
_owner:null
_store:{validated: false}
_self:null
_source:null

$$typeof:Symbol(react.element)
key:null
props:{}
ref:null
type:ƒ Footer(props)
_owner:null
_store:{validated: false}
_self:null
_source:null

源码:
export function createFactory(type) {
const factory = createElement.bind(null, type);
factory.type = type;
return factory;
}

cloneAndReplaceKey
克隆一个旧的 react 元素,得到的新的 react 元素被设置了新的 key
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);

return newElement;
}
isValidElement
判断一个对象是否是合法的 react 元素,即判断其 $$typeof 属性是否为 REACT_ELEMENT_TYPE
export function isValidElement(object) {
return (
typeof object === ‘object’ &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}

cloneElement
cloneElement 官方 API 介绍

React.cloneElement(
element,
[props],
[…children]
)

使用 element 作为起点,克隆并返回一个新的 React 元素。所产生的元素的 props 由原始元素的 props 被新的 props 浅层合并而来,并且最终合并后的 props 的属性为 undefined,就用 element.type.defaultProps 也就是默认 props 值进行设置。如果 props 不是 react 元素的 props,呢么 props 中的 key 和 ref 将被存放在返回的新元素的 key 与 ref 上。
返回的元素相当于:
<element.type {…element.props} {…props}>{children}</element.type>

其源码与 createElement 类似,不同的地方是在开发环境下 cloneElement 不会对 props 调用 defineKeyPropWarningGetter 与 defineRefPropWarningGetter 对 props.ref 与 props.key 进行获取拦截。
总结
react 元素的 key 和 ref 为什么不会在 props 上,并且传递,开发环境下与生产环境下处理 key 和 ref 的区别?
creatElement 函数中阻止 ref、key 等属性赋值给 props,所以 react 元素的 key 和 ref 不会在 props 上,并且在组件间通过 props 传递
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}

开发环境下与生产环境下处理 key 和 ref 的区别:开发环境下还会调用 defineRefPropWarningGetter 与 defineKeyPropWarningGetter,利用 Object.defineProperty 进行拦截报错:
Object.defineProperty(props, ‘key’, {
get: warnAboutAccessingKey,
configurable: true,
});

不能将一个 react 元素的 ref 通过 props 传递给其他组件。

正文完
 0