手写react核心Api(createElement、render、Component),react原理解析(一)

1,056 阅读7分钟

本文将通过一个简单的小例子介绍react的原理,并手写实现react的几个核心Api,包括:

  • React.createElement
  • ReactDOM.render
  • Component

我们先看下一个最简单的hello world的例子,代码如下:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <div className="a_calss_name">
    hello world
  </div>,
  document.getElementById('root')
);

jsx

我们的UI是用JSX语法来写的,JSX语法实际在编译的过程中,会通过bable转换成原生的js,例如我们上面的例子:

<div className="a_calss_name">
    hello world
</div>

编译后实际的代码是:

React.createElement("div", {
  className: "a_calss_name"
}, "hello world");

大家可以通过这个在线babel编译器进行编译查看。

React.createElement

所以说我们编写的jsx实际等同于写React.createElementReact.createElement执行后生成的是一个虚拟dom(vnode),生成的虚拟dom用来完整描述我们上面写的这个jsx。React.createElement接收的第一个参数是节点的类型,第二个参数是传给该节点的props,后面的参数是它的children。这些参数是babel对jsx解析的时候解析出来的,涉及到编译原理的知识这里就不介绍了,不是本文的重点。

React.createElement(
    "div",  // 节点类型
    { className: "a_calss_name" }, // props
    "hello world" // children
);

执行后生成虚拟dom:

vnode:
{
    type: "div",
    props: {
        className: "a_calss_name",
        children: {
            type: "TEXT",
            props: {
                nodeValue: "hello world",
                children: []
            }
        }
    }
}

然后ReactDOM.render方法接收该生成的Vnode,并将该Vnode转换为真正的node插入到页面上。

我们先来实现react的createElement方法,createElement方法的作用就是接收type、props和children参数,然后返回一个vnode,实现代码如下:

// react.js

// 返回虚拟dom
// vnode的每一个节点的格式都是
//{
//    type: ..., // 节点类型
//    props: {
//        ..., // props
//        children: [...] // 子元素
//    }
//}

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                // 如果child不是对象则说明是文本,将文本节点的vnode格式处理成和元素节点一样
                // 方便后面统一处理
                return typeof child === 'object' ? child : createTextNode(child)
            })
        }
    }
}

// 将文本节点的vnode格式处理成和元素节点一样,方便后续统一处理
function createTextNode(text) {
    return {
        type: 'TEXT',
        props: {
            children: [],
            nodeValue: text
        }
    }
}

export default {
    createElement
}

经过上面的createElement方法,我们就可以将我们的jsx转化为vnode了,再来梳理下这个流程:

<div className="a_calss_name">
    hello world
</div>

babel编译后实际的代码是:

React.createElement("div", {
  className: "a_calss_name"
}, "hello world");

React.createElement执行后返回vnode:

{
    type: "div",
    props: {
        className: "a_calss_name",
        children: {
            type: "TEXT",
            props: {
                nodeValue: "hello world",
                children: []
            }
        }
    }
}

以上就是我们书写的jsx通过babel编译成原生js,再通过React.createElement生成vnode的原理,下面我们再来看下得到vnode后如何生成真实的dom。

ReactDOM.render

ReactDOM.render接收两个参数,第一个参数是上面生成的vnode,第二个参数是container(也可以叫做parent node),ReactDOM.render会将接收到的第一个参数vnode生成一个真实node,并将该node添加到container中,这样就实现了:

jsx ==> 虚拟dom ==> 真实dom ==> 添加到页面中

的整个流程。

我们来看下 ReactDOM.render 是怎么实现的:

// React-dom 大致框架
// vnode表示虚拟dom
// node表示真实dom

function render (vnode, container) {
    // 将虚拟dom转化为真实dom
    let node = createNode(vnode);
    
    // 将真实dom添加到父节点中
    container.appendChild(node);
}

export default {
    render
}

从以上代码可以知道ReactDOM的大致实现流程,再来看下 createNode是如何实现的:

// createNode
// 作用:vnode ==> node

function createNode (vnode) {
    let {type, props} = vnode;
    let node;
    
    if (type === 'TEXT') { // 如果是文本,则创建文本节点
        node = document.createTextNode("");
    } else { // 否则认为是元素,创建元素节点
        node = document.createElement(type);
    }

    // 将props上的相关属性添加到节点上
    updateNode(node, props);
    
    // 对子元素(子vnode)进行递归,执行相同的操作
    // 子vnode ==》 子node ==》 将子node append 到node
    reconcilerChildren(props.children, node);
    
    return node;
}

// 将props上的属性添加到元素上
function updateNode (node, props) {
    Object.keys(props)
        .filter(key => key !== 'children') // 过滤掉props上的children属性,该属性不用添加到元素上
        .forEach(key => {
            // 将props上的属性添加到元素上,例如className
            // 这里实现的比较简单,实际有些属性要用setAttribute方法来添加
            node[key] = props[key]
        })
}

// 对children进行递归
function reconcilerChildren(children, parentNode) {
    children.forEach(child => {
        render(child, parentNode);
    })
}

ReactDOM.render的完整代码:

