createElement
用于创建并返回一个新的ReactElement
元素createFactory
用于返回固定类的createElement
方法 【已废弃】cloneElement
克隆一个元素isValidElement
验证一个对象是否为ReactElement
。cloneAndReplaceKey
使用给定key
返回一个新的ReactElement
类型
源码
包:packages/react/src/ReactElement.js
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */import invariant from 'shared/invariant';import warningWithoutStack from 'shared/warningWithoutStack';import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';import ReactCurrentOwner from './ReactCurrentOwner';const hasOwnProperty = Object.prototype.hasOwnProperty;const RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true,};let specialPropKeyWarningShown, specialPropRefWarningShown;function hasValidRef(config) { if (__DEV__) { if (hasOwnProperty.call(config, 'ref')) { const getter = Object.getOwnPropertyDescriptor(config, 'ref').get; if (getter && getter.isReactWarning) { return false; } } } return config.ref !== undefined;}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;}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, });}function defineRefPropWarningGetter(props, displayName) { const warnAboutAccessingRef = function() { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; warningWithoutStack( false, '%s: `ref` 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, ); } }; warnAboutAccessingRef.isReactWarning = true; Object.defineProperty(props, 'ref', { get: warnAboutAccessingRef, configurable: true, });}/** * Factory method to create a new React element. This no longer adheres to * the class pattern, so do not use new to call it. Also, no instanceof check * will work. Instead test $$typeof field against Symbol.for('react.element') to check * if something is a React Element. * * @param {*} type * @param {*} key * @param {string|object} ref * @param {*} self A *temporary* helper to detect places where `this` is * different from the `owner` when React.createElement is called, so that we * can warn. We want to get rid of owner and replace string `ref`s with arrow * functions, and as long as `this` and owner are the same, there will be no * change in behavior. * @param {*} source An annotation object (added by a transpiler or otherwise) * indicating filename, line number, and/or other information. * @param {*} owner * @param {*} props * @internal */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__) { // 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, }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element;};/** * Create and return a new ReactElement of the given type. * See https://reactjs.org/docs/react-api.html#createelement */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 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. 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 if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } if (__DEV__) { if (key || ref) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );}/** * Return a function that produces ReactElements of a given type. * See https://reactjs.org/docs/react-api.html#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. `.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;}export function cloneAndReplaceKey(oldElement, newKey) { const newElement = ReactElement( oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props, ); return newElement;}/** * Clone and return a new ReactElement using element as the starting point. * See https://reactjs.org/docs/react-api.html#cloneelement */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);}/** * 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
/** * * 用来创建 React 元素的工厂方法。 * 不再遵循类的模式,因为不能使用 new 来调用它,instance 检查无效。 * 取而代之,可以检测 $$typeof 字段与 Symbol.for('react.element') 是否匹配来判断是否是一个 React 元素 * * @param {*} type * @param {*} key * @param {string|object} ref * @param {*} self A *temporary* helper to detect places where `this` is * different from the `owner` when React.createElement is called, so that we * can warn. We want to get rid of owner and replace string `ref`s with arrow * functions, and as long as `this` and owner are the same, there will be no * change in behavior. * @param {*} source An annotation object (added by a transpiler or otherwise) * indicating filename, line number, and/or other information. * @param {*} owner * @param {*} props * @internal */const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // 这个标记允许我们唯一地将其标识为 React 元素 $$typeof: REACT_ELEMENT_TYPE, // 元素的内置属性 type: type, key: key, ref: ref, props: props, // 记录负责创建此元素的组件. _owner: owner, }; 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, }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element;};复制代码
createElement
React.createElement
函数在 React 中具有举足轻重的地位,我们的组件最后都会编译成它。
/** * 使用给定 type 创建并返回的新的 React 元素。 * See https://reactjs.org/docs/react-api.html#createelement */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; // 剩余的属性被添加到一个新的 props 对象中 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__) { if (Object.freeze) { Object.freeze(childArray); } } 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__) { if (key || ref) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );}复制代码
来看看一个示例:
React.createElement('div');复制代码
返回一个对象,对象如下:
可以看到 就是创建了一个普通的 javascript 对象。这时候,产生了几个疑问:
- 这个普普通通的 javascript 对象是如何变成了我们页面的 DOM 结构的
- DOM 层级如此之多,信息之复杂,React 又是如何实现的?
- 上一章节中讲到的
Component
,它经过编译后传递给了React.createElement
方法什么样的参数,类中的实例属性和原型方法如何进行了传递。
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 );}复制代码
React 如何实现 DOM 层级
来看下面的 Hello
组件:
function Hello(props) { function sayHi() { return (Hi); } function handleClick(e) { console.log('click'); } return ({ sayHi() } Hello { props.name });}复制代码React
React is cool!
concatcompany info
MyFooter
组件:
function MyFooter(props) { return ({ props.children });}复制代码
我们采用了函数式组件的方式,他们将分别被编译为:
// 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.name
React.createElement(...)
React.createElement(...)
React.createElement(...)
- 自定义组件的
type
参数值是组件的直接引用,而不是组件名的字符串。MyFooter 组件的 type 属性是一个函数,是它本身,而不是"MyFooter"
字符串。 - 只要是写在 JSX 上的属性,都被当做
config
的一部分传递给了React.createElement()
,包括事件,例如这里的onClick
再看看生成的 JavaScript 对象:
所谓的层级结构就是通过 React 元素的 props
属性中的 children
属性层层嵌套得来的。
class 组件的编译
还是使用刚才的 Hello 组件,我们把它改造成 class 组件:
class Hello extends React.Component { handleClick(e) { console.log('click'); } render() { function sayHi() { return (Hi); } return ({ sayHi() } Hello { this.props.name }); }}复制代码React
React is cool!
concatcompany info
编译结果:
class Hello extends React.Component { handleClick(e) { console.log('click'); } render() { function sayHi() { return React.createElement("div", null, "Hi"); } return React.createElement("div", null, sayHi(), "Hello ", this.props.name, React.createElement("p", { className: "title", onClick: this.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"))); }}复制代码
可以看到,编译器并不会处理类本身,而是将其中的创建 React 元素的语句全局转换成了 React.createElement
函数调用的方式。
这时候发现自己以前的知识有误区,遂查看了 官方文档,描述中写着:
Turn JSX into React function calls
将 JSX 转换为 React 函数调用,那到底什么是 JSX?是函数组件?class 组件?的所有代码都是 JSX 吗?
什么是 JSX
官网例子:
const element =Hello, world!
;复制代码这个有趣的标签语法既不是字符串也不是 HTML。
这个看起来像 HTML 的东西就是 JSX。
且看下面代码:
function getGreeting(user) { if (user) { returnHello, {formatName(user)}!
; } returnHello, Stranger.
;}复制代码
这段代码叫 JSX,答案是否定的。它不过是一个普通的函数声明,不过是将两句 JSX 当做了表达式。也就是说,其中的 <h1>Hello, {formatName(user)}!</h1>
和 <h1>Hello, Stranger.</h1>
这两部分才能称为 JSX。
最后它们将被编译为:
function getGreeting(user) { if (user) { return React.createElement("h1", null, "Hello, ", formatName(user), "!"); } return React.createElement("h1", null, "Hello, Stranger.");}复制代码
遗留问题
- 普通的 javascript 对象(React 元素)是如何变成了我们页面的 DOM 结构的
写文章的时候再阅读 React 的官方文档,发现这份文档制作很用心,循序渐进,由浅入深。
借用文档的一句话:
我们将在下一章节中探讨如何将 React 元素渲染为 DOM。