rwson

rwson

一个前端开发

从零开始写一个React - setState和生命周期

在前面一篇中我们用instantiateReactComponent方法来根据node的不同来返回不同的组件实例,之前的分类可能有些问题,就是当该组件中JSX部分有返回null的情况,instantiateReactComponent就不能返回正确的组件,所以在这里加了一种新的组件类型:ReactEmptyComponent,作用就是返回一段空的注释,标记这是一个空组件:

export function instantiateReactComponent(node) {
    if (lodash.isNull(node) || lodash.isUndefined(node)) {
        return new ReactEmptyComponent(null);
    }

    //  ...
}

//  空组件
export class ReactEmptyComponent {
    constructor(node) {
        this.type = "ReactEmptyComponent";
        this._currentElement = null;
        this._rootNodeID = null;
    }

    /**
     *  空组件挂载直接返回一段空注释回去
     */
    mountComponent(rootID) {
        this._rootNodeID = rootID;
        return `<!-- empty component data-reactid="${this._rootNodeID}" -->`;
    }
}

我们之前简单实现了一个初始化渲染的过程,现在我们一起实现一个setState方法以及组件后面的更新逻辑。setState是在组件中被调用的,所以我们需要在之前的Component类中加入一个setState方法:

export class Component {

	//	...

    setState(newState, callback) {
        const stacks = StackTrace.getSync();
        for(let {functionName, source} of stacks) {
            if(RENDER_REG.test(functionName) && RENDER_REG.test(source)) {
                throw new Error("callStack Error: you can't call setState in render method!");
            }
        }
        this._reactInternalInstance.receiveComponent(null, newState);

        if (lodash.isFunction(callback)) {
            callback();
        }
    }
  
    //	...
}

之前一篇我们说到一共可分成文本组件,浏览器标签组件,自定义标签组件,所以我们需要在这三个组件中各实现一个receiveComponent来接收新组件并且实现相应更新:

对于普通的文本节点,要做的相对简单,就是在receiveComponent中去更新相关DOMtextContent

export class ReactDOMTextComponent {
  	//	...
  
    /**
     *  接收到新组件
     *  @param   {String}  text  [接收到的新组件]
     */
    receiveComponent(text) {
        const nextStringText = ("" + text);
        if (nextStringText !== this._currentElement) {
            this._currentElement = nextStringText;
          	//	更新相关DOM的textContent
            $(`[data-reactid='${this._rootNodeID}']`).textContent = nextStringText;
        }
    }
}

在自定标签组件中,我们需要做的事情大概如下

  • 如果调用时传入了新的Vnode就把当前的_currentElement改成新传入的Vnode
  • 合并新老state
  • 调用组件实例下的shouldComponentUpdate根据返回的布尔值去判断是否需要更新组件
  • 调用组件实例下的componentWillUpdate
  • 调用组件的render去拿到新的Vnode,和之前的做对比,如果之前的组件Vnode不存在,就直接调用instantiateReactComponent返回新的组件实例
  • 调用组件生命周期下的componentDidUpdate方法
export class ReactCompositeComponent {
    //	...

    /**
     *  接收到新组件, 更新实例下的state, 组件生命周期方法调用
     *  @param   {ReactElement}  nextElement  [新的Vnode]
     *  @param   {Object}        newState     [this.setState(state)中的state]
     */
    receiveComponent(nextElement, newState) {
        //  如果接受了新的, 就使用最新的element
        this._currentElement = nextElement || this._currentElement;

        const { _rootNodeID } = this;

        let inst = this._instance,

            //  nextState和nextProps的处理
            nextState = Object.assign(inst.state || {}, newState),
            nextProps = lodash.clone(this._currentElement.props),
            finalProps,
            prevComponentInstance,
            prevRenderedElement,
            nextRenderedElement,
            nextMarkup,
            child;

        //  修改组件的state和props
        this._instance.state = nextState;
        this._instance.props = nextProps;
        inst.state = nextState;
        inst.props = nextProps;

        //  声明周期shouldComponentUpdate
        if (!inst.shouldComponentUpdate(nextProps, nextState)) {
            return;
        }

        //  声明周期componentWillUpdate
        inst.componentWillUpdate(nextProps, nextState);

        //  之前的组件组件实例
        prevComponentInstance = this._renderedComponent;

        //  之前的组件元素
        prevRenderedElement = prevComponentInstance._currentElement;

        //  即将被渲染的新组件元素
        nextRenderedElement = inst.render();

        //  判断是需要更新还是直接就重新渲染
        if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
            prevComponentInstance.receiveComponent(nextRenderedElement);
        } else {
            //  重新new一个对应的component
            this._renderedComponent = instantiateReactComponent(nextRenderedElement);

            //  重新生成对应的元素内容
            nextMarkup = this._renderedComponent.mountComponent(_rootNodeID);

            //  替换整个节点
            $(`[data-reactid="${_rootNodeID}"]`).innerHTML = nextMarkup;
        }
        inst.componentDidUpdate();
    }
}

