5

本节我们看如何更新组件。在上一节也反复提到renderComponent这个方法了,这节直接从它入手吧。它位于src/vdom/component.js文件中。

从参数来看,我们会惊讶它竟然会有这么多参数,原来我们只看到它有两个参数,第二个为数字。第一个参数不用说,是组件的实例。

export function renderComponent(component, opts, mountAll, isChild) {
    //如果它是_disable状态,立即返回,
    if (component._disable) return;
    //开始取出它前后的props, state,context, base
    //base是这个组件的render方法生成的虚拟DOM最后转化出来的真实DOM
    //如果有这个真实DOM,说明它已经mount了,现在是处于更新状态
    let props = component.props,
        state = component.state,
        context = component.context,
        previousProps = component.prevProps || props,
        previousState = component.prevState || state,
        previousContext = component.prevContext || context,
        isUpdate = component.base,
        nextBase = component.nextBase,
        //真实DOM
        initialBase = isUpdate || nextBase,
        //这个变早比较难理,它是component的render方法生成的虚拟DOM的type函数再实例化出来的子组件,相当于一个组件又return出另一个组件。通常情况下,组件会return出来的虚拟DOM的type为一个字符串,对应div, p, span这些真实存在的nodeName。而type为函数时,它就是一个组件。
        initialChildComponent = component._component,
        skip = false,
        rendered, inst, cbase;

    // 如果是更新状态,会经过shouldComponentUpdate,componentWillUpdate钩子
    if (isUpdate) {
        component.props = previousProps;
        component.state = previousState;
        component.context = previousContext;
        if (opts!==FORCE_RENDER
            && component.shouldComponentUpdate
            && component.shouldComponentUpdate(props, state, context) === false) {
            skip = true;
        }
        else if (component.componentWillUpdate) {
            component.componentWillUpdate(props, state, context);
        }
        component.props = props;
        component.state = state;
        component.context = context;
    }
    //GC
    component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
    component._dirty = false;
    
    if (!skip) {
        //mount与update都要调用render方法,这时与官方react有点不一样,官方react是没有传参,可能是早期官方文档没有规范render的参数吧。而后来的官方源码上,render是没有参数的。这个参数不应该preact来背。
        rendered = component.render(props, state, context);

        // 如果用户定义了getChildContext,那么用它与context生成孩子的context
        if (component.getChildContext) {
            context = extend(extend({}, context), component.getChildContext());
        }

        let childComponent = rendered && rendered.nodeName,
            toUnmount, base;
        //判定render出来的虚拟DOM是否还是一个组件
        if (typeof childComponent==='function') {
            // set up high order component link

            let childProps = getNodeProps(rendered);
            inst = initialChildComponent;
            //如果前后两次的子组件的类型都一致,并且key也一样,则用setComponentProps方法更新这个子组件
            if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
                setComponentProps(inst, childProps, SYNC_RENDER, context, false);
            }
            else {
            //否则要替换原来的组件
            //toUnmount用来标识一会儿要进行unmount操作
                toUnmount = inst;
            //实例化另一个组件
                component._component = inst = createComponent(childComponent, childProps, context);
                //刷新真实DOM
                inst.nextBase = inst.nextBase || nextBase;
                inst._parentComponent = component;
                //更新子组件的属性,这里面调用WillRecieveProps钩子
                setComponentProps(inst, childProps, NO_RENDER, context, false);
                //异步渲染子组件,这招比较妙,这里你可以看到isChild参数的作用
                renderComponent(inst, SYNC_RENDER, mountAll, true);
            }
           
            base = inst.base;
        }
        else {
          //如果这次render出来的不是组件,而是普通虚拟DOM,
            cbase = initialBase;

            // destroy high order component link
            toUnmount = initialChildComponent;
            if (toUnmount) {
                cbase = component._component = null;
            }

            if (initialBase || opts===SYNC_RENDER) {
                if (cbase) cbase._component = null;
                base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
            }
        }
        //如果元素节点不同,并且组件实例也不是一个
        if (initialBase && base!==initialBase && inst!==initialChildComponent) {
            let baseParent = initialBase.parentNode;
            if (baseParent && base!==baseParent) {
                baseParent.replaceChild(base, initialBase);

                if (!toUnmount) {
                    initialBase._component = null;
                    recollectNodeTree(initialBase, false);
                }
            }
        }

        if (toUnmount) {
            unmountComponent(toUnmount);
        }
        //重写真实DOM
        component.base = base;
        if (base && !isChild) {
            let componentRef = component,
                t = component;
         //由于组件能返回组件,可能经过N次render后才能返回一个能转换成为真实DOM的普通虚拟DOM,这些组件通过_parentComponent链接在一起,它们都是共享同一个真实DOM(base), 这时我们需要为这些组件都重写base属性
            while ((t=t._parentComponent)) {
                (componentRef = t).base = base;
            }
            //在真实DOM上保存最初的那个组件与组件的构造器
            //在真实DOM上保存这么多对象其实是不太好的实现,因为会导致内存泄露,因此才有了recollectNodeTree这个方法
            base._component = componentRef;
            base._componentConstructor = componentRef.constructor;
        }
    }
    //如果是异步插入进行组件的单个render或者是ReactDOM.render,这些组件实例都会先放到mounts数组中。
    if (!isUpdate || mountAll) {
        mounts.unshift(component);
    }
    else if (!skip) {
         //更新完毕,调用componentDidUpdate,afterUpdate钩子
        if (component.componentDidUpdate) {
            component.componentDidUpdate(previousProps, previousState, previousContext);
        }
        if (options.afterUpdate){
           options.afterUpdate(component);
        }
    }
    //调用setState, forceUpdate钩子
    if (component._renderCallbacks!=null) {
        while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
    }
    //执行其他组件的更新或插入,diffLevel为一个全局变量
    if (!diffLevel && !isChild) flushMounts();
}

