前言
React.createElement
是React中一种创建React组件的方式,它古老而神秘。
虽然日常开发中已经很少能够见到他的身影。但是将JSX用babel编译之后,就是 createElement
函数
ReactDOM.render
是React实例渲染到dom的入口方法
React.createElement
参数
createElement
支持传入n个参数。
- type:表示你要渲染的元素类型。这里可以传入一个元素Tag名称,也可以传入一个组件(如div span ul li 等,也可以是是函数组件和类组件)
- config:创建React元素所需要的props。包含 style,className 等
- children:要渲染元素的子元素,这里可以向后传入n个参数。参数类型皆为
React.createElement
返回的React元素对象。
React.createElement(type, config, children1, children2, children3...);
createElement 方法
我们新建一个JS文件,导出一个 createElement 函数。
方法内置一个props
变量。将我们的config
对象本身所有的属性完全copy到 props
上
function createElement(type, config, children) {
const props = {};
for (let propName in config) {
// 如果对象本身存在该属性值,就copy
if (Object.prototype.hasOwnProperty.call(config, propName)) {
props[propName] = config[propName];
}
}
}
export default {
createElement,
}
接着开始处理子元素。由于子元素的参数位置在 第2个 及其之后,所以我们需要用到函数的 arguments
对象获取参数值。
在 createElement 中声明一个 childrenLength
变量,值为 arguments.length - 2
- 如果
childrenLength
=== 1,也就是子元素只有1个,就将唯一的子元素挂到props.children
上面。 - 如果
childrenLength
> 1,那就从第二个参数向后截取arguments
对象。
这里可以使用 Array.prototype.slice.call
进行截取,当然也可以使用 React 的官方写法。如下方代码注释:
// 获得子元素长度
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 1. React 官方实现,声明一个和 childrenLength 一样长的数组
// 然后遍历 arguments对象,把第二个之后的参数项逐个赋值给 childrenArray
let childrenArray = Array(childrenLength);
for (let i = 0; i < arguments.length; i++) {
childrenArray[i] = arguments[i + 2]
}
props.children = childrenArray;
// 2. 数组slice截取,截取第二个之后所有的参数项给props.children
props.children = Array.prototype.slice.call(arguments, 2);
}
最后,我们返回 ReactElement(type, props)
工厂函数,React元素对象创建完成。
ReactElement 方法
ReactElement
方法是一个工厂函数,可以包装一个React虚拟Dom对象。
这里实现也很简单,只需要返回一个对象即可:
function ReactElement(type, props) {
return {
?typeof: REACT_ELEMENT_TYPE,
type,
props
}
}
?typeof: REACT_ELEMENT_TYPE
?typeof: REACT_ELEMENT_TYPE
是React元素对象的标识属性
REACT_ELEMENT_TYPE
的值是一个Symbol类型,代表了一个独一无二的值。如果浏览器不支持
Symbol类型,值就是一个二进制值。
为什么是 Symbol?主要防止XSS攻击伪造一个假的React组件。因为JSON中是不会存在Symbol类型的。
为什么是 0xeac7
?因为 0xeac7
和单词 React 长得很像。
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
const REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;
这样我们的 React.createElement
方法就实现了。
使用示例
import React from './react';
import ReactDOM from 'react-dom';
let apple = React.createElement('li', { id: 'apple' }, 'apple');
let banana = React.createElement('li', { id: 'banana' }, 'banana');
let list = React.createElement('ul', {id: 'list'}, apple, banana);
ReactDOM.render(list, document.getElementById('root'));
完整实现
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props
}
}
const hasSymbol = typeof Symbol === 'function' && Symbol.for; // 浏览器是否支持 Symbol
// 支持Symbol的话,就创建一个Symbol类型的标识,否则就以二进制 0xeac7代替。
// 为什么是 Symbol?主要防止xss攻击伪造一个fake的react组件。因为json中是不会存在symbol的.
// 为什么是 二进制 0xeac7 ?因为 0xeac7 和单词 React长得很像。
const REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;
function ReactElement(type, props) {
return {
?typeof: REACT_ELEMENT_TYPE,
type,
props
}
}
function createElement(type, config, children) {
const props = {};
for (let propName in config) {
// 如果对象本身存在该属性值,就copy
if (Object.prototype.hasOwnProperty.call(config, propName)) {
props[propName] = config[propName];
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 1. React 官方实现,声明一个和childrenLength一样长的数组
// 然后遍历 arguments对象,把第二个之后的参数项给 childrenArray
// let childrenArray = Array(childrenLength);
// for (let i = 0; i < arguments.length; i++) {
// childrenArray[i] = arguments[i + 2]
// }
// props.children = childrenArray;
// 2. 数组slice截取,截取第二个之后所有的参数项给props.children
props.children = Array.prototype.slice.call(arguments, 2);
}
return ReactElement(type, props)
}
export default {
createElement,
Component
}
官方源码
ReactDOM.render
参数
render
支持传入2个参数。
- node:React组件
- mountNode:要挂载的DOM对象
ReactDOM.render(node, mountNode);
render 方法
新建一个JS文件,导出一个 render 函数。
先判断,如果传入的组件是一个字符串,直接使用 document.createTextNode
方法创建一个文本节点,拼接在要挂载的DOM元素里面。
新开辟 type
和 props
两个变量,将组件元素内的 type
和 props
赋值上去
function render(node, mountNode) {
if (typeof node === 'string') { // 如果是字符串
return mountNode.append(document.createTextNode(node))
}
let type = node.type;
let props = node.props;
}
export default {
render
}
接着,使用 type
创建一个相应的DOM元素节点,遍历 props
中的属性。
属性名为 children
,判断 children 是不是一个数组。如果不是的话,将其包装为一个数组。如果是的话,遍历子元素,并递归调用 render
函数
如果是style,代表是行内样式。将style内的css对象,逐个复制到domElement.style上面
let domElement = document.createElement(type);
for (let propName in props) {
if (propName === 'children') {
let children = props[propName];
children = Array.isArray(children) ? children : [children];
children.forEach(child => render(child, domElement))
} else if (propName === 'style') {
let styleObj = props[propName];
for (let attr in styleObj) {
domElement.style[attr] = styleObj[attr];
}
}
}
最后,调用 mountNode.appendChild
方法,将处理好的元素挂载到dom元素上
mountNode.appendChild(domElement);
函数组件的处理
我们可以判断 type
的值是否为function。如果是function,执行函数。将执行后的返回值上的 props type属性赋值给 props
和 type
变量
let type = node.type;
let props = node.props;
if (typeof type === 'function') {
let element = type(props); // 执行函数
props = element.props;
type = element.type;
}
let domElement = document.createElement(type);
类组件的处理
我们新建一个 Componet
类,模拟 React.Component
类的实现
Componet
类中有一个 isReactComponent
的静态属性,代表该类为一个React类组件。
class Component {
// 是否为React组件
static isReactComponent = true;
constructor(props) {
this.props = props
}
}
我们可以判断 type
上的 isReactComponent
是否为true。如果为true,代表该元素为一个 类组件。
先使用new实例化类组件,然后调用render方法获取到React元素对象。
let type = node.type;
let props = node.props;
// 是否为类组件
if (type.isReactComponent) {
//传入props,并实例化,调用render方法
let element = new type(props).render();
props = element.props;
type = element.type;
}
使用示例
React.createElement
let apple = React.createElement('li', { id: 'apple' }, 'apple');
let banana = React.createElement('li', { id: 'banana' }, 'banana');
let list = React.createElement('ul', {id: 'list'}, apple, banana);
ReactDOM.render(list, document.getElementById('root'));
函数组件
function list(props) {
return (
<ul>
<li style={{color: props.color}}>banana</li>
<li style={{color: props.color}}>apple</li>
</ul>
)
}
ReactDOM.render(
React.createElement(List, {
color: 'red'
}),
document.getElementById('root')
);
类组件
class List extends React.Component {
render() {
return (
<ul>
<Item name={'banana'}/>
<Item name={'Apple'}/>
</ul>
);
}
}
class Item extends React.Component {
render() {
return (
<li>{this.props.name}</li>
)
}
}
ReactDOM.render(React.createElement(List), document.getElementById('root'));
完整实现
function render(node, mountNode) {
if (typeof node === 'string') {
return mountNode.append(document.createTextNode(node))
}
let type = node.type;
let props = node.props;
if (type.isReactComponent) {
let element = new type(props).render();
props = element.props;
type = element.type;
} else if (typeof type === 'function') {
let element = type(props);
props = element.props;
type = element.type;
}
let domElement = document.createElement(type);
for (let propName in props) {
if (propName === 'children') {
let children = props[propName];
children = Array.isArray(children) ? children : [children];
children.forEach(child => render(child, domElement))
} else if (propName === 'style') {
let styleObj = props[propName];
for (let attr in styleObj) {
domElement.style[attr] = styleObj[attr];
}
}
}
mountNode.appendChild(domElement);
}
export default {
render
}