從Preact瞭解一個類React的框架是怎麼實現的(三): 組件

前言

  首先歡迎你們關注個人掘金帳號和Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。
  以前分享過幾篇關於React的文章:javascript

  其實我在閱讀React源碼的時候,真的很是痛苦。React的代碼及其複雜、龐大,閱讀起來挑戰很是大,可是這卻又擋不住咱們的React的原理的好奇。前段時間有人就安利過Preact,千行代碼就基本實現了React的絕大部分功能,相比於React動輒幾萬行的代碼,Preact顯得別樣的簡潔,這也就爲了咱們學習React開闢了另外一條路。本系列文章將重點分析相似於React的這類框架是如何實現的,歡迎你們關注和討論。若有不許確的地方,歡迎你們指正。
  
  在前兩篇文章java

  咱們分別瞭解了Preact中元素建立以及diff算法,其中就講到了組件相關一部份內容。對於一個類React庫,組件(Component)多是最須要着重分析的部分,由於編寫類React程序的過程當中,咱們幾乎都是在寫一個個組件(Component)並將其組合起來造成咱們所須要的應用。下面咱們就從頭開始瞭解一下Preact中的組件是怎麼實現的。
  node

組件渲染

  首先咱們來了解組件返回的虛擬dom是怎麼渲染爲真實dom,來看一下Preact的組件是如何構造的:
  react

//component.js
function Component(props, context) {
    this._dirty = true;

    this.context = context;

    this.props = props;

    this.state = this.state || {};
}

extend(Component.prototype, {

    setState(state, callback) {
        //......
    },

    forceUpdate(callback) {
        //......
    },

    render() {}

});

  可能咱們會想固然地認爲組件Component的構造函數定義將會及其複雜,事實上偏偏相反,Preact的組件定義代碼極少。組件的實例屬性僅僅有四個:git

  • _dirty: 用來表示存在髒數據(即數據與存在的對應渲染不一致),例如屢次在組件實例調用setState,使得_dirtytrue,但由於該屬性的存在,只會使得組件僅有一次纔會被放入更新隊列。
  • context: 組件的context屬性
  • props: 組件的props屬性
  • state: 組件的state屬性

  經過extends方法(原理相似於ES6中的Object.assign或者Underscore.js中的_.extends),咱們給組件的構造函數的原型中建立一下幾個方法:github

  • setState: 與React的setState相同,用來更新組件的state
  • forceUpdate: 與React的forceUpdate相同,馬上同步從新渲染組件
  • render: 返回組件的渲染內容的虛擬dom,此處函數體爲空

  因此當咱們編寫組件(Component)類繼承preact.Component時,也就僅僅只能繼承上述的方法和屬性,這樣因此對於用戶而言,不只提供了及其簡潔的API以供使用,並且最重要的是咱們將組件內部的邏輯封裝起來,與用戶相隔離,避免用戶無心間修改了組件的內部實現,形成沒必要要的錯誤。
  
  對於閱讀過從Preact瞭解一個類React的框架是怎麼實現的(二): 元素diff的同窗應該還記的preact所提供的render函數調用了內部的diff函數,而diff實際會調用idiff函數(更詳細的能夠閱讀第二篇文章):
  
算法

  從上面的圖中能夠看到,在idiff函數內部中在開始若是vnode.nodeName是函數(function)類型,則會調用函數buildComponentFromVNode:
  數組

function buildComponentFromVNode(dom, vnode, context, mountAll) {
    //block-1
    let c = dom && dom._component,
        originalComponent = c,
        oldDom = dom,
        isDirectOwner = c && dom._componentConstructor===vnode.nodeName,
        isOwner = isDirectOwner,
        props = getNodeProps(vnode);
    //block-2    
    while (c && !isOwner && (c=c._parentComponent)) {
        isOwner = c.constructor===vnode.nodeName;
    }
    //block-3
    if (c && isOwner && (!mountAll || c._component)) {
        setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
        dom = c.base;
    }
    else {
    //block-4
        if (originalComponent && !isDirectOwner) {
            unmountComponent(originalComponent);
            dom = oldDom = null;
        }

        c = createComponent(vnode.nodeName, props, context);
        if (dom && !c.nextBase) {
            c.nextBase = dom;
            oldDom = null;
        }
        setComponentProps(c, props, SYNC_RENDER, context, mountAll);
        dom = c.base;
        
        if (oldDom && dom!==oldDom) {
            oldDom._component = null;
            recollectNodeTree(oldDom, false);
        }
    }
    return dom;
}

  函數buildComponentFromVNode的做用就是將表示組件的虛擬dom(VNode)轉化成真實dom。參數分別是:瀏覽器

  • dom: 組件對應的真實dom節點
  • vnode: 組件的虛擬dom節點
  • context: 組件的中的context屬性
  • mountAll: 表示組件的內容須要從新渲染而不是基於上一次渲染內容進行修改。

  爲了方便分析,咱們將函數分解成幾個部分,依次分析:
  緩存

  • 第一段代碼: dom是組件對應的真實dom節點(若是未渲染,則爲undefined),在dom節點中的_component屬性是組件實例的緩存。isDirectOwner用來指示用來標識原dom節點對應的組件類型是否與當前虛擬dom的組件類型相同。而後使用函數getNodeProps來獲取虛擬dom節點的屬性值。