这个函数出现的对象与关系太多了,究竟某某是某某的什么,看下图就知了。

clipboard.png

我们需要知道,组件render后可能产生普通虚拟DOM与子组件,而只有普通虚拟DOM才能转化为真实DOM。组件的实例通过_component_parentComponent联结在一块,方便上下回溯。而实例总是保存着最后转化出来的真实DOM(base, 也叫initialBase)。base上保存着最上面的那个组件实例,也就是_component,此外,为了方便比较,它的构造器也放在DOM节点上。

renderComponent这个方法主要处理组件更新时的钩子,及建立父子组件间的联系。

这个方法的参数的起名也很奇葩,如果改成

renderComponent(componentInstance, renderModel, isRenderByReactDOM, isRenderChildComponent)

则好理解些。显示preact的作者不太想知道其奥秘,因此源码的注释也很少很少。

好了,我们看setComponentProps方法,它在renderComponent用了两次。

//更新已有的子组件实例
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
//新旧子组件的类型不一致,用新组件的实例进行替换
setComponentProps(inst, childProps, NO_RENDER, context, false);

setComponentProps的源码

export function setComponentProps(component, props, opts, context, mountAll) {
    if (component._disable) return;
    //_disable状态下阻止用户
    component._disable = true;

    if ((component.__ref = props.ref)) delete props.ref;
    if ((component.__key = props.key)) delete props.key;

    if (!component.base || mountAll) {
    //如果没有插入到DOM树或正在被ReactDOM.render渲染
        if (component.componentWillMount) component.componentWillMount();
    }
    else if (component.componentWillReceiveProps) {
    //如果是在更新过程中
        component.componentWillReceiveProps(props, context);
    }
    //下面依次设置provProps, props, prevContext, context
    if (context && context!==component.context) {
        if (!component.prevContext) component.prevContext = component.context;
        component.context = context;
    }
    
    if (!component.prevProps) component.prevProps = component.props;
    component.props = props;

    component._disable = false;
    //=====================
    if (opts!==NO_RENDER) {
        if (opts===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) {
            renderComponent(component, SYNC_RENDER, mountAll);
        }
        else {
            enqueueRender(component);
        }
    }

    if (component.__ref) component.__ref(component);
}

最后看 createComponent,这是创建一个组件实例。React的组件有三种,经典组件,纯组件,无状态组件,前两种都是类的形式,可以归为一种,最后一种是普通函数。但在src/vdom/component-recycler.js我们看到它们都是new出来的。

export function createComponent(Ctor, props, context) {
    let list = components[Ctor.name],
        inst;
    //类形式的组件
    if (Ctor.prototype && Ctor.prototype.render) {
        inst = new Ctor(props, context);
        Component.call(inst, props, context);
    }else {//无状态组件
        inst = new Component(props, context);
        inst.constructor = Ctor;
        inst.render = doRender;
    }

    if (list) {
        for (let i=list.length; i--; ) {
            if (list[i].constructor===Ctor) {
                inst.nextBase = list[i].nextBase;
                list.splice(i, 1);
                break;
            }
        }
    }
    return inst;
}

我们再看一下doRender,这时恍然大悟,原来preact是统一所有组件以后更新都要通过render方法生成它的普通虚拟DOM或子组件。

function doRender(props, state, context) {
    return this.constructor(props, context);
}

此外,preact还通过collectComponent来回收它的真实DOM,然后在createComponent中重复利用。这是它高效的缘由之一。

const components = {};

export function collectComponent(component) {
    let name = component.constructor.name;
    (components[name] || (components[name] = [])).push(component);
}

司徒正美
5.6k 声望3.5k 粉丝

穿梭于二次元与二进制间的魔法师( ̄(工) ̄) 凸ส้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้้