本文将通过一个简单的小例子介绍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.createElement。React.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
}