在组件的render被重新调用之后,最后还是需要要更新DOM的,所以在ReactDOMComponent下的receiveComponent里我们需要对组件里的DOM下的属性和结构进行更新。

React中,有一套diff算法来比较新老组件间的差异,返回需要更新的队列,然后统一对DOM结构进行更新,在ReactDOMComponent下的receiveComponent中,我们需要完成下面的几件事情

  • 拿到老的props和新的props做,在_updateDOMProperties中对DOM下的属性进行更新
  • 调用_updateDOMChildren,传入新的组件子节点,去拼凑差异队列,然后更新DOM
  • 修改currentElement变成本次渲染的,供下次使用
export class ReactDOMComponent {
    /**
     *  接收到新组件
     *  @param   {Object}  nextElement  [新组件]
     */
    receiveComponent(nextElement) {
        const lastProps = lodash.clone(this._currentElement.props),
            nextProps = nextElement.props;

        //  需要单独的更新属性
        this._updateDOMProperties(lastProps, nextProps);

        //  再更新子节点
        this._updateDOMChildren(nextElement.props.children);

        //  修改currentElement变成本次渲染的
        this._currentElement = nextElement;
    }

    /**
     *  更新组件中相关DOM的属性
     *  @param    {Object}  lastProps  [旧属性]
     *  @param    {Object}  nextProps  [新属性]
     *  @private
     */
    _updateDOMProperties(lastProps, nextProps) {
        const { _rootNodeID } = this,
        element = $(`[data-reactid="${_rootNodeID}"]`);
        let propKey, propValue, eventType, removed;

        for (propKey in lastProps) {
            //  只删除老属性中有但是新属性中没有的
            if (hasOwnProperty(lastProps, propKey) && !hasOwnProperty(nextProps, propKey)) {
                propValue = lastProps[propKey];

                //  之前的事件代理需要解除
                if (EVENT_REG.test(propKey)) {
                    eventType = propKey.replace("on", "");
                    Event.undelegate({
                        element: doc,
                        type: eventType,
                        selector: `[data-reactid="${_rootNodeID}"]`
                    });
                    continue;
                } else if (propKey === "className") {
                    removed = "class";
                } else {
                    removed = propKey;
                }

                //  删除DOM上的相关属性
                element.removeAttribute(removed);
            }
        }

        //  开始遍历新属性集合
        for (propKey in nextProps) {
            if (hasOwnProperty(nextProps, propKey) && propKey !== "children") {
                propValue = lastProps[propKey];
              	//	重新代理事件
                if (EVENT_REG.test(propKey)) {
                    eventType = propKey.replace("on", "");
                    Event.undelegate({
                        element: doc,
                        type: eventType,
                        selector: `[data-reactid="${_rootNodeID}"]`
                    });
                    Event.delegate({
                        element: doc,
                        type: eventType,
                        selector: `[data-reactid="${_rootNodeID}"]`,
                        handler: propValue,
                        context: null
                    });
                    continue;
                } else if (propKey === "className") {
                    element.setAttribute("class", propValue);
                } else if (propKey === "style") {
                    if (lodash.isObject(propValue)) {
                        propValue = toStyle.string(propValue);
                    }
                    element.setAttribute(propKey, propValue);
                } else {
                    element.setAttribute(propKey, propValue);
                }
            }
        }
    }

    /**
     *  更新子元素
     *  @param    {Array}  nextChildrenElements  [被更新的组件队列]
     */
    _updateDOMChildren(nextChildrenElements) {
        if (nextChildrenElements && nextChildrenElements.length) {

            update.updateDepth++;
            //  递归找出差别, 组装差异对象
            update.diff(update.diffQueue, nextChildrenElements, this);
            update.updateDepth--;

            //  应用更新
            if (update.updateDepth === 0) {
                update.patch(update.diffQueue);
            }
        }
    }
}

_updateDOMChildren中我们调用了update.diffupdate.patch方法,一个对比一个应用,这里我是把diffpatch放到一个对象下作为一个模块暴露出去的,下面就是具体的实现代码:

//	定义更新类型(移动已经存在的,删除节点,插入的新标签)
const UPDATE_TYPES = {
    MOVE_EXISTING: 1,
    REMOVE_NODE: 2,
    INSERT_MARKUP: 3
};

