React.createElement 和 ReactDOM.render 的简易实现

4,960

前言

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
}

官方源码

Github地址

ReactDOM.render

参数

render 支持传入2个参数。

  • node:React组件
  • mountNode:要挂载的DOM对象
ReactDOM.render(node, mountNode);

render 方法

新建一个JS文件,导出一个 render 函数。

先判断,如果传入的组件是一个字符串,直接使用 document.createTextNode方法创建一个文本节点,拼接在要挂载的DOM元素里面。

新开辟 typeprops 两个变量,将组件元素内的 typeprops赋值上去

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属性赋值给 propstype 变量

    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
}