React.createElement
是React中一种创建React组件的方式,它古老而神秘。
虽然日常开发中已经很少能够见到他的身影。但是将JSX用babel编译之后,就是 createElement
函数
ReactDOM.render
是React实例渲染到dom的入口方法
createElement
支持传入n个参数。
React.createElement
返回的React元素对象。React.createElement(type, config, children1, children2, children3...); 复制代码
我们新建一个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
方法是一个工厂函数,可以包装一个React虚拟Dom对象。
这里实现也很简单,只需要返回一个对象即可:
function ReactElement(type, props) { return { $$typeof: REACT_ELEMENT_TYPE, type, props } } 复制代码
$$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 } 复制代码
Github地址
render
支持传入2个参数。
ReactDOM.render(node, mountNode); 复制代码
新建一个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; } 复制代码
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 } 复制代码