export const update = {
    //  更新深度标识
    updateDepth: 0,

    //  更新队列
    diffQueue: [],

    /**
     *  递归找出差别, 组装差异对象, 添加到更新队列diffQueue
     *  @param   {Array}  diffQueue             [更新队列]
     *  @param   {Array}  nextChildrenElements  [新的子组件集合]
     *  @param   {Object} component             [被diff的组件]
     *  @return  {Array}                        [需要更新的内容]
     */
    diff(diffQueue, nextChildrenElements, component) {
        //  获取到当前组件下已经渲染的组件集合,把component._renderedChildren扁平成一个对象,如果child有key,就拿key作为对应的属性名,否则用下标做属性名,具体实现可以看下面
        const prevChildren = flattenChildren(component._renderedChildren),

            //  生成新的子节点的component对象集合(如果是组件有更新, 就复用原来的, 如果是新增就是新的组件实例)
            nextChildren = generateComponentChildren(prevChildren, nextChildrenElements);

        let lastIndex = 0,
            nextIndex = 0,
            prevChild = null,
            nextChild = null,
            name, props, propKey, eventType;

        //  枚举nextChildren
        for (name in nextChildren) {
            if (!hasOwnProperty(nextChildren, name)) {
                continue;
            }

            prevChild = prevChildren && prevChildren[name];
            nextChild = nextChildren[name];

            //  两个相同说明是使用的同一个component,所以我们需要做移动的操作
            if (lodash.isEqual(prevChild, nextChild)) {
                if (prevChild._mountIndex < lastIndex) {
                    this.diffQueue.push({
                        parentId: component._rootNodeID,
                        parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
                        type: UPDATE_TYPES.MOVE_EXISTING,
                      	//	从组件原来的mountIndex
                        fromIndex: prevChild._mountIndex,
                      	//	到nextIndex
                        toIndex: nextIndex
                    });
                }
              
              	//	缓存上次遍历时最大的index
                lastIndex = Math.max(prevChild._mountIndex, lastIndex);
            } else {
                //  之前存在子节点, 需要先将子节点移除
                if (prevChild) {
                    this.diffQueue.push({
                        parentId: component._rootNodeID,
                        parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
                        type: UPDATE_TYPES.REMOVE_NODE,
                        fromIndex: prevChild._mountIndex,
                        toIndex: null
                    });
                    lastIndex = Math.max(prevChild._mountIndex, lastIndex);

                    props = (prevChild._currentElement && prevChild._currentElement.props) ? prevChild._currentElement.props : {};

                  	//	对移除的子节点需要进行事件代理的接触,防止重复
                    for (propKey in props) {
                        if (hasOwnProperty(props, propKey) && EVENT_REG.test(propKey)) {
                            eventType = propKey.replace("on", "");
                            Event.undelegate({
                                element: doc,
                                type: eventType,
                                selector: `[data-reactid="${prevChild._rootNodeID}"]`
                            });
                        }
                    }
                }

                //  新增的节点, 需要push到diffQueue
                if (nextChild) {
                    this.diffQueue.push({
                        parentId: component._rootNodeID,
                        parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
                        type: UPDATE_TYPES.INSERT_MARKUP,
                        fromIndex: null,
                        toIndex: nextIndex,
                        markup: nextChild.mountComponent(`${component._rootNodeID}.${name}`)
                    });
                }
            }

            //  更新_mountIndex和nextIndex
            nextChild._mountIndex = nextIndex;
            nextIndex++;

            //  把nextChildren克隆一份给_renderedChildren
            component._renderedChildren = makeArray(nextChildren);
        }
    },

    /**
     *  应用更新, 执行DOM操作
     *  @param   {Array}  updates  [差异对象集合]
     */
    patch(updates) {
        let initialChildren = {},
            deleteChildren = [],
            updatedIndex, updatedChild, parentID;
        for (let update of updates) {
            updatedIndex = update.fromIndex;
            updatedChild = update.parentNode.children[updatedIndex];
            parentID = update.parentID;

            //  把所有需要更新的节点都保存下来
            initialChildren[parentID] = initialChildren[parentID] || [];

            //  使用parentID作为简易命名空间
            initialChildren[parentID][updatedIndex] = updatedChild;

            //  所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
            if (!lodash.isNull(updatedChild) && !lodash.isUndefined(updatedChild)) {
                deleteChildren.push(updatedChild);
            }
        }

        //  删除需要删除的节点
        for (let child of deleteChildren) {
            child.parentNode.removeChild(child);
        }

        for (let updateItem of updates) {
            switch (updateItem.type) {
                //  插入新元素
                case UPDATE_TYPES.INSERT_MARKUP:
                    insertChildAt(updateItem.parentNode, updateItem.markup, updateItem.toIndex);
                    break;

                    //  元素位置发生改变    
                case UPDATE_TYPES.MOVE_EXISTING:
                    insertChildAt(updateItem.parentNode, initialChildren[updateItem.parentID][updateItem.fromIndex], updateItem.toIndex);
                    break;

                    //  上面已经删除, 所以不需要处理
                case UPDATE_TYPES.REMOVE_NODE:
                    break;

                default:
                    break;
            }
        }

        //  重置相关变量
        this.reset();
    },

    /**
     *  重置相关变量
     */
    reset() {
        this.updateDepth = 0;
        this.diffQueue = [];
    }
};

