1、通常我们写react代码的时候,都是以React.createClass()或者React.Component的形式创建我们的组件,但是经过webpack的babel编译之后,它默认被转换成为React.createElement()这个方法,然后经过React.render的进一步处理;例如
React.createElement('ul',{id:'new',style: {fontSize:"20px"}},
React.createElement('li',{key:'A'},'A1'),
React.createElement('li',{key:'C'},'C1'),
React.createElement('li',{key:'B'},'B1'),
React.createElement('li',{key:'E'},'E'),
React.createElement('li',{key:'F'},'F')
);
2、接着我们定义一个Element的类,它的方法主要有createElement,接受的参数主要有三个:type,props,...children;
type是标签的类型,props是属性,children是子节点。然后通过createElement生成虚拟dom,再返回一个Element的对象的格式:{type:"";props:{}};接着我们导出Element, createElement;例如
//虚拟dom的格式
class Element {
constructor(type,props) {
this.type = type;
this.props = props;
}
}
//type标签名称 props属性 children子节点
function createElement(type,props,...children) {
//如果木有属性,默认是空对象
props = props || {};
//将子节点作为props的属性
props.children = children;
//返回对象格式{type:value;props:{}}
return new Element(type, props);
}
export {
Element,
createElement
};
3、当我们创建好虚拟dom的时候,它会将结果返回给我们的render函数去执行下一步动作。而render函数接收参数是:el,root;el是我们创建好的虚拟dom,主要负责调用以怎么样的方式来创建组件、原生react元素、文本的方法。调用createUnit创建之后,会通过getMarkUp这个方法获取真正的dom,然后将其渲染到页面;例如
import $ from "jquery";
import {createUnit} from "./Unit";
import {createElement} from "./Element";
import {Component} from "./Component";
let React = {
rootIndex:0,
render,
createElement,
Component
};
function render(el, root) {
let unit = new createUnit(el);
let markUp = unit.getMarkUp(React.rootIndex);
$(root).html(markUp);
//出发页面注册完成事件
$(document).trigger("mounted");//componentDidMount
}
export default React;
4、接下来我们要创建一个Unit.js,这个js主要负责处理各种类型的组件、react元素、文本节点、diff算法等。
第一:先创建Unit类,这个是所有文本、组件、节点类的基类,它会保存当前实例和提供getMarkUp给子类重写
import {Element} from "./Element"
import $ from "jquery"
import React from "./react";
import types from "./types";
let diffQueue = [];//差异队列
let updateDepth = 0;//更新级别
//这个是所有文本、组件、节点类的基类
class Unit {
constructor(element) {
//保存当前的dom
this._currentElement = element;
}
//这个方法给子类重写
getMarkUp() {
throw new Error("不能调用此方法!");
}
}
第二:创建文本类,getMarkUp负责将转入的文本节点进行拼接返回字符串;update方法则是在页面中setState的时候获取到当前文本的实例,从而调用了update方法,将文本替换更新。
//文本类
class TextUnit extends Unit {
getMarkUp(reactId) {
this._reactid = reactId;
return `<span data-reactid="${reactId}">${this._currentElement}</span>`
}
update(nextElement) {
//文本节点,对比值是不是相同的
if (this._currentElement !== nextElement) {
//节点不同,直接更新
this._currentElement = nextElement;
//替换页面中的节点
$(`[data-reactid="${this._reactid}"]`).html(this._currentElement);
}
}
}
第三:创建原生节点类NativeUnit,它主要负责将react原元素转为dom节点。getMarkUp中会遍历props属性,如果遇到属性是事件,那么就直接绑定事件;如果是遇到class名,则将class写入;如果是遇到style,则将值取出遍历拼接成字符串;如果遇到事子节点,则会遍历每个子节点,然后调用createUnit为每个子节点创建相应元素;如果是自定义属性,那就直接赋值。
update这个方法就会复杂一些,当你的页面更新时候,也就是setState的时候就会触发。这时候需要获取上一次虚拟dom和新的虚拟dom进行对比。首先对比属性,然后更新属性。然后再对比子节点,同diff的比较,会得到一个diffQueue数组,这个是保存了当前新旧虚拟dom需要删除、移动、还是新增的一个标记。然后再通过patch进行遍历我们的diffQueue,如果是删除那就直接删除dom,如果是新增,那就找到对应位置新增;如果是移动,也是找到对应的位置移动。
//原生节点类
class NativeUnit extends Unit {
getMarkUp(reactid) {
this._reactid = reactid;
let {type, props} = this._currentElement;
//拼接字符串
let tagStart = `<${type} data-reactid="${this._reactid}"`;
let childString = '';
let tagEnd = `</${type}>`;
this._renderedChildrenUnits = [];
//遍历props
for (let propName in props) {
//如果是事件
if (/^on[A-Z]/.test(propName)) {
//绑定事件
let eventName = propName.slice(2).toLowerCase();
$(document).delegate(`[data-reactid="${this._reactid}"]`, `${eventName}.${this._reactid}`, props[propName]);
} else if (propName === "style") {
//如果是样式 遍历拼接赋值
let styleObj = props[propName];
let styles = Object.entries(styleObj).map(([attr, value]) => {
return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`;
}).join(";");
tagStart += (` style="${styles}" `);
} else if (propName === "className") {
//如果是类名,需要进行转换class
tagStart += (` class="${props[propName]}" `);
} else if (propName === "children") {
//如果是子节点 需要创建元素 重新生成
let children = props[propName];
//遍历子节点树
children.forEach((child, index) => {
//获取每个节点树的unit
let childUnit = createUnit(child);
//给每个childUnit,数组中的每个节点添加索引标记
childUnit._mountIndex = index;
//把生成过的childUnit给缓存起来
this._renderedChildrenUnits.push(childUnit);
//获取每个unit生成的dom节点
let childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`);
//拼接字符串
childString += childMarkUp;
});
} else {
//如果是自定义属性
tagStart += (` ${propName}=${props[propName]} `);
}
}
return tagStart + ">" + childString + tagEnd;
}
//添加本类的update
//现在的更新只是虚拟dom的更新了
update(nextElement) {
//获取渲染过的虚拟dom
let oldProps = this._currentElement.props;
//获取新的虚拟dom的props
let newProps = nextElement.props;
//如果第一层标签相同 检测属性 更新属性
this.updateDomProperties(oldProps, newProps);
this.updateDomChildren(nextElement.props.children);
}
//更新属性 将老的属性去掉,换成新的属性
updateDomProperties(oldProps, newProps) {
let propName;
//遍历loader的属性集合 去掉属性 然后直接给操作dom的属性 或者事件
for (propName in oldProps) {
//在新的属性集合看有没有对应的key,没有则删除就的属性
if (!newProps.hasOwnProperty(propName)) {
$(`[data-reactid="${this._reactid}"]`).removeAttr(propName);
}
//把旧的事件也删除
if (/^on[A-Z]/.test(propName)) {
$(document).undelegate(`.${this._reactid}`);
}
}
//遍历新的属性集合 直接给dom添加属性或者事件
for (propName in newProps) {
//如果有子节点
if (propName === "children") {
continue;
} else if (/^on[A-Z]/.test(propName)) {
//如果是事件 绑定事件
let eventName = propName.slice(2).toLowerCase();//click
$(document).delegate(`[data-reactid="${this._reactid}"]`, `${eventName}.${this._reactid}`, newProps[propName]);
} else if (propName === "className") {
//如果是className 赋值给class
$(`[data-reactid="${this._reactid}"]`).attr('class', newProps[propName]);
} else if (propName === "style") {
//如果是style 遍历 拼接
let styleObj = newProps[propName];
Object.entries(styleObj).map(([attr, value]) => {
$(`[data-reactid="${this._reactid}"]`).css(attr, value);
})
} else {
//其他自定义属性
$(`[data-reactid="${this._reactid}"]`).prop(propName, newProps[propName]);
}
}
}
//然后就更新子节点 也是一样,先跟新属性 再更新其他的
//更新子节点 也是要对比新的子节点和旧的子节点的差异
updateDomChildren(newChildrenElements) {
updateDepth++;
//先要做比较
this.diff(diffQueue, newChildrenElements);
updateDepth--;
if (updateDepth === 0) {
//遍历所有的子节点树后开始 打补丁
this.patch(diffQueue);
diffQueue = [];
}
}
//比较新的子树和旧的子树
diff(diffQueue, newChildrenElements) {
//首先将旧的子树生成一个unitMap
let oldChildrenUnitMap = this.getOldChildrenMap(this._renderedChildrenUnits);
//第二部生成一个新的儿子的unit数组
let {newChildrenUnitMap, newChildrenUnits} = this.getNewChildren(oldChildrenUnitMap, newChildrenElements);
//保存上一个已经确定的索引
let lastIndex = 0;
//遍历新的unit数组
for (let i = 0; i < newChildrenUnits.length; i++) {
//获取一个unit
let newUnit = newChildrenUnits[i];
//拿出每个newKey
let newKey = (newUnit._currentElement.props && newUnit._currentElement.props.key) || i.toString();
//获取对应老的childUnit
let oldChildUnit = oldChildrenUnitMap[newKey];
//如果新老节点一致,说明复用了老节点
if (newUnit === oldChildUnit) {
//lastIndex是我遍历新数组节点=》相同节点的索引,如果lastIndex是新节点的索引,_mountIndex是旧节点的索引
//如果新节点的索引大于旧节点的索引,例如旧的是1 新的是2 则当前节点要往后移动
if (oldChildUnit._mountIndex < lastIndex) {
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="${this._reactid}"]`),
type: types.MOVE,
fromId: oldChildUnit._mountIndex,
fromIndex: oldChildUnit._mountIndex,
toIndex: i
});
}
lastIndex = Math.max(lastIndex, oldChildUnit._mountIndex);
} else {
//如果新老节点不一致,但是老节点存在,需要删除
if (oldChildUnit) {
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="${this._reactid}"]`),
type: types.REMOVE,
fromIndex:oldChildUnit._mountIndex
});
}
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="${this._reactid}"]`),
type: types.INSERT,
toIndex: i,
markUp: newUnit.getMarkUp(`${this._reactid}.${i}`)
});
}
newUnit._mountIndex = i;
}
//遍历旧的树节点 标记为删除
for (let oldKey in oldChildrenUnitMap) {
let oldChild = oldChildrenUnitMap[oldKey];
//如果新的节点中找不到用旧的key找不到,说明该节点被删除了
if (!newChildrenUnitMap.hasOwnProperty(oldKey)) {
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="${this._reactid}"]`),
type: types.REMOVE,
fromIndex: oldChild._mountIndex
});
//如果删除了某一个节点,则把它对应的unit也删除
this._renderedChildrenUnits = this._renderedChildrenUnits.filter(item => item != oldChild);
//还要把这个节点地应的事件委托也删除掉
$(document).undelegate(`.${oldChild._reactid}`);
}
}
}
getOldChildrenMap(childrenUnits = []) {
let Map = {};
for (let i = 0; i < childrenUnits.length; i++) {
//获取每个子节点的unit
let unit = childrenUnits[i];
//获取每个unit的key 没有key则只用索引
let key = (unit._currentElement.props && unit._currentElement.props.key) || i.toString();
Map[key] = unit
}
return Map;
}
getNewChildren(oldChildrenUnitMap, newChildrenElements) {
let newChildrenUnits = [];
let newChildrenUnitMap = {};
//遍历新的子节点树
newChildrenElements.forEach((newElement, index) => {
//获取每个子节点树的key
let newKey = (newElement.props && newElement.props.key) || index.toString();
//将新的key在旧的unitMap中找看看有没有对应的树 找到老的unit
let oldUnit = oldChildrenUnitMap[newKey];
//获取老的元素 比如获取到key为ACB的unit
let oldElement = oldUnit && oldUnit._currentElement;
//看看是不是需要深比较
if (shouldDeepCompare(oldElement, newElement)) {
//然后用旧点unit更新 如果是在旧的树中找到key 则在旧的基础上修改
oldUnit.update(newElement);
newChildrenUnits.push(oldUnit);
newChildrenUnitMap[newKey] = oldUnit;
} else {
//如果是新的key 例如 EF
//则创建新的unit
let nextUnit = createUnit(newElement);
newChildrenUnits.push(nextUnit);
newChildrenUnitMap[newKey] = nextUnit;
//缓存新渲染过的子节点树
this._renderedChildrenUnits[index] = nextUnit;
}
});
return {newChildrenUnits, newChildrenUnitMap}
}
patch(diffQueue) {
//这里存放所有将要删除的节点
let deleteChildren = [];
//暂存复用的节点
let deleteMap = {};
//遍历需要更新的节点标记
for (let i = 0; i < diffQueue.length; i++) {
//找到每个一节点
let difference = diffQueue[i];
//判断当前节点的type是remove还是move
if (difference.type === types.MOVE || difference.type === types.REMOVE) {
//说明这个节点需要移动
let fromIndex = difference.fromIndex;
//获取到真实的dom
let oldChild = $(difference.parentNode.children().get(fromIndex));
//如果还没有存在复用的节点,则添加
if (!deleteMap[difference.parentId]) {
deleteMap[difference.parentId] = {};
}
//暂存复用的节点
deleteMap[difference.parentId][fromIndex] = oldChild;
deleteChildren.push(oldChild);
}
}
$.each(deleteChildren, (idx, item) => $(item).remove());
//判断该插入的节点
for (let i = 0; i < diffQueue.length; i++) {
let difference = diffQueue[i];
switch (difference.type) {
case types.INSERT:
this.insertChildAt(difference.parentNode, difference.toIndex, $(difference.markUp));
break;
case types.MOVE:
this.insertChildAt(difference.parentNode, difference.toIndex, deleteMap[difference.parentId][difference.fromIndex]);
break;
default:
break
}
}
}
insertChildAt(parentNode, toIndex, newNode) {
let oldChild = parentNode.children().get(toIndex);
oldChild ? newNode.insertBefore(oldChild) : newNode.appendTo(parentNode)
}
}
第四:创建自定义组件的类CompositeUnit,这个类主要负责处理以component的方式创建组件。getMarkUp主要将传进来的组件进行实例化,然后缓存到一个属性下面。然后给组件实例传入props属性,得到实例后执行componentWillMount,执行render得到虚拟dom,然后调用createUnit创建元素。
update方法呢,主要将转入的state进行合拼,然后判断是不是需要更新shouldComponentUpdate,然后通过上一次生成的虚拟dom与这次的虚拟dom进行对比,是不是需要深度比较,深度比较后则调用之前的实例更新,否则直接创建新的元素。
//处理自定义组件
class CompositeUnit extends Unit {
//负责自定义组件的更新操作 在getMarkUp的时候已经获取到组件的实例,保存起来,所有更新操作都在这里做
//能够调用update是因为使用的setState,默认会调用当前类的update
update(nextElement, partialState) {
//如果传进来新的元素,则取新的元素,否则使用旧的
this._currentElement = nextElement || this._currentElement;
//获取新的state和状态,更新状态 不管组件有没有更新,状态一定要更新
//直接更新组件的state属性
let nextState = Object.assign(this._componentInstance.state, partialState);
let nextProps = this._currentElement.props;
//如果有shouldComponentUpdate执行,判断是否返回值是true还是false,是不是需要更新
if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
return;
}
//获取虚拟dom,看看是不是需要深比较
//获取上一次实例的组件对象
let preRenderedUnitInstance = this._renderedUnitInstance;
//从unit中获取获取渲染过的虚拟dom
let preRenderedElement = preRenderedUnitInstance._currentElement;
//获取新的虚拟dom
//注意 :重新调用render的时候会返回下面:原生节点类=》调用update的时候会触发原生节点类的update
/* let p = React.createElement('p',{},this.state.number);
let button = React.createElement('button',{onClick:this.handleClick},'+');
return React.createElement('div',{id:"counter",style:{color:this.state.number%2 === 0 ? "red" : "green"}},p,button);*/
// return this.state.number;
//此时重新render就会触发createElement,然后通过createUnit创建,会得到原生节点类的实例;
// nextRenderElement就是NativeUnit类的实例
let nextRenderElement = this._componentInstance.render();
//进行dom diff比较
//判断是否需要深度比较
if (shouldDeepCompare(preRenderedElement, nextRenderElement)) {
//如果需要更新,则调用子节点update方法进行更新,转入新的el节点,是文本=》调用textUnit 等
//preRenderUnitInstance => NativeUnit类的实例,可以调用update方法
//此时的update是原生节点类的update
preRenderedUnitInstance.update(nextRenderElement);
//调用更新完成钩子
this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate();
} else {
//不需要深比较,直接替换元素为新的
this._renderedUnitInstance = createUnit(nextRenderElement);
let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._reactid);
//替换整个节点
$(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp);
}
}
getMarkUp(reactId) {
this._reactid = reactId;
//解构拿出参数 Component是自定义组件
let {type: Component, props} = this._currentElement;
//componentInstance是自定义组件的实例
let componentInstance = this._componentInstance = new Component(props);
//创建component实例_currentUnit中 this 是CompositeUint
componentInstance._currentUnit = this;
//将当前实例存到自定义属性
//组件将要渲染 存在componentWillMount运行
componentInstance.componentWillMount && componentInstance.componentWillMount();
//执行render,获取虚拟dom的实例
let renderElement = componentInstance.render();
//此时获取的结果有可能是 string number 组件 原生节点 需调用 createUnit
//this._renderedUnitInstance 这个保存起来,更新的时候用到
let renderUnitInstance = this._renderedUnitInstance = createUnit(renderElement);
//获取html标记
let renderMarkUp = renderUnitInstance.getMarkUp(this._reactid);
//页面注册挂载完成的监听
$(document).on("mounted", () => {
componentInstance.componentDidMount && componentInstance.componentDidMount()
});
return renderMarkUp;
}
}
第五:定义深比较的函数
//是否需要深度比较 类型一样才可以进行深比较
function shouldDeepCompare(oldElement, newElement) {
//判断元素是否为null
if (oldElement != null && newElement != null) {
//先比较类型
let oldType = typeof oldElement;
let newType = typeof newElement;
//如果新老节点是文本可以进行比较
if ((oldType === "string" || oldType === "number") && (newType === "string" || newType === "number")) {
return true;
}
//如果是react元素,判断type是不是相同的
if (oldElement instanceof Element && newElement instanceof Element) {
return oldElement.type == newElement.type;
}
}
//默认不需要深比较
return false;
}
第六:创建调用各种类的入口方法
//调用对应处理文本、组件、节点的类
function createUnit(element) {
//处理文本的类型
if (typeof element === 'string' || typeof element === 'number') {
return new TextUnit(element);
}
//处理react原生节点
if (element instanceof Element && typeof element.type === 'string') {
return new NativeUnit(element);
}
//处理自定义组件
if (element instanceof Element && typeof element.type === 'function') {
return new CompositeUnit(element);
}
}
第七:导出createUnit
5、接下来我们创建一个Component.js用来处理我们组件的事情。
class Component {
constructor(type,props) {
this.props = props;
}
setState(partialState) {
this._currentUnit.update(null,partialState)
}
}
export {
Component
};
6、最后创建一个types.js主要是标记了删除、新增、移动的枚举
export default {
MOVE:"MOVE",//移动
INSERT:"INSERT",//插入
REMOVE:"REMOVE" //删除
};
这个是创库的地址:https://github.com/weikehang/react-source.git