function render (vnode, container) {
    let node = createNode(vnode);
    container.appendChild(node);
}

function createNode (vnode) {
    let {type, props} = vnode;
    let node;
    
    if (type === 'TEXT') {
        node = document.createTextNode("");
    } else {
        node = document.createElement(type);
    }

    updateNode(node, props);
    reconcilerChildren(props.children, node);
    return node;
}

function updateNode (node, props) {
    Object.keys(props)
        .filter(key => key !== 'children')
        .forEach(key => {
            node[key] = props[key]
        })
}

function reconcilerChildren(children, parentNode) {
    children.forEach(child => {
        render(child, parentNode);
    })
}


export default {
    render
}

上面就是我们实现的ReactDOM.render的简易版本,通过该版本我们应该就能理解ReactDOM.render的基本实现原理了,但是该版本实现的功能还是太简单了,对函数组件、类组件都没有处理,下面我们就来处理下,只要在上面版本的基础上再进行加强一下就可以啦。

ReactDOM.render处理组件

加上了函数式组件和class组件,现在我们上面的小例子变成了这样:

import React, {Component} from 'react';
import ReactDOM from 'react-dom';

// 函数组件
function FunCom (props) {
    return <div>this is a fun component, name: {props.name}</div>
}

// 类组件
class ClassCom  extends Component {
  render () {
    return <div>this is a class component, name: {this.props.name}</div>
  }
}

ReactDOM.render(
  <div className="a_calss_name">
    hello world
    <FunCom name="funName"></FunCom>
    <ClassCom name="className"></ClassCom>
  </div>,
  document.getElementById('root')
);

正常的页面输出为这样:

下面我们来看下如何对函数组件和类组件进行处理。

首先看下我们写的jsx现在编译后转换成了什么样子:

<div className="a_calss_name">
    hello world
    <FunCom name="funName"></FunCom>
    <ClassCom name="className"></ClassCom>
</div>

babel编译转换为:

React.createElement(
    "div", // 元素名称
    { className: "a_calss_name"}, // props
    "hello world", // 第一个child
    React.createElement(FunCom, { // 第二个child,即我们新增的fun组件,type为FunCom
      name: "funName"
    }), 
    React.createElement(ClassCom, { // 第三个child,即我们新增的class组件,type为ClassCom
      name: "className"
    })
);

可以看到就是在React.createElement方法里多添加了两个参数,即上面的第二个child和第三个child。第二个child和第三个child返回的都是该child对应的vnode。 函数式组件转化成vnode,其type为对应的函数,class组件vnode的type为对应的class,所以我们在ReactDOM.render函数中,针对type为函数式组件和class组件的情况要进行下处理,新增的代码如下,函数和组件typeof返回的值都是'function'

react-dom.js新增处理function组件和class组件的代码:

react.js新增的代码:

export class Component {
    static isReactComponent = {};
    constructor(props) {
      this.props = props;
    }
}

因为我们的class组件会继承Component,所以我们可以给通过class声明的组件统一设置一个static isReactComponent,我们通过判断组件是否有isReactComponent属性来判断是函数组件还是class组件,然后分别对两种组件进行对应的处理逻辑。

function组件: function组件的vnode里的type就是对应的function,我们最终要使用的vnode是通过执行function函数返回的vnode。class组件也是一样的道理,要得到的是通过执行class组件内部的render方法得到的vnode。

如上面的例子,type为对应的function,通过执行type(props)得到我们最终需要的vnode,然后再调用createNode把该vnode传进去,得到function组件内部的vnode对应的真实node。

class组件: 理解了上面的function组件,class组件就很好理解了,class组件的type指向对应的class,我们将该class实例化,然后调用实例的render方法,就能得到class组件的vnode了,然后再调用createNode方法把该vnode传进去得到真实的node。

react-dom完整代码

function render (vnode, container) {
    let node = createNode(vnode);
    container.appendChild(node);
}

function createNode (vnode) {
    let {type, props} = vnode;
    let node;
    if (typeof type === 'function') {
        // 函数式组件
        if (!type.isReactComponent) {
            node = createNode(type(props));
        } else { // class组件
            let instance = new type(props);
            node = createNode(instance.render())
        }    
    } else if (type === 'TEXT') {
        node = document.createTextNode("");
    } else {
        node = document.createElement(type);
    }

    updateNode(node, props);
    reconcilerChildren(props.children, node);
    return node;
}

function updateNode (node, props) {
    Object.keys(props)
        .filter(key => key !== 'children')
        .forEach(key => {
            node[key] = props[key]
        })
}

function reconcilerChildren(children, parentNode) {
    children.forEach(child => {
        render(child, parentNode);
    })
}


export default {
    render
}

react.js完整代码

// 返回虚拟dom
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return typeof child === 'object' ? child : createTextNode(child)
            })
        }
    }
}

function createTextNode(text) {
    return {
        type: 'TEXT',
        props: {
            children: [],
            nodeValue: text
        }
    }
}

export class Component {
    static isReactComponent = {};
    constructor(props) {
      this.props = props;
    }
}

export default {
    createElement
}