diff中,我们看到了两个新方法,分别是flattenChildrengenerateComponentChildren,我们先看下flattenChildren的实现:

/**
 *  把原来是数组的子组件集合转换成Map返回
 *  @param   {Array}  componentChildren     [子组件集合]
 *  @return  {Object}                       [输出的Map, 每个子组件的key或者一个随机数做key]
 */
function flattenChildren(componentChildren) {
    let childrenMap = {},
        child, name, i, len;

    for (i = 0, len = componentChildren.length; i < len; i++) {
        child = componentChildren[i];
        name = child && child._currentelement && child._currentelement.key ? child._currentelement.key : i.toString(36);
        childrenMap[name] = child;
    }
    return childrenMap;
}

generateComponentChildren我们大概需要完成下面几件事情:

  • 遍历拿到即将渲染的新组件children(做参数 nextChildrenElements传入)和老节点进行对比
  • 如果老节点存在且和新节点有差异,即调用老节点下的receiveComponent去更新
  • 否则如果老节点不存在,则重新调用instantiateReactComponent返回一个组件实例
/**
 *  生成子节点elements的component集合
 *  @param   {Object}  prevChildren          [flattenChildren返回的Map]
 *  @param   {Array}   nextChildrenElements  [即将要渲染的节点]
 *  @return  {Object}                        [子节点elements的component集合]
 */
function generateComponentChildren(prevChildren, nextChildrenElements) {
    let nextChildren = {},
        index, len, name, prevChild, prevElement, nextElement, nextChildInstance, element;
    nextChildrenElements = nextChildrenElements || [];

    for (index = 0, len = nextChildrenElements.length; index < len; index++) {
        element = nextChildrenElements[index];
        name = (element && element.key) ? element.key : index;
        prevChild = prevChildren && prevChildren[name];
        prevElement = prevChild && prevChild._currentElement;
        nextElement = element;

        //  组件有更新, 调用当前组件下的reciveComponent去更新组件
        if (shouldUpdateReactComponent(prevElement, nextElement)) {
            prevChild.receiveComponent(nextElement);
            nextChildren[name] = prevChild;
        } else {
            //  新节点, 实例化新组件
            nextChildInstance = instantiateReactComponent(nextElement);
            nextChildren[name] = nextChildInstance;
        }
    }

    return nextChildren;
}

在之前好几个地方我们都看到了shouldUpdateReactComponent这个方法,它完成的功能主要是判断两个Vnode之前是否有差异,返回布尔值,主要完成下面几件事情:

  • 如果新老Vnode有一个或者都为空,直接返回false
  • 文本节点之间的对比
  • 当新的Vnodetypeobject,比较老节点的typekey,并且拿到两者的children数组,做一个简单的长度对比
/**
 *  判断组件是否需要更新
 *  @param   {Object}  prevElement  [老的vnode]
 *  @param   {Object}  nextElement  [新的vnode]
 *  @return  {Boolean}              [标识组件是否需要更新]
 */
export function shouldUpdateReactComponent(prevElement, nextElement) {
    //  排除为空的情况
    if (!lodash.isNull(prevElement) && !lodash.isNull(nextElement) && !lodash.isUndefined(prevElement) && !lodash.isUndefined(nextElement)) {
        const prevType = typeof prevElement,
            nextType = typeof nextElement;

        //  纯文本组件
        if (prevType === "number" || prevType === "string") {
            return (nextType === "number" || nextType === "number");
        } else {
            let prevChildren = [],
                nextChildren = [],
                childEqual = true;

            if(prevElement && prevElement.props) {
                prevChildren = prevElement.props.children || [];
            }

            if (nextElement && nextElement.props) {
                nextChildren = nextElement.props.children || [];
            }

            childEqual = prevChildren.length === nextChildren.length;

            return (nextType === "object" &&
                (prevElement.type === nextElement.type) &&
                (prevElement.key === nextElement.key) &&
                childEqual);
        }
    }

    return false;
}

到这里我们就实现一个setState和生命周期,在例子中实现了一个Todo,一起看下效果

todo

至此我们就实现了一个简单的React,但是仅仅实现了虚拟节点差异算法props单向数据流,还有很多更优秀的没实现,比如批量更新,事件优化,组件中的refs,服务端渲染等等,只是一个玩具,对于想深入了解React原理的可能会有些帮助。