getNodeProps(vnode) {
    let props = extend({}, vnode.attributes);
    props.children = vnode.children;

    let defaultProps = vnode.nodeName.defaultProps;
    if (defaultProps!==undefined) {
        for (let i in defaultProps) {
            if (props[i]===undefined) {
                props[i] = defaultProps[i];
            }
        }
    }
    
    return props;
}

  函數getNodeProps的邏輯並不複雜,將vnodeattributeschidlren的屬性賦值到props,而後若是存在組件中存在defaultProps的話,將defaultProps存在的屬性而且對應props不存在的屬性賦值進入了props中,並將props返回。

  • 第二段代碼: 若是當前的dom節點對應的組件類型與當前虛擬dom對應的組件類型不一致時,會向上在父組件中查找到與虛擬dom節點類型相同的組件實例(但也有可能不存在)。其實這個只是針對於高階組件,假設有高階組件的順序:
HOC  => component => DOM元素

  上面HOC表明高階組件,返回組件component,而後組件component渲染DOM元素。在Preact,這種高階組件與返回的子組件之間存在屬性標識,即HOC的組件實例中的_component指向compoent的組件實例而組件component實例的_parentComponent屬性指向HOC實例。咱們知道,DOM中的屬性_component指向的是對應的組件實例,須要注意的是在上面的例子中DOM對應的_component指向的是HOC實例,而不是component實例。若是理解了上面的部分,就能理解爲何會存在這個循環了,其目的就是爲了找到最開始渲染該DOM的高階組件(防止某些狀況下dom對應的_component屬性指代的實例被修改),而後再判斷該高階組件是否與當前的vnode類型一致。

  • 第三段代碼: 若是存在當前虛擬dom對應的組件實例存在,則直接調用函數setComponentProps,至關於基於組件的實例進行修改渲染,而後組件實例中的base屬性即爲最新的dom節點。
  • 第四段代碼: 咱們先不具體關心某個函數的具體實現細節,只關注代碼邏輯。首先若是以前的dom節點對應存在組件,而且虛擬dom對應的組件類型與其不相同時,則卸載以前的組件(unmountComponent)。接着咱們經過調用函數createComponent建立當前虛擬dom對應的組件實例,而後調用函數setComponentProps去建立組件實例的dom節點,最後若是當前的dom與以前的dom元素不相同時,將以前的dom回收(recollectNodeTree函數在diff的文章中已經介紹)。

  其實若是以前就閱讀過Preact的diff算法的同窗來講,其實整個組件大體渲染的流程咱們已經清楚了,可是若是想要更深層次的瞭解其中的細節咱們必須去深究函數createComponentsetComponentProps的內部細節。
  

createComponent

  關於函數createComponent,咱們看一下component-recycler.js文件:
  

import { Component } from '../component';

const components = {};

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

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

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

  變量components的主要做用就是爲了能重用組件渲染的內容而設置的共享池(Share Pool),經過函數collectComponent就能夠實現回收一個組件以供之後重複利用。在函數collectComponent中經過組件名(component.constructor.name)分類將可重用的組件緩存在緩存池中。
  
  函數createComponent主要做用就是建立組件實例。參數propscontext分別對應的是組件的中屬性和context(與React一致),而Ctor組件則是須要建立的組件類型(函數或者是類)。咱們知道若是咱們的組件定義用ES6定義以下:

class App extends Component{}

  咱們知道class僅僅只是一個語法糖,上面的代碼使用ES5去實現至關於:

