react源码实现解析

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

发布了80 篇原创文章 · 获赞 5 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_28473733/article/details/97101674