# 前言
本次仅针对React 15.8之前的代码原理解析说明,忽略fiber,事务机制,时间分片等底层操作,仅根据原理实现react基础的核心思想
# React原理分析
# JSX:
react中所有的类似html代码到最终都会被babel编译为createElement的形式,createElement实现。
<div>
<a>
target
</a>
</div>
使用babel编译后:
React.createElement("div", null, React.createElement("a", null, "target"));
# 组件:
在React中,组件分为两种,即函数组件和类组件。 我们来看看函数式组件,babel会编译成什么样子
function test(){
let name="CJ";
return(
<div>
{name}
</div>
)
}
babel编译后:
"use strict";
function test() {
var name = "CJ";
return React.createElement("div", null, name);
}
//这样就能很好的解释为什么return内含的变量和函数能够被渲染和执行
我们来看看更复杂的函数组件,含有map遍历和传递参数
function test(){
let name="CJ";
return(
[1,2,3,4,5].map((item,idx)=>
<div name={name}>
{name}
</div>
)
)
}
babel编译后:
"use strict";
function test() {
var name = "CJ";
return [1, 2, 3, 4, 5].map(function (item, idx) {//循环渲染五个div
return React.createElement("div", {
name: name//节点props
}, name/*子节点的内容*/);
});
}
看看比较复杂的类组件
class Test extends React.Component{
constructor(props){//es6 接收参数
super(props);
console.log(this.props);//{name:'CJ'}
this.state={
name:'CJ'
}
}
render(){
return(//编译为createElement渲染
<div>
{this.state.name}
</div>
)
}
}
ReactDOM.render(
<Test name='CJ'/>,//渲染改类的render函数,等同于new Test({name:'CJ'}).render()
document.getElementById('example')
);
//等同于
ReactDOM.render(
new Test({name:'CJ'}).render(),//渲染时调用实例化后该函数的render方法
document.getElementById('example');
);
函数组件就比较简单了
function Test (props){
let name="CJ";
return (
<div>
{props.name}
</div>
)
}
ReactDOM.render(
<Test name="CJ"/>,//直接运行函数即可渲染
document.getElementById('example')
);
//等同于
ReactDOM.render(
Test({name:'CJ'}),
document.getElementById('example')
);
可以看出,无论是函数组件,还是类组件,返回的都是React.createElement的格式,第一个参数可以是多个类型,可以是指向虚拟DOM的变量,也可以是一个类组件
var El=<div>123</div>;
//babel:转换后
var El = React.createElement("div", null, "item");
var Ell = React.createElement(El, null, "123");
# 简易实现React
基于上面这些特性,我们就能写出一个拥有上述功能的的react。
# 文件描述
使用webpack(ESModul语法)构建,自行手动搭建好webpack运行环境。
react 文件夹
- index.js //import入口文件,提供React基础API,React.render,React.Component,React.createElement
- addEvent.js //实现事件委托功能,使用发布订阅在节点DOM还没渲染时给未渲染的DOM原生绑定事件
- Component.js //类组件的父类,实现state,setState,接收props
- createElement.js //将craeteElement创建的节点映射为type,children格式的可递归的JS对象,通过递归渲染DOM原生.
- createReactUnit.js //核心文件,渲染React的各种组件,类组件,函数组件,以及数字和字符串
- webpack入口文件
import React from './src/react';//导入自己的react文件夹入口文件
var functionTest =function(){
var name='FunctionTEST name'
return React.createElement("div", {
name: name
}, React.createElement("p", {},name));
}
//调用自己的React对象下的基础方法渲染节点
let TestNode = React.createElement("div", {
name: "CJ",
}, 'name',
React.createElement("p",null,name),
React.createElement("p", {style:'color:red'}, "TEST2",
React.createElement('span', {
name: 'spanCJ',
onclick:()=>{alert(1)}
}, '我是span标签')));
//渲染类组件
class TestClass extends React.Component{
constructor(props){
super(props);
this.state={
name:'stateCJ',
userList:[{name:'CJ',age:23},{name:'CJ2'}],
count:0,
}
}
componentWillDidmount(){
console.log('组件渲染完成')
}
componentWillMount(){
console.log('组件开始渲染')
}
handleChangeState(){
this.setState({
name:'StateChangeCJ',
count:this.state.count+1
})
}
render(){
console.log('Test组件render函数渲染',this.props);
return React.createElement("div", {
name: name
}, React.createElement("p", null,`name:${this.state.name}`),
React.createElement("p", null,`count:${this.state.count}`),
React.createElement("p", null,`Props:${this.props.name}`),
React.createElement('input',{value:'点击触发setState',type:'button',onclick:()=>{this.handleChangeState()}}),
React.createElement(TestNode)//类组件render()函数再次渲染虚拟DOM对象
);
}
}
//入口组件,渲染TestClass,babel打包前就是(<TestClass name="CJPROPS"/>)
const App=React.createElement(TestClass, {
name: "CJPROPS"
},null);
//调用我们自己写的React的render方法
React.render(App, document.querySelector('#app'));
看完上述的组件代码,懂一点React的都应该能直到运行的结果是什么了吧,展示this.state对象内的name和count以及传递的props的name属性,点击按钮触发事件执行this.setState()去触发视图更新,每次点击count+1,以及修改name的值,并且在组件执行的生命周期执行各自的方法打印对应的值。我们来实现一下和react相同语法的功能。
- index.js(react入口文件)
import createElement from './createElement';
import Component from './Component';
import createReactUnit from './createReactUnit';
let React={//组装React对象,挂载对应方法和属性
Component:Component,
createElement:createElement,
nextRootIndex:0,
//事件委托,为每一个节点赋值一个唯一的节点标记的起始值,委托给documeny.body
render:(element,el)=>{//render函数,传递组件和挂载的节点
let createReactUnitInstance=createReactUnit(element,el);
//创建react单元实例
let markUp=createReactUnitInstance.getMarkUp(React.nextRootIndex);
//获取到单元实例上渲染的元素
el.innerHTML=markUp;//挂载元素
}
}
export default React;
- createReactUnit.js 重头戏,根据传递的对象类型渲染对应的组件或者字符,执行生命周期函数,render的第一个参数可能是类组件,也可能是函数组件,也可能是数字或者字符串,根据传递的对象进行判断。
import addEvent from './addEvent';
class ReactUnit {
//React节点的祖先类,传递一个element对象,为每个继承自该组件的子类自动添加element对象
constructor(element) {
this.element = element;
}
}
class ReactTextUnit extends ReactUnit {//渲染数字或者字符串节点,没什么可讲的
//省略constructor,es6类组件默认执行父类的constructor
getMarkUp(rootId) {
this._rootId = rootId;
let markUp = `<span data-react-id=${this._rootId}>${this.element}</span>`;
return markUp;
}
}
class ReactNativeUnit extends ReactUnit {
//省略constructor,es6类组件默认执行父类的constructor
//渲染虚拟DOM对象
getMarkUp(rootId) {
this._rootId = rootId;
//获取根节点,为每个子节点添加序号,保证节点的序列号唯一性
let {
type,
props
} = this.element;
//该element是createElement返回的对象,格式
// {type:'div',props:{name:'CJ',childrend:[]}},使用原生react打印下节点试试
//获取到节点的type,可以为div,p或者类或者函数
//props为节点传递的参数,如{click:xxx}
let attrsString = '',
content = '';//初始化props和content,转换string输出
if (props) {
for (let key in props) {
if (key !== 'children') {
//遍历非子节点,设置props属性,即dom的attributes
switch (key) {
//仅渲染点击事件,有时间添加其他事件
case 'onclick':
//用事件委托预添加事件,此时DOM并没有生成,所以无法添加事件
//传递唯一的索引rootId,事件类型和需要触发的事件
//使用事件委托也大大提高的浏览器的性能
addEvent(`${this._rootId}`,key,props[key])
break;
default:
//否则就是普通的props,直接渲染attributes
attrsString += (key + '=' + props[key] + ' ');
break;
}
} else {//如果props中有children,就是子节点属性
props.children.forEach((element, idx) => {
//遍历子节点,循环拼接html属性
if (typeof element === 'string'||typeof element === 'number') {
//简单类型直接渲染
content += element;
} else {
if(typeof element.type==='object'){
//发现该节点类型为虚拟DOMM赋值给变量类型
content += new ReactNativeUnit(element.type).getMarkUp(`${this._rootId}.${idx}`);
//childrend的子项type为虚拟节点变量时,
//再次递归调用渲染虚拟节点的方法渲染element.type,返回渲染后的html元素
//并传递唯一的rootId,在原基础上加上自己的循环索引
}else{
content += new ReactNativeUnit(element).getMarkUp(`${this._rootId}.${idx}`);
//否则就时普通的节点,如数字或者字符串,递归渲染element
}
}
});
}
}
}
//开始标签和attributes
let startTag = `<${type} react-unid=${this._rootId} ${attrsString}>`;
let endTag = `</${type}>`;//结束标签
//content为递归渲染所有子节点后的文本内容
return `${startTag} ${content} ${endTag}`
}
}
class ReactClassUnit extends ReactUnit{
//省略constructor,es6类组件默认执行父类的constructor
//渲染react类组件
getMarkUp(rootId){
this._rootId=rootId;
//同理,渲染唯一的reactunid
let reactInstance=new this.element.type(this.element.props);
//实例化类实例
reactInstance.componentWillDidmount&&reactInstance.componentWillDidmount();
//执行生命周期钩子,实例化后到渲染前期间
let markup;
this.element.type.prototype.forchUpdates=()=>{
let el=reactInstance.render();
//执行执行实例化后的render()函数返回的createElement对象
let reactClassUnitInstance= createReactUnit(el);
//创建react单元函数,根据reactElemenet返回的对象调用对应的React单元
//判断是函数组件还是类组件和虚拟DOM
markup=reactClassUnitInstance.getMarkUp(rootId);
//reactClassUnitInstance.getmarkup返回渲染的innerhtml元素
reactInstance.el.innerHTML=markup;//渲染到节点上去
return markup
}
//对象原型上添加forceUpdates方法,强制视图刷新,即再次调用实例化后的render()函数
reactInstance.forchUpdates();
reactInstance.componentWillMount&&reactInstance.componentWillMount();
//渲染完执行生命周期钩子
return markup;
}
}
function createReactUnit(el,el2) { //创建React单元对象
let type = typeof el;
if (type === 'string' || type === 'number') {//进渲染变量
return new ReactTextUnit(el);
}
if(type==='object'&& typeof el.type==='function'){//类组件
el.type.prototype.el=el2;
return new ReactClassUnit(el);
}
if (type === 'object'&& typeof el.type==='string') {//渲染虚拟DOM
return new ReactNativeUnit(el);
}
}
export default createReactUnit;
- addEvent.js:简单的事件委托机制
let observe=new Observe();//实例化一个发布订阅模式
export default function addEvent(reactUnid, type, fn) {
//给发布订阅添加一个发布
observe.push(reactUnid,fn)
document.body[type] = function (event) {
//事件委托给全局的body对象,根据target的触发源去判定有没有事件执行
//其他事件类型同理
let enUnid=event.target.getAttribute('react-unid');
//得到触发源的react-unid
observe.isPublish(enUnid)&&observe.fireSingle(enUnid);
//在发布订阅中查看是否有订阅,有的话就执行该方法
}
}
class Observe{
constructor(){
this.fnCenter={};//实例化发布订阅存储器
}
push(name,fn){
this.fnCenter[name]=fn;
//添加事件和key,使用对象确保唯一性
}
isPublish(name){
return this.fnCenter[name]//返回是否有订阅者
}
fireSingle(name){//触发订阅者
for (const key in this.fnCenter) {
if (this.fnCenter.hasOwnProperty(key)) {
if(key===name){
this.fnCenter[key]&&this.fnCenter[key]();
}
}
}
}
fireAll(){//触发所有订阅者
for (const key in this.fnCenter) {
if (this.fnCenter.hasOwnProperty(key)) {
const element = this.fnCenter[key];
element&&element();
}
}
}
}
- createElement.js
class Element{
constructor(type,props){
this.type=type;
this.props=props;
}
}
export default function(tag,attrs,...children){
//就是把createElement形式的元素变换为{type,props}格式,props内放置children
//方便各种reactUnit递归遍历
let props =attrs || {};
props.children=children;
return new Element(tag,props);
}
- Component.js 提供一些基本的功能,如接收props,setState重新渲染等
export default class{
constructor(props){//传给子类接收props
this.props=props;
this.state={};//初始化子类state
}
setState(patch){
this.state={//合并state
...this.state,
...patch
}
console.log('重置状态')
this.forchUpdates();//更新最新的状态
}
}
# 实现效果预览
实现出来还是很自豪的,前端路上又艰难的前进了一小步