function App(){}
App.prototype = Object.create(Component.prototype, {
    constructor: {
        value: App,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

  若是你對ES5中的Object.create也不熟悉的話,我簡要的介紹一下,Object.create的做用就是實現原型繼承(Prototypal Inheritance)來實現基於已有對象建立新對象。Object.create的第一個參數就是所要繼承的原型對象,第二個參數就是新對象定義額外屬性的對象(相似於Object.defineProperty的參數),若是要我本身實現一個簡單的Object.create函數咱們能夠這樣寫:
  

function create(prototype, ...obj){
    function F(){}
    F.prototype = prototype;
    return Object.defineProperties(new F(), ...obj);
}

  如今你確定知道了若是你的組件繼承了Preact中的Component的話,在原型中必定存在render方法,這時候經過new建立Ctor的實例inst(實例中已經含有了你自定義的render函數),可是若是沒有給父級構造函數super傳入propscontext,那麼inst中的propscontext的屬性爲undefined,經過強制調用Component.call(inst, props, context)能夠給instpropscontext進行初始化賦值。
  
  若是組件中不存在render函數,說明該函數是PFC(Pure Function Component)類型,便是純函數組件。這時直接調用函數Component建立實例,實例的constructor屬性設置爲傳入的函數。因爲實例中不存在render函數,則將doRender函數做爲實例的render屬性,doRender函數會將Ctor的返回的虛擬dom做爲結果返回。
  
  而後咱們從組件回收的共享池中那拿到同類型組件的實例,從其中取出該實例以前渲染的實例(nextBase),而後將其賦值到咱們的新建立組件實例的nextBase屬性上,其目的就是爲了能基於此DOM元素進行渲染,以更少的代價進行相關的渲染。

setComponentProps

function setComponentProps(component, props, opts, context, mountAll) {
    if (component._disable) return;
    component._disable = true;

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

    if (!component.base || mountAll) {
        if (component.componentWillMount) component.componentWillMount();
    }
    else if (component.componentWillReceiveProps) {
        component.componentWillReceiveProps(props, 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 || !component.base) {
            renderComponent(component, SYNC_RENDER, mountAll);
        }
        else {
            enqueueRender(component);
        }
    }

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

  函數setComponentProps的主要做用就是爲組件實例設置屬性(props),其中props一般來源於JSX中的屬性(attributes)。函數的參數componentpropscontextmountAll的含義從名字就能夠看出來,值得注意地是參數opts,表明的是不一樣的刷新模式:

  • NO_RENDER: 不進行渲染
  • SYNC_RENDER: 同步渲染
  • FORCE_RENDER: 強制刷新渲染
  • ASYNC_RENDER: 異步渲染

  首先若是組件component_disable屬性爲true時則直接退出,不然將屬性_disable置爲true,其目的至關於一個,保證修改過程的原子性。若是傳入組件的屬性props中存在refkey,則將其分別緩存在組件的__ref__key,並將其從props將其刪除。
  
  組件實例中的base中存放的是以前組件實例對應的真實dom節點,若是不存在該屬性,說明是該組件的初次渲染,若是組件中定義了生命週期函數(鉤子函數)componentWillMount,則在此處執行。若是不是首次執行,若是存在生命週期函數componentWillReceiveProps,則須要將最新的propscontext做爲參數調用componentWillReceiveProps。而後分別將當前的屬性contextprops緩存在組件的preContextprevProps屬性中,並將contextprops屬性更新爲最新的contextprops。最後將組件的_disable屬性置回false
  
  若是組件更新的模式爲NO_RENDER,則不須要進行渲染。若是是同步渲染(SYNC_RENDER)或者是首次渲染(base屬性爲空),則執行函數renderComponent,其他狀況下(例如setState觸發的異步渲染ASYNC_RENDER)均執行函數enqueueRender(enqueueRender函數將在setState處分析)。在函數的最後,若是存在ref函數,則將組件實例做爲參數調用ref函數。在這裏咱們能夠顯然能夠看出在Preact中是不支持React的中字符串類型的ref屬性,不過這個也並不重要,由於React自己也不推薦使用字符串類型的ref屬性,並表示可能會在未來版本中廢除這一屬性。
  
  接下來咱們還須要瞭解renderComponent函數(很是冗長)與enqueueRender函數的做用:
  

renderComponent

renderComponent(component, opts, mountAll, isChild) {
    if (component._disable) return;

    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,
        initialBase = isUpdate || nextBase,
        initialChildComponent = component._component,
        skip = false,
        rendered, inst, cbase;
    // block-1
    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;
    }

    component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
    component._dirty = false;
    if (!skip) {
        // block-2
        rendered = component.render(props, state, context);

        if (component.getChildContext) {
            context = extend(extend({}, context), component.getChildContext());
        }
   
        let childComponent = rendered && rendered.nodeName,
            toUnmount, base;
        //block-3
        if (typeof childComponent==='function') {
            let childProps = getNodeProps(rendered);
            inst = initialChildComponent;

            if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
                setComponentProps(inst, childProps, SYNC_RENDER, context, false);
            }
            else {
                toUnmount = inst;

                component._component = inst = createComponent(childComponent, childProps, context);
                inst.nextBase = inst.nextBase || nextBase;
                inst._parentComponent = component;
                setComponentProps(inst, childProps, NO_RENDER, context, false);
                renderComponent(inst, SYNC_RENDER, mountAll, true);
            }

            base = inst.base;
        }
        else {
        //block-4
            cbase = initialBase;

            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);
            }
        }
        // block-5
        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);
        }
        
        //block-6
        component.base = base;
        if (base && !isChild) {
            let componentRef = component,
                t = component;
            while ((t=t._parentComponent)) {
                (componentRef = t).base = base;
            }
            base._component = componentRef;
            base._componentConstructor = componentRef.constructor;
        }
    }
    //block-7
    if (!isUpdate || mountAll) {
        mounts.unshift(component);
    }
    else if (!skip) {

        if (component.componentDidUpdate) {
            component.componentDidUpdate(previousProps, previousState, previousContext);
        }
    }

    if (component._renderCallbacks!=null) {
        while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
    }
    //block-8
    if (!diffLevel && !isChild) flushMounts();
}

  爲了方便閱讀,咱們將代碼分紅了八個部分,不過爲了更方便的閱讀代碼,咱們首先看一下函數開始處的變量聲明:
  
  所要渲染的component實例中的propscontextstate屬性表示的是最新的所要渲染的組件實例屬性。而對應的prePropspreContextpreState表明的是渲染以前上一個狀態組件實例屬性。變量isUpdate表明的是當前是處於組件更新的過程仍是組件渲染的過程(mount),咱們經過以前組件實例是否對應存在真實DOM節點來判斷,若是存在則認爲是更新的過程,不然認爲是渲染(mount)過程。nextBase表示能夠基於此DOM元素進行修改(可能來源於上一次渲染或者是回收以前同類型的組件實例),以尋求最小的渲染代價。
