preact源碼學習(2)

本節咱們看如何更新組件。在上一節也反覆提到renderComponent這個方法了,這節直接從它入手吧。它位於src/vdom/component.js文件中。javascript

從參數來看,咱們會驚訝它居然會有這麼多參數,原來咱們只看到它有兩個參數,第二個爲數字。第一個參數不用說,是組件的實例。java

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();
}

這個函數出現的對象與關係太多了,究竟某某是某某的什麼,看下圖就知了。node

clipboard.png

咱們須要知道,組件render後可能產生普通虛擬DOM與子組件,而只有普通虛擬DOM才能轉化爲真實DOM。組件的實例經過_component_parentComponent聯結在一塊,方便上下回溯。而實例老是保存着最後轉化出來的真實DOM(base, 也叫initialBase)。base上保存着最上面的那個組件實例,也就是_component,此外,爲了方便比較,它的構造器也放在DOM節點上。react

renderComponent這個方法主要處理組件更新時的鉤子,及創建父子組件間的聯繫。數組

這個方法的參數的起名也很奇葩,若是改爲dom

renderComponent(componentInstance, renderModel, isRenderByReactDOM, isRenderChildComponent)

則好理解些。顯示preact的做者不太想知道其奧祕,所以源碼的註釋也不多不多。異步

好了,咱們看setComponentProps方法,它在renderComponent用了兩次。函數

//更新已有的子組件實例
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
//新舊子組件的類型不一致,用新組件的實例進行替換
setComponentProps(inst, childProps, NO_RENDER, context, false);

setComponentProps的源碼this

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出來的。spa

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);
}
相關文章
相關標籤/搜索