ReactElement源码解析

30次阅读

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

前言
ReactElement 并不像之前所谈的 PureComponent 和 Component 那样被频繁的显示使用,但我估计他应该是在 react 暴露出的 api 中被调用最为频繁的,关于此看完后面便知。ReactElement 中暴露出 createElement,createFactory,cloneElement,isValidElement,cloneAndReplaceKey 五个方法,总共 400 来行代码,比较容易。
文章中如有不当之处,欢迎交流指点。react 版本 16.8.2。在源码添加的注释在 githubreact-source-learn 的 learn 分支。
jsx 与 ReactElement
在使用 react 时我们经常在 render 方法返回(函数组件的话可能是直接返回)类似下面的代码。
<Wrap>
<h1> 测试 </h1>
<List />
<Footer />
</Wrap>
这就是传说中的 jsx 语法,js 并没有这种东西,这种语法最终都会被转换成标准的 js。请看下图:

发现这些 jsx 被转化成了 js,每个组件或者 html 标签转化后都是调用 React.createElement(type, config, children)。这里的 React.createElement 其实就是 ReactElement.createElement。由此可以推测,ReactElement 暴露的方法是调用最频繁的。
createElement 解析
createElement 的主要调用如下:
createElement -> ReactElement
当然在 dev 下还有些其他的调用。
createElement 源码如下
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/

// jsx 转换后调用的方法
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;

if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = ” + config.key;
}

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 中, key,ref,__self,__source 除外
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.
// 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
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 这里不然从 props 中读 key, 和 ref, 但是里边事实上就是没有的
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === ‘function’
? type.displayName || type.name || ‘Unknown’
: type;
if (key) {
// displayName: 构造函数名, 或标签名 a , h1
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
// 就一个普通对象
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
createElement 主要做了如下事情:

将特殊属性从 config 取出,如 key,ref,__self,__source
将非特殊属性挂到 props 上,比如上边那个图中的 className
将第三个及之后的参数挂到 props.children 上,多个是生成数组,单个是直接挂
默认值 defaultProps 的处理
将处理好的数据作为参数调用 ReactElement 并返回

ReactElement 源码如下
// 这个函数做的事非常简单, 就是将传进来的参放到一个对象里边返回
// 其中 source, self 在生产模式没有返回
// owner 变成了_owner
// 开发模式下, 返回了将 source, self 也挂在了返回的对象上, 变成了_source, _self
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, // 一个 Symobol 或者 16 进制数,
// 用于表示 ReactElement 类型

// 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,
};

// 这里边放了 self, source
if (__DEV__) {
// The validation flag is currently mutative. We put it on
// an external backing store so that we can freeze the whole object.
// This can be replaced with a WeakMap once they are implemented in
// commonly used development environments.
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,
});

// Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;
// 冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、
// 可配置性、可写性,以及不能修改已有属性的值。
// 此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

// Object.seal() 方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。

// Object.preventExtensions() 方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

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

return element;
};
他几乎是没做什么事情的,就是将传入的参数放到一个对象返回,加了一个 $$typeof 标识 ReactElement。其中使用了一个 Object.freeze 方法,这个方法不太常用,意思是冻结一个对象,使其不能被修改,相关的还有 Object.seal,Object.preventExtensions,可以找些文档了解下。
小结下 ReactElement.createElement
ReactElement.createElement 最终返回的是一个普通的对象,对参数进行了校验,提取等操作。上面为解析 dev 下的代码,去看一下会发现也是比较有趣的。
createFactory,cloneAndReplaceKey,cloneElement 和 isValidElement
createFactory
export function createFactory(type) {
const factory = createElement.bind(null, type);
// Expose the type on the factory and the prototype so that it can be
// easily accessed on elements. E.g. `<Foo />.type === Foo`.
// This should not be named `constructor` since this may not be the function
// that created the element, and it may not even be a constructor.
// Legacy hook: remove it
factory.type = type;
return factory;

// 这样
// return function factory(…args) {
// return createElement(type, …args);
// }
}
这个方法很简单,是对 createElement 的一个柯里化的操作。
cloneAndReplaceKey
// 克隆 reactElement 并将 key 改为新 key
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);

return newElement;
}
从旧的 ReactElement 对象生成一个新的,将 key 属性替换成新的
isValidElement
/**
* Verifies the object is a ReactElement.
* See https://reactjs.org/docs/react-api.html#isvalidelement
* @param {?object} object
* @return {boolean} True if `object` is a ReactElement.
* @final
*/
export function isValidElement(object) {
// 还是很严谨的
return (
typeof object === ‘object’ &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
判断一个值是不是 ReactElement, 使用了创建时挂上去的 $$typeof
cloneElement
// 和 createElement 基本相同
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;

// Original props are copied
const props = Object.assign({}, element.props);

// Reserved names are extracted
let key = element.key;
let ref = element.ref;
// Self is preserved since the owner is preserved.
const self = element._self;
// Source is preserved since cloneElement is unlikely to be targeted by a
// transpiler, and the original source is probably a better indicator of the
// true owner.
const source = element._source;

// Owner will be preserved, unless ref is overridden
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;
}

// Remaining properties override existing 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[propName] = defaultProps[propName];
} else {
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);
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);
}
这段代码和 createElement 非常相似,不同之处在于他是返回第一个参数 ReactElement 的一个副本。他的 key,ref 等属性和提供的需要被克隆的 ReactElement 的相同,props 也是原来的 props,但是可以传入 config 修改。
/packages/react.js 小结
至此,/packages/react.js 总的最最重要的东西已经分析完了,关于 hooks 等其他内容就像不分析了。这里边的代码其实并没有做什神奇的事情,ReactElement 只是创建和操作普通对象,Component 和 PureComponent 只是定义了两个简单的构造函数,定义了几个方法,其中比较重要的应该是 updater,但是到目前为止还没有看到他的身影。这些东西都不涉及 dom 操作,是平台无关的。
这里代码都比较好理解,后面就将进入深水区了,要开始研究 ReactDOM 里边的 render 了。加油!

正文完
 0