組件實例中的_component屬性表示的組件的子組件,僅僅只有當組件返回的是組件時(也就是當前組件爲高階組件),纔會存在。變量skip用來標誌是否須要跳過更新的過程(例如: 生命週期函數shouldComponentUpdate返回false)。

    • 第一段代碼: 若是存在component.base存在,說明該組件以前對應的真實dom元素,說明組件處於更新的過程。要將propsstatecontext替換成以前的previousPropspreviousStatepreviousContext,這是由於在生命週期函數shouldComponentUpdatecomponentWillUpdate中的this.propsthis.statethis.context仍然是更新前的狀態。若是不是強制刷新(FORCE_RENDER)並存在生命週期函數shouldComponentUpdate,則以最新的propsstatecontext做爲參數執行shouldComponentUpdate,若是返回的結果爲false代表要跳過這次的刷新過程,即置標誌位skip爲true。不然若是生命週期shouldComponentUpdate返回的不是false(說明若是不返回值或者其餘非false的值,都會執行更新),則查看生命週期函數componentWillUpdate是否存在,存在則執行。最後則將組件實例的propsstatecontext替換成最新的狀態,並置空組件實例中的prevPropsprevStateprevContext的屬性,以及將_dirty屬性置爲false。須要注意的是隻有_dirtyfalse纔會被放入更新隊列,而後_dirty會被置爲true,這樣組件實例就不會被屢次放入更新隊列。
    • 若是沒有跳過更新的過程(即skipfalse),則執行到第二段代碼。首先執行組件實例的render函數(相比於React中的render函數,Preact中的render函數執行時傳入了參數propsstatecontext),執行render函數的返回值rendered則是組件實例對應的虛擬dom元素(VNode)。若是組件存在函數getChildContext,則生成當前須要傳遞給子組件的context。咱們從代碼extend(extend({}, context), component.getChildContext())能夠看出,若是父組件存在某個context屬性而且當前組件實例中getChildContext函數返回的context也存在相同的屬性時,那麼當前組件實例getChildContext返回的context中的屬性會覆蓋父組件的context中的相同屬性。
    • 接下來到第三段代碼,childComponent是組件實例render函數返回的虛擬dom的類型(rendered.nodeName),若是childComponent的類型爲函數,說明該組件爲高階組件(High Order Component),若是你不瞭解高階組件,能夠戳這篇文章。若是是高階組件的狀況下,首先經過getNodeProps函數得到虛擬dom中子組件的屬性。若是組件存在子組件的實例而且子組件實例的構造函數與當前組件返回的子組件虛擬dom類型相同(inst.constructor===childComponent)並且先後的key值相同時(childProps.key==inst.__key),僅須要以同步渲染(SYNC_RENDER)的模式遞歸調用函數setComponentProps來更新子組件的屬性props。之因此這樣是由於若是知足前面的條件說明,先後兩次渲染的子組件對應的實例不發生改變,僅改變傳入子組件的參數(props)。這時子組件僅須要根據當前最新的props對應渲染真實dom便可。不然若是以前的子組件實例的構造函數與當前組件返回的子組件虛擬dom類型不相同時或者根據key值標定兩個組件實例不相同時,則須要渲染的新的子組件,不只須要調用createComponent建立子組件的實例(createComponent(childComponent, childProps, context))併爲當前的子組件和組件設置相關關係(即_component_parentComponent屬性)並且用toUnmount指示待卸載的組件實例。而後經過調用setComponentProps來設置組件的refkey等,以及調用組件的相關生命週期函數(例如:componentWillMount),須要注意的是這裏的調用模式是NO_RENDER,不會進行渲染。而在下一句調用renderComponent(inst, SYNC_RENDER, mountAll, true)去同步地渲染子組件。因此咱們就要注意爲何在調用函數setComponentProps時沒有采用SYNC_RENDER模式,SYNC_RENDER模式也自己就會觸發renderComponent去渲染組件,其緣由就是爲了在調用renerComponent賦予isChild值爲true,這個標誌量的做用咱們後面能夠看到。調用完renderComponent以後,inst.base中已是咱們子組件渲染的真實dom節點。
    • 在第四段代碼中,處理的是當前組件須要渲染的虛擬dom類型是非組件類型(即普通的DOM元素)。首先賦值cbase = initialBase,咱們知道initialBase來自於initialBase = isUpdate || nextBase,也就是說若是當前是更新的模式,則initialBase等於isUpdate,即爲上次組件渲染的內容。不然,若是組件實例存在nextBase(從回收池獲得的DOM結構),也能夠基於其進行修改,總的目的是爲了以更少的代價去渲染。若是以前的組件渲染的是函數類型的元素(即組件),但如今卻渲染的是非函數類型的,賦值toUnmount = initialChildComponent,用來存儲以後須要卸載的組件,而且因爲cbase對應的是以前的組件的dom節點,所以就沒法使用了,須要賦值cbase = null以使得從新渲染。而component._component = null目的就是切斷以前組件間的父子關係,畢竟如今返回的都不是組件。若是是同步渲染(SYNC_RENDER),則會經過調用idiff函數去渲染組件返回的虛擬dom(詳情見第二篇文章diff)。咱們來看看調用idiff函數的形參和實參:
    1. cbase對應的是diffdom參數,表示用來渲染的VNode以前的真實dom。能夠看到若是以前是組件類型,那麼cbase值爲undefined,咱們就須要從新開始渲染。不然咱們就能夠在以前的渲染基礎上更新以尋求最小的更新代價。
    1. rendered對應diff中的vnode參數,表示須要渲染的虛擬dom節點。
    2. context對應diff中的context參數,表示組件的context屬性。
    3. mountAll || !isUpdate對應的是diff中的mountAll參數,表示是不是從新渲染DOM節點而不是基於以前的DOM修改,!isUpdate表示的就是非更新狀態。
    4. initialBase && initialBase.parentNode對應的是diff中的parent參數,表示的是當前渲染節點的父級節點。
    5. diff函數的第六個參數爲componentRoot,實參爲true表示的是當前diff是以組件中render函數的渲染內容的形式調用,也能夠說當前的渲染內容是屬於組件類型的。

      咱們知道idiff函數返回的是虛擬dom對應渲染後的真實dom節點,因此變量base存儲的就是本次組件渲染的真實DOM元素。

    • 代碼第五部分: 若是組件先後返回的虛擬dom節點對應的真實DOM節點不相同,或者先後返回的虛擬DOM節點對應的先後組件實例不一致時,則在父級的DOM元素中將以前的DOM節點替換成當前對應渲染的DOM節點(baseParent.replaceChild(base, initialBase)),若是沒有須要卸載的組件實例,則調用函數recollectNodeTree回收該DOM節點。不然若是以前組件渲染的是函數類型的元素,但須要廢棄,則調用函數unmountComponent進行卸載(調用相關的生命週期函數)。
    function unmountComponent(component) {
        let base = component.base;
        component._disable = true;
    
        if (component.componentWillUnmount) component.componentWillUnmount();
    
        component.base = null;
    
        let inner = component._component;
        if (inner) {
            unmountComponent(inner);
        }
        else if (base) {
            if (base[ATTR_KEY] && base[ATTR_KEY].ref) base[ATTR_KEY].ref(null);
            component.nextBase = base;
            removeNode(base);
            collectComponent(component);
            removeChildren(base);
        }
        if (component.__ref) component.__ref(null);
    }

      來看unmountComponent函數的做用,首先將函數實例中的_disable置爲true表示組件禁用,若是組件存在生命週期函數componentWillUnmount進行調用。而後遞歸調用函數unmountComponent遞歸卸載組件。若是以前組件渲染的DOM節點,而且最外層節點存在ref函數,則以參數null執行(和React保持一致,ref函數會執行兩次,第一次是mount會以DOM元素或者組件實例回調,第二次是unmount會回調null表示卸載)。而後將DOM元素存入nextBase用以回收。調用removeNode函數做用是將base節點的父節點脫離出來。函數removeChildren的目的是用遞歸遍歷全部的子DOM元素,回收節點(以前的文章已經介紹過,其中就涉及到子元素的ref調用)。最後若是組件自己存在ref屬性,則直接以null爲參數調用。

    • 代碼第六部分:component.base = base用來將當前的組件渲染的dom元素存儲在組件實例的base屬性中。下面的代碼咱們先舉個例子,假若有以下的結構:
    HOC1 => HOC2 => component => DOM元素

      其中HOC表明高階組件,component表明自定義組件。你會發現HOC1HOC2compoentbase屬性都指向最後的DOM元素,而DOM元素的中的_component是指向HOC1的組價實例的。看懂了這個你就能明白爲何會存在下面這個循環語句,其目的就是爲了給父組件賦值正確的base屬性以及爲DOM節點的_component屬性賦值正確的組件實例。

    • 在第七段代碼中,若是是非更新模式,則須要將當前組件存入mounts(unshift方法存入,pop方法取出,實質上是至關於隊列的方式,而且子組件先於父組件存儲隊列mounts,所以能夠保證正確的調用順序),方便在後期調用組件對應相似於componentDidMount生命週期函數和其餘的操做。若是沒有跳過更新過程(skip === false),則在此時調用組件對應的生命週期函數componentDidUpdate。而後若是存在組件存在_renderCallbacks屬性(存儲對應的setState的回調函數,由於setState函數實質也是經過renderComponent實現的),則在此處將其彈出並執行。
    • 在第八段代碼中,若是diffLevel0而且isChildfalse時,對應執行flushMounts函數
    function flushMounts() {
        let c;
        while ((c=mounts.pop())) {
            if (c.componentDidMount) c.componentDidMount();
        }
    }

      其實flushMounts也是很是的簡單,就是將隊列mounts中取出組件實例,而後若是存在生命週期函數componentDidMount,則對應執行。
      
      其實若是閱讀了以前diff的文章的同窗應該記得在diff函數中有:

    function diff(dom, vnode, context, mountAll, parent, componentRoot) {
        //......
        if (!--diffLevel) {
            // ......
            if (!componentRoot) flushMounts();
        }
    }

      上面有兩處調用函數flushMounts,一個是在renderComponent內部①,一個是在diff函數②。那麼在什麼狀況下觸發上下兩段代碼呢?首先componentRoot表示的是當前diff是否是以組件中渲染內容的形式調用(好比組件中render函數返回HTML類型的VNode),那麼preact.render函數調用時確定componentRootfalsediffLevel表示渲染的層次,diffLevel回減到0說明已經要結束diff的調用,因此在使用preact.render渲染的最後確定會使用上面的代碼去調用函數flushMounts。可是若是其中某個已經渲染的組件經過setState或者forceUpdate的方式致使了從新渲染而且導致子組件建立了新的實例(好比先後兩次返回了不一樣的組件類型),這時,就會採用第一種方式在調用flushMounts函數。

    setState

      對於Preact的組件而言,state是及其重要的部分。其中涉及到的API爲setState,定義在函數Component的原型中,這樣全部的繼承於Component的自定義組件實例均可以引用到函數setState

    extend(Component.prototype,{
        //.......
    
        setState(state, callback) {
            let s = this.state;
            if (!this.prevState) this.prevState = extend({}, s);
            extend(s, typeof ··==='function' ? state(s, this.props) : state);
            if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
            enqueueRender(this);
        }
        
        //......
    });

      首先咱們看到setState接受兩個參數: 新的state以及state更新後的回調函數,其中state既能夠是對象類型的部分對象,也能夠是函數類型。首先使用函數extend生成當前state的拷貝prevState,存儲以前的state的狀態。而後若是
    state類型爲函數時,將函數的生成值覆蓋進入state,不然直接將新的state覆蓋進入state,此時this.state已經成爲了新的state。若是setState存在第二個參數callback,則將其存入實例屬性_renderCallbacks(若是不存在_renderCallbacks屬性,則須要初始化)。而後執行函數enqueueRender

    enqueueRender

      接下來咱們看一下神奇的enqueueRender函數:
      

    let items = [];
    
    function enqueueRender(component) {
        if (!component._dirty && (component._dirty = true) && items.push(component) == 1) {
            defer(rerender);
        }
    }
    
    function rerender() {
        let p, list = items;
        items = [];
        while ((p = list.pop())) {
            if (p._dirty) renderComponent(p);
        }
    }
    
    const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout;

      咱們能夠看到當組件實例中的_dirty屬性爲false時,會將屬性_dirty置爲true,並將其放入items中。當更新隊列第一次被items時,則延遲異步執行函數rerender。這個延遲異步函數在支持Promise的瀏覽器中,會使用Promise.resolve().then,不然會使用setTimeout
      
      rerender函數就是將items中待更新的組件,逐個取出,並對其執行renderComponent。其實renderComponentopt參數不傳入ASYNC_RENDER,而是傳入undefined二者之間並沒有區別。惟一要注意的是:
      

    //renderComponent內部
    if (initialBase || opts===SYNC_RENDER) {
        base = diff(//...;
    }

      咱們渲染過程必定是要執行diff,那就說明initialBase必定是個非假值,這也是能夠保證的。
      

    initialBase = isUpdate || nextBase

      其實由於以前組件已經渲染過,因此是能夠保證isUpdate必定爲非假值,由於isUpdate = component.base而且component.base是必定存在的而且爲上次渲染的內容。你們可能會擔憂若是上次組件render函數返回的是null該怎麼辦?其實閱讀過第二篇文章的同窗應該知道在idiff函數內部
      

    if (vnode==null || typeof vnode==='boolean') vnode = '';

      即便render返回的是null也會被當作一個空文本去控制,對應會渲染成DOM中的Text類型。

      

    forceUpdate

    extend(Component.prototype,{
        //.......
    
        forceUpdate(callback) {
            if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
            renderComponent(this, FORCE_RENDER);
        }
        
        //......
    });

      執行forceUpdate所須要作的就是將回調函數放入組件實例中的_renderCallbacks屬性並調用函數renderComponent強制刷新當前的組件。須要注意的是,咱們渲染的模式是FORCE_RENDER強制刷新,與其餘的模式到的區別就是不須要通過生命週期函數shouldComponentUpdate的判斷,直接進行刷新。

    結語

      至此咱們已經看完了Preact中的組件相關的代碼,可能並無對每個場景都進行講解,可是我也儘可能嘗試去覆蓋全部相關的部分。代碼相對比較長,看起來也常常使人頭疼,有時候爲了搞清楚某個變量的部分不得不數次回顧。可是你會發現你屢次地、反覆性的閱讀、仔細地推敲,代碼的含義會逐漸清晰。書讀百遍其義自見,其實對代碼來講也是同樣的。文章如有不正確的地方,歡迎指出,共同窗習。

    相關文章
    相關標籤/搜索