国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

從Preact了解一個(gè)類React的框架是怎么實(shí)現(xiàn)的(三): 組件

AlphaWatch / 1170人閱讀

摘要:組件渲染首先我們來了解組件返回的虛擬是怎么渲染為真實(shí),來看一下的組件是如何構(gòu)造的可能我們會(huì)想當(dāng)然地認(rèn)為組件的構(gòu)造函數(shù)定義將會(huì)及其復(fù)雜,事實(shí)上恰恰相反,的組件定義代碼極少。

前言

  首先歡迎大家關(guān)注我的掘金賬號和Github博客,也算是對我的一點(diǎn)鼓勵(lì),畢竟寫東西沒法獲得變現(xiàn),能堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)。
  之前分享過幾篇關(guān)于React的文章:

React技術(shù)內(nèi)幕: key帶來了什么

React技術(shù)內(nèi)幕: setState的秘密

  其實(shí)我在閱讀React源碼的時(shí)候,真的非常痛苦。React的代碼及其復(fù)雜、龐大,閱讀起來挑戰(zhàn)非常大,但是這卻又擋不住我們的React的原理的好奇。前段時(shí)間有人就安利過Preact,千行代碼就基本實(shí)現(xiàn)了React的絕大部分功能,相比于React動(dòng)輒幾萬行的代碼,Preact顯得別樣的簡潔,這也就為了我們學(xué)習(xí)React開辟了另一條路。本系列文章將重點(diǎn)分析類似于React的這類框架是如何實(shí)現(xiàn)的,歡迎大家關(guān)注和討論。如有不準(zhǔn)確的地方,歡迎大家指正。
  
  在前兩篇文章

從preact了解一個(gè)類React的框架是怎么實(shí)現(xiàn)的(一): 元素創(chuàng)建

從Preact了解一個(gè)類React的框架是怎么實(shí)現(xiàn)的(二): 元素diff

  我們分別了解了Preact中元素創(chuàng)建以及diff算法,其中就講到了組件相關(guān)一部分內(nèi)容。對于一個(gè)類React庫,組件(Component)可能是最需要著重分析的部分,因?yàn)榫帉戭怰eact程序的過程中,我們幾乎都是在寫一個(gè)個(gè)組件(Component)并將其組合起來形成我們所需要的應(yīng)用。下面我們就從頭開始了解一下Preact中的組件是怎么實(shí)現(xiàn)的。
  

組件渲染

  首先我們來了解組件返回的虛擬dom是怎么渲染為真實(shí)dom,來看一下Preact的組件是如何構(gòu)造的:
  

//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() {}

});

  可能我們會(huì)想當(dāng)然地認(rèn)為組件Component的構(gòu)造函數(shù)定義將會(huì)及其復(fù)雜,事實(shí)上恰恰相反,Preact的組件定義代碼極少。組件的實(shí)例屬性僅僅有四個(gè):

_dirty: 用來表示存在臟數(shù)據(jù)(即數(shù)據(jù)與存在的對應(yīng)渲染不一致),例如多次在組件實(shí)例調(diào)用setState,使得_dirtytrue,但因?yàn)樵搶傩缘拇嬖冢粫?huì)使得組件僅有一次才會(huì)被放入更新隊(duì)列。

context: 組件的context屬性

props: 組件的props屬性

state: 組件的state屬性

  通過extends方法(原理類似于ES6中的Object.assign或者Underscore.js中的_.extends),我們給組件的構(gòu)造函數(shù)的原型中創(chuàng)建一下幾個(gè)方法:

setState: 與React的setState相同,用來更新組件的state

forceUpdate: 與React的forceUpdate相同,立刻同步重新渲染組件

render: 返回組件的渲染內(nèi)容的虛擬dom,此處函數(shù)體為空

  所以當(dāng)我們編寫組件(Component)類繼承preact.Component時(shí),也就僅僅只能繼承上述的方法和屬性,這樣所以對于用戶而言,不僅提供了及其簡潔的API以供使用,而且最重要的是我們將組件內(nèi)部的邏輯封裝起來,與用戶相隔離,避免用戶無意間修改了組件的內(nèi)部實(shí)現(xiàn),造成不必要的錯(cuò)誤。
  
  對于閱讀過從Preact了解一個(gè)類React的框架是怎么實(shí)現(xiàn)的(二): 元素diff的同學(xué)應(yīng)該還記的preact所提供的render函數(shù)調(diào)用了內(nèi)部的diff函數(shù),而diff實(shí)際會(huì)調(diào)用idiff函數(shù)(更詳細(xì)的可以閱讀第二篇文章):
  

  從上面的圖中可以看到,在idiff函數(shù)內(nèi)部中在開始如果vnode.nodeName是函數(shù)(function)類型,則會(huì)調(diào)用函數(shù)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;
}

  函數(shù)buildComponentFromVNode的作用就是將表示組件的虛擬dom(VNode)轉(zhuǎn)化成真實(shí)dom。參數(shù)分別是:

dom: 組件對應(yīng)的真實(shí)dom節(jié)點(diǎn)

vnode: 組件的虛擬dom節(jié)點(diǎn)

context: 組件的中的context屬性

mountAll: 表示組件的內(nèi)容需要重新渲染而不是基于上一次渲染內(nèi)容進(jìn)行修改。

  為了方便分析,我們將函數(shù)分解成幾個(gè)部分,依次分析:
  

第一段代碼: dom是組件對應(yīng)的真實(shí)dom節(jié)點(diǎn)(如果未渲染,則為undefined),在dom節(jié)點(diǎn)中的_component屬性是組件實(shí)例的緩存。isDirectOwner用來指示用來標(biāo)識原dom節(jié)點(diǎn)對應(yīng)的組件類型是否與當(dāng)前虛擬dom的組件類型相同。然后使用函數(shù)getNodeProps來獲取虛擬dom節(jié)點(diǎn)的屬性值。

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

  函數(shù)getNodeProps的邏輯并不復(fù)雜,將vnodeattributeschidlren的屬性賦值到props,然后如果存在組件中存在defaultProps的話,將defaultProps存在的屬性并且對應(yīng)props不存在的屬性賦值進(jìn)入了props中,并將props返回。

第二段代碼: 如果當(dāng)前的dom節(jié)點(diǎn)對應(yīng)的組件類型與當(dāng)前虛擬dom對應(yīng)的組件類型不一致時(shí),會(huì)向上在父組件中查找到與虛擬dom節(jié)點(diǎn)類型相同的組件實(shí)例(但也有可能不存在)。其實(shí)這個(gè)只是針對于高階組件,假設(shè)有高階組件的順序:

HOC  => component => DOM元素

  上面HOC代表高階組件,返回組件component,然后組件component渲染DOM元素。在Preact,這種高階組件與返回的子組件之間存在屬性標(biāo)識,即HOC的組件實(shí)例中的_component指向compoent的組件實(shí)例而組件component實(shí)例的_parentComponent屬性指向HOC實(shí)例。我們知道,DOM中的屬性_component指向的是對應(yīng)的組件實(shí)例,需要注意的是在上面的例子中DOM對應(yīng)的_component指向的是HOC實(shí)例,而不是component實(shí)例。如果理解了上面的部分,就能理解為什么會(huì)存在這個(gè)循環(huán)了,其目的就是為了找到最開始渲染該DOM的高階組件(防止某些情況下dom對應(yīng)的_component屬性指代的實(shí)例被修改),然后再判斷該高階組件是否與當(dāng)前的vnode類型一致。

第三段代碼: 如果存在當(dāng)前虛擬dom對應(yīng)的組件實(shí)例存在,則直接調(diào)用函數(shù)setComponentProps,相當(dāng)于基于組件的實(shí)例進(jìn)行修改渲染,然后組件實(shí)例中的base屬性即為最新的dom節(jié)點(diǎn)。

第四段代碼: 我們先不具體關(guān)心某個(gè)函數(shù)的具體實(shí)現(xiàn)細(xì)節(jié),只關(guān)注代碼邏輯。首先如果之前的dom節(jié)點(diǎn)對應(yīng)存在組件,并且虛擬dom對應(yīng)的組件類型與其不相同時(shí),則卸載之前的組件(unmountComponent)。接著我們通過調(diào)用函數(shù)createComponent創(chuàng)建當(dāng)前虛擬dom對應(yīng)的組件實(shí)例,然后調(diào)用函數(shù)setComponentProps去創(chuàng)建組件實(shí)例的dom節(jié)點(diǎn),最后如果當(dāng)前的dom與之前的dom元素不相同時(shí),將之前的dom回收(recollectNodeTree函數(shù)在diff的文章中已經(jīng)介紹)。

  其實(shí)如果之前就閱讀過Preact的diff算法的同學(xué)來說,其實(shí)整個(gè)組件大致渲染的流程我們已經(jīng)清楚了,但是如果想要更深層次的了解其中的細(xì)節(jié)我們必須去深究函數(shù)createComponentsetComponentProps的內(nèi)部細(xì)節(jié)。
  

createComponent

  關(guān)于函數(shù)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的主要作用就是為了能重用組件渲染的內(nèi)容而設(shè)置的共享池(Share Pool),通過函數(shù)collectComponent就可以實(shí)現(xiàn)回收一個(gè)組件以供以后重復(fù)利用。在函數(shù)collectComponent中通過組件名(component.constructor.name)分類將可重用的組件緩存在緩存池中。
  
  函數(shù)createComponent主要作用就是創(chuàng)建組件實(shí)例。參數(shù)propscontext分別對應(yīng)的是組件的中屬性和context(與React一致),而Ctor組件則是需要?jiǎng)?chuàng)建的組件類型(函數(shù)或者是類)。我們知道如果我們的組件定義用ES6定義如下:

class App extends Component{}

  我們知道class僅僅只是一個(gè)語法糖,上面的代碼使用ES5去實(shí)現(xiàn)相當(dāng)于:

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

  如果你對ES5中的Object.create也不熟悉的話,我簡要的介紹一下,Object.create的作用就是實(shí)現(xiàn)原型繼承(Prototypal Inheritance)來實(shí)現(xiàn)基于已有對象創(chuàng)建新對象。Object.create的第一個(gè)參數(shù)就是所要繼承的原型對象,第二個(gè)參數(shù)就是新對象定義額外屬性的對象(類似于Object.defineProperty的參數(shù)),如果要我自己實(shí)現(xiàn)一個(gè)簡單的Object.create函數(shù)我們可以這樣寫:
  

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

  現(xiàn)在你肯定知道了如果你的組件繼承了Preact中的Component的話,在原型中一定存在render方法,這時(shí)候通過new創(chuàng)建Ctor的實(shí)例inst(實(shí)例中已經(jīng)含有了你自定義的render函數(shù)),但是如果沒有給父級構(gòu)造函數(shù)super傳入propscontext,那么inst中的propscontext的屬性為undefined,通過強(qiáng)制調(diào)用Component.call(inst, props, context)可以給instprops、context進(jìn)行初始化賦值。
  
  如果組件中不存在render函數(shù),說明該函數(shù)是PFC(Pure Function Component)類型,即是純函數(shù)組件。這時(shí)直接調(diào)用函數(shù)Component創(chuàng)建實(shí)例,實(shí)例的constructor屬性設(shè)置為傳入的函數(shù)。由于實(shí)例中不存在render函數(shù),則將doRender函數(shù)作為實(shí)例的render屬性,doRender函數(shù)會(huì)將Ctor的返回的虛擬dom作為結(jié)果返回。
  
  然后我們從組件回收的共享池中那拿到同類型組件的實(shí)例,從其中取出該實(shí)例之前渲染的實(shí)例(nextBase),然后將其賦值到我們的新創(chuàng)建組件實(shí)例的nextBase屬性上,其目的就是為了能基于此DOM元素進(jìn)行渲染,以更少的代價(jià)進(jìn)行相關(guān)的渲染。

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

  函數(shù)setComponentProps的主要作用就是為組件實(shí)例設(shè)置屬性(props),其中props通常來源于JSX中的屬性(attributes)。函數(shù)的參數(shù)component、props、contextmountAll的含義從名字就可以看出來,值得注意地是參數(shù)opts,代表的是不同的刷新模式:

NO_RENDER: 不進(jìn)行渲染

SYNC_RENDER: 同步渲染

FORCE_RENDER: 強(qiáng)制刷新渲染

ASYNC_RENDER: 異步渲染

  首先如果組件component_disable屬性為true時(shí)則直接退出,否則將屬性_disable置為true,其目的相當(dāng)于一個(gè),保證修改過程的原子性。如果傳入組件的屬性props中存在refkey,則將其分別緩存在組件的__ref__key,并將其從props將其刪除。
  
  組件實(shí)例中的base中存放的是之前組件實(shí)例對應(yīng)的真實(shí)dom節(jié)點(diǎn),如果不存在該屬性,說明是該組件的初次渲染,如果組件中定義了生命周期函數(shù)(鉤子函數(shù))componentWillMount,則在此處執(zhí)行。如果不是首次執(zhí)行,如果存在生命周期函數(shù)componentWillReceiveProps,則需要將最新的propscontext作為參數(shù)調(diào)用componentWillReceiveProps。然后分別將當(dāng)前的屬性contextprops緩存在組件的preContextprevProps屬性中,并將contextprops屬性更新為最新的contextprops。最后將組件的_disable屬性置回false。
  
  如果組件更新的模式為NO_RENDER,則不需要進(jìn)行渲染。如果是同步渲染(SYNC_RENDER)或者是首次渲染(base屬性為空),則執(zhí)行函數(shù)renderComponent,其余情況下(例如setState觸發(fā)的異步渲染ASYNC_RENDER)均執(zhí)行函數(shù)enqueueRender(enqueueRender函數(shù)將在setState處分析)。在函數(shù)的最后,如果存在ref函數(shù),則將組件實(shí)例作為參數(shù)調(diào)用ref函數(shù)。在這里我們可以顯然可以看出在Preact中是不支持React的中字符串類型的ref屬性,不過這個(gè)也并不重要,因?yàn)镽eact本身也不推薦使用字符串類型的ref屬性,并表示可能會(huì)在將來版本中廢除這一屬性。
  
  接下來我們還需要了解renderComponent函數(shù)(非常冗長)與enqueueRender函數(shù)的作用:
  

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

  為了方便閱讀,我們將代碼分成了八個(gè)部分,不過為了更方便的閱讀代碼,我們首先看一下函數(shù)開始處的變量聲明:
  
  所要渲染的component實(shí)例中的propscontext、state屬性表示的是最新的所要渲染的組件實(shí)例屬性。而對應(yīng)的prePropspreContextpreState代表的是渲染之前上一個(gè)狀態(tài)組件實(shí)例屬性。變量isUpdate代表的是當(dāng)前是處于組件更新的過程還是組件渲染的過程(mount),我們通過之前組件實(shí)例是否對應(yīng)存在真實(shí)DOM節(jié)點(diǎn)來判斷,如果存在則認(rèn)為是更新的過程,否則認(rèn)為是渲染(mount)過程。nextBase表示可以基于此DOM元素進(jìn)行修改(可能來源于上一次渲染或者是回收之前同類型的組件實(shí)例),以尋求最小的渲染代價(jià)。
組件實(shí)例中的_component屬性表示的組件的子組件,僅僅只有當(dāng)組件返回的是組件時(shí)(也就是當(dāng)前組件為高階組件),才會(huì)存在。變量skip用來標(biāo)志是否需要跳過更新的過程(例如: 生命周期函數(shù)shouldComponentUpdate返回false)。

第一段代碼: 如果存在component.base存在,說明該組件之前對應(yīng)的真實(shí)dom元素,說明組件處于更新的過程。要將props、state、context替換成之前的previousProps、previousState、previousContext,這是因?yàn)樵谏芷诤瘮?shù)shouldComponentUpdate、componentWillUpdate中的this.props、this.statethis.context仍然是更新前的狀態(tài)。如果不是強(qiáng)制刷新(FORCE_RENDER)并存在生命周期函數(shù)shouldComponentUpdate,則以最新的props、state、context作為參數(shù)執(zhí)行shouldComponentUpdate,如果返回的結(jié)果為false表明要跳過此次的刷新過程,即置標(biāo)志位skip為true。否則如果生命周期shouldComponentUpdate返回的不是false(說明如果不返回值或者其他非false的值,都會(huì)執(zhí)行更新),則查看生命周期函數(shù)componentWillUpdate是否存在,存在則執(zhí)行。最后則將組件實(shí)例的props、state、context替換成最新的狀態(tài),并置空組件實(shí)例中的prevProps、prevStateprevContext的屬性,以及將_dirty屬性置為false。需要注意的是只有_dirtyfalse才會(huì)被放入更新隊(duì)列,然后_dirty會(huì)被置為true,這樣組件實(shí)例就不會(huì)被多次放入更新隊(duì)列。

如果沒有跳過更新的過程(即skipfalse),則執(zhí)行到第二段代碼。首先執(zhí)行組件實(shí)例的render函數(shù)(相比于React中的render函數(shù),Preact中的render函數(shù)執(zhí)行時(shí)傳入了參數(shù)propsstate、context),執(zhí)行render函數(shù)的返回值rendered則是組件實(shí)例對應(yīng)的虛擬dom元素(VNode)。如果組件存在函數(shù)getChildContext,則生成當(dāng)前需要傳遞給子組件的context。我們從代碼extend(extend({}, context), component.getChildContext())可以看出,如果父組件存在某個(gè)context屬性并且當(dāng)前組件實(shí)例中getChildContext函數(shù)返回的context也存在相同的屬性時(shí),那么當(dāng)前組件實(shí)例getChildContext返回的context中的屬性會(huì)覆蓋父組件的context中的相同屬性。

接下來到第三段代碼,childComponent是組件實(shí)例render函數(shù)返回的虛擬dom的類型(rendered.nodeName),如果childComponent的類型為函數(shù),說明該組件為高階組件(High Order Component),如果你不了解高階組件,可以戳這篇文章。如果是高階組件的情況下,首先通過getNodeProps函數(shù)獲得虛擬dom中子組件的屬性。如果組件存在子組件的實(shí)例并且子組件實(shí)例的構(gòu)造函數(shù)與當(dāng)前組件返回的子組件虛擬dom類型相同(inst.constructor===childComponent)而且前后的key值相同時(shí)(childProps.key==inst.__key),僅需要以同步渲染(SYNC_RENDER)的模式遞歸調(diào)用函數(shù)setComponentProps來更新子組件的屬性props。之所以這樣是因?yàn)槿绻麧M足前面的條件說明,前后兩次渲染的子組件對應(yīng)的實(shí)例不發(fā)生改變,僅改變傳入子組件的參數(shù)(props)。這時(shí)子組件僅需要根據(jù)當(dāng)前最新的props對應(yīng)渲染真實(shí)dom即可。否則如果之前的子組件實(shí)例的構(gòu)造函數(shù)與當(dāng)前組件返回的子組件虛擬dom類型不相同時(shí)或者根據(jù)key值標(biāo)定兩個(gè)組件實(shí)例不相同時(shí),則需要渲染的新的子組件,不僅需要調(diào)用createComponent創(chuàng)建子組件的實(shí)例(createComponent(childComponent, childProps, context))并為當(dāng)前的子組件和組件設(shè)置相關(guān)關(guān)系(即_component_parentComponent屬性)而且用toUnmount指示待卸載的組件實(shí)例。然后通過調(diào)用setComponentProps來設(shè)置組件的refkey等,以及調(diào)用組件的相關(guān)生命周期函數(shù)(例如:componentWillMount),需要注意的是這里的調(diào)用模式是NO_RENDER,不會(huì)進(jìn)行渲染。而在下一句調(diào)用renderComponent(inst, SYNC_RENDER, mountAll, true)去同步地渲染子組件。所以我們就要注意為什么在調(diào)用函數(shù)setComponentProps時(shí)沒有采用SYNC_RENDER模式,SYNC_RENDER模式也本身就會(huì)觸發(fā)renderComponent去渲染組件,其原因就是為了在調(diào)用renerComponent賦予isChild值為true,這個(gè)標(biāo)志量的作用我們后面可以看到。調(diào)用完renderComponent之后,inst.base中已經(jīng)是我們子組件渲染的真實(shí)dom節(jié)點(diǎn)。

在第四段代碼中,處理的是當(dāng)前組件需要渲染的虛擬dom類型是非組件類型(即普通的DOM元素)。首先賦值cbase = initialBase,我們知道initialBase來自于initialBase = isUpdate || nextBase,也就是說如果當(dāng)前是更新的模式,則initialBase等于isUpdate,即為上次組件渲染的內(nèi)容。否則,如果組件實(shí)例存在nextBase(從回收池得到的DOM結(jié)構(gòu)),也可以基于其進(jìn)行修改,總的目的是為了以更少的代價(jià)去渲染。如果之前的組件渲染的是函數(shù)類型的元素(即組件),但現(xiàn)在卻渲染的是非函數(shù)類型的,賦值toUnmount = initialChildComponent,用來存儲之后需要卸載的組件,并且由于cbase對應(yīng)的是之前的組件的dom節(jié)點(diǎn),因此就無法使用了,需要賦值cbase = null以使得重新渲染。而component._component = null目的就是切斷之前組件間的父子關(guān)系,畢竟現(xiàn)在返回的都不是組件。如果是同步渲染(SYNC_RENDER),則會(huì)通過調(diào)用idiff函數(shù)去渲染組件返回的虛擬dom(詳情見第二篇文章diff)。我們來看看調(diào)用idiff函數(shù)的形參和實(shí)參:

cbase對應(yīng)的是diffdom參數(shù),表示用來渲染的VNode之前的真實(shí)dom??梢钥吹饺绻笆墙M件類型,那么cbase值為undefined,我們就需要重新開始渲染。否則我們就可以在之前的渲染基礎(chǔ)上更新以尋求最小的更新代價(jià)。

rendered對應(yīng)diff中的vnode參數(shù),表示需要渲染的虛擬dom節(jié)點(diǎn)。

context對應(yīng)diff中的context參數(shù),表示組件的context屬性。

mountAll || !isUpdate對應(yīng)的是diff中的mountAll參數(shù),表示是否是重新渲染DOM節(jié)點(diǎn)而不是基于之前的DOM修改,!isUpdate表示的就是非更新狀態(tài)。

initialBase && initialBase.parentNode對應(yīng)的是diff中的parent參數(shù),表示的是當(dāng)前渲染節(jié)點(diǎn)的父級節(jié)點(diǎn)。

diff函數(shù)的第六個(gè)參數(shù)為componentRoot,實(shí)參為true表示的是當(dāng)前diff是以組件中render函數(shù)的渲染內(nèi)容的形式調(diào)用,也可以說當(dāng)前的渲染內(nèi)容是屬于組件類型的。

  我們知道idiff函數(shù)返回的是虛擬dom對應(yīng)渲染后的真實(shí)dom節(jié)點(diǎn),所以變量base存儲的就是本次組件渲染的真實(shí)DOM元素。

代碼第五部分: 如果組件前后返回的虛擬dom節(jié)點(diǎn)對應(yīng)的真實(shí)DOM節(jié)點(diǎn)不相同,或者前后返回的虛擬DOM節(jié)點(diǎn)對應(yīng)的前后組件實(shí)例不一致時(shí),則在父級的DOM元素中將之前的DOM節(jié)點(diǎn)替換成當(dāng)前對應(yīng)渲染的DOM節(jié)點(diǎn)(baseParent.replaceChild(base, initialBase)),如果沒有需要卸載的組件實(shí)例,則調(diào)用函數(shù)recollectNodeTree回收該DOM節(jié)點(diǎn)。否則如果之前組件渲染的是函數(shù)類型的元素,但需要廢棄,則調(diào)用函數(shù)unmountComponent進(jìn)行卸載(調(diào)用相關(guān)的生命周期函數(shù))。

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函數(shù)的作用,首先將函數(shù)實(shí)例中的_disable置為true表示組件禁用,如果組件存在生命周期函數(shù)componentWillUnmount進(jìn)行調(diào)用。然后遞歸調(diào)用函數(shù)unmountComponent遞歸卸載組件。如果之前組件渲染的DOM節(jié)點(diǎn),并且最外層節(jié)點(diǎn)存在ref函數(shù),則以參數(shù)null執(zhí)行(和React保持一致,ref函數(shù)會(huì)執(zhí)行兩次,第一次是mount會(huì)以DOM元素或者組件實(shí)例回調(diào),第二次是unmount會(huì)回調(diào)null表示卸載)。然后將DOM元素存入nextBase用以回收。調(diào)用removeNode函數(shù)作用是將base節(jié)點(diǎn)的父節(jié)點(diǎn)脫離出來。函數(shù)removeChildren的目的是用遞歸遍歷所有的子DOM元素,回收節(jié)點(diǎn)(之前的文章已經(jīng)介紹過,其中就涉及到子元素的ref調(diào)用)。最后如果組件本身存在ref屬性,則直接以null為參數(shù)調(diào)用。

代碼第六部分:component.base = base用來將當(dāng)前的組件渲染的dom元素存儲在組件實(shí)例的base屬性中。下面的代碼我們先舉個(gè)例子,假如有如下的結(jié)構(gòu):

HOC1 => HOC2 => component => DOM元素

  其中HOC代表高階組件,component代表自定義組件。你會(huì)發(fā)現(xiàn)HOC1HOC2compoentbase屬性都指向最后的DOM元素,而DOM元素的中的_component是指向HOC1的組價(jià)實(shí)例的??炊诉@個(gè)你就能明白為什么會(huì)存在下面這個(gè)循環(huán)語句,其目的就是為了給父組件賦值正確的base屬性以及為DOM節(jié)點(diǎn)的_component屬性賦值正確的組件實(shí)例。

在第七段代碼中,如果是非更新模式,則需要將當(dāng)前組件存入mounts(unshift方法存入,pop方法取出,實(shí)質(zhì)上是相當(dāng)于隊(duì)列的方式,并且子組件先于父組件存儲隊(duì)列mounts,因此可以保證正確的調(diào)用順序),方便在后期調(diào)用組件對應(yīng)類似于componentDidMount生命周期函數(shù)和其他的操作。如果沒有跳過更新過程(skip === false),則在此時(shí)調(diào)用組件對應(yīng)的生命周期函數(shù)componentDidUpdate。然后如果存在組件存在_renderCallbacks屬性(存儲對應(yīng)的setState的回調(diào)函數(shù),因?yàn)?b>setState函數(shù)實(shí)質(zhì)也是通過renderComponent實(shí)現(xiàn)的),則在此處將其彈出并執(zhí)行。

在第八段代碼中,如果diffLevel0并且isChildfalse時(shí),對應(yīng)執(zhí)行flushMounts函數(shù)

function flushMounts() {
    let c;
    while ((c=mounts.pop())) {
        if (c.componentDidMount) c.componentDidMount();
    }
}

  其實(shí)flushMounts也是非常的簡單,就是將隊(duì)列mounts中取出組件實(shí)例,然后如果存在生命周期函數(shù)componentDidMount,則對應(yīng)執(zhí)行。
  
  其實(shí)如果閱讀了之前diff的文章的同學(xué)應(yīng)該記得在diff函數(shù)中有:

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

  上面有兩處調(diào)用函數(shù)flushMounts,一個(gè)是在renderComponent內(nèi)部①,一個(gè)是在diff函數(shù)②。那么在什么情況下觸發(fā)上下兩段代碼呢?首先componentRoot表示的是當(dāng)前diff是不是以組件中渲染內(nèi)容的形式調(diào)用(比如組件中render函數(shù)返回HTML類型的VNode),那么preact.render函數(shù)調(diào)用時(shí)肯定componentRootfalsediffLevel表示渲染的層次,diffLevel回減到0說明已經(jīng)要結(jié)束diff的調(diào)用,所以在使用preact.render渲染的最后肯定會(huì)使用上面的代碼去調(diào)用函數(shù)flushMounts。但是如果其中某個(gè)已經(jīng)渲染的組件通過setState或者forceUpdate的方式導(dǎo)致了重新渲染并且致使子組件創(chuàng)建了新的實(shí)例(比如前后兩次返回了不同的組件類型),這時(shí),就會(huì)采用第一種方式在調(diào)用flushMounts函數(shù)。

setState

  對于Preact的組件而言,state是及其重要的部分。其中涉及到的API為setState,定義在函數(shù)Component的原型中,這樣所有的繼承于Component的自定義組件實(shí)例都可以引用到函數(shù)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接受兩個(gè)參數(shù): 新的state以及state更新后的回調(diào)函數(shù),其中state既可以是對象類型的部分對象,也可以是函數(shù)類型。首先使用函數(shù)extend生成當(dāng)前state的拷貝prevState,存儲之前的state的狀態(tài)。然后如果
state類型為函數(shù)時(shí),將函數(shù)的生成值覆蓋進(jìn)入state,否則直接將新的state覆蓋進(jìn)入state,此時(shí)this.state已經(jīng)成為了新的state。如果setState存在第二個(gè)參數(shù)callback,則將其存入實(shí)例屬性_renderCallbacks(如果不存在_renderCallbacks屬性,則需要初始化)。然后執(zhí)行函數(shù)enqueueRender。

enqueueRender

  接下來我們看一下神奇的enqueueRender函數(shù):
  

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;

  我們可以看到當(dāng)組件實(shí)例中的_dirty屬性為false時(shí),會(huì)將屬性_dirty置為true,并將其放入items中。當(dāng)更新隊(duì)列第一次被items時(shí),則延遲異步執(zhí)行函數(shù)rerender。這個(gè)延遲異步函數(shù)在支持Promise的瀏覽器中,會(huì)使用Promise.resolve().then,否則會(huì)使用setTimeout。
  
  rerender函數(shù)就是將items中待更新的組件,逐個(gè)取出,并對其執(zhí)行renderComponent。其實(shí)renderComponentopt參數(shù)不傳入ASYNC_RENDER,而是傳入undefined兩者之間并無區(qū)別。唯一要注意的是:
  

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

  我們渲染過程一定是要執(zhí)行diff,那就說明initialBase一定是個(gè)非假值,這也是可以保證的。
  

initialBase = isUpdate || nextBase

  其實(shí)因?yàn)橹敖M件已經(jīng)渲染過,所以是可以保證isUpdate一定為非假值,因?yàn)?b>isUpdate = component.base并且component.base是一定存在的并且為上次渲染的內(nèi)容。大家可能會(huì)擔(dān)心如果上次組件render函數(shù)返回的是null該怎么辦?其實(shí)閱讀過第二篇文章的同學(xué)應(yīng)該知道在idiff函數(shù)內(nèi)部
  

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

  即使render返回的是null也會(huì)被當(dāng)做一個(gè)空文本去控制,對應(yīng)會(huì)渲染成DOM中的Text類型。

  

forceUpdate
extend(Component.prototype,{
    //.......

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

  執(zhí)行forceUpdate所需要做的就是將回調(diào)函數(shù)放入組件實(shí)例中的_renderCallbacks屬性并調(diào)用函數(shù)renderComponent強(qiáng)制刷新當(dāng)前的組件。需要注意的是,我們渲染的模式是FORCE_RENDER強(qiáng)制刷新,與其他的模式到的區(qū)別就是不需要經(jīng)過生命周期函數(shù)shouldComponentUpdate的判斷,直接進(jìn)行刷新。

結(jié)語

  至此我們已經(jīng)看完了Preact中的組件相關(guān)的代碼,可能并沒有對每一個(gè)場景都進(jìn)行講解,但是我也盡量嘗試去覆蓋所有相關(guān)的部分。代碼相對比較長,看起來也經(jīng)常令人頭疼,有時(shí)候?yàn)榱烁闱宄硞€(gè)變量的部分不得不數(shù)次回顧。但是你會(huì)發(fā)現(xiàn)你多次地、反復(fù)性的閱讀、仔細(xì)地推敲,代碼的含義會(huì)逐漸清晰。書讀百遍其義自見,其實(shí)對代碼來說也是一樣的。文章若有不正確的地方,歡迎指出,共同學(xué)習(xí)。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/89337.html

相關(guān)文章

  • Preact了解一個(gè)React框架怎么實(shí)現(xiàn)(二): 元素diff

    摘要:本系列文章將重點(diǎn)分析類似于的這類框架是如何實(shí)現(xiàn)的,歡迎大家關(guān)注和討論。作為一個(gè)極度精簡的庫,函數(shù)是屬于本身的。 前言   首先歡迎大家關(guān)注我的掘金賬號和Github博客,也算是對我的一點(diǎn)鼓勵(lì),畢竟寫東西沒法獲得變現(xiàn),能堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)?! ≈胺窒磉^幾篇關(guān)于React的文章: React技術(shù)內(nèi)幕: key帶來了什么 React技術(shù)內(nèi)幕: setState的秘密...

    張巨偉 評論0 收藏0
  • 前端清單第 27 期:React Patent License 回復(fù),Shopify WebVR 購

    摘要:新聞熱點(diǎn)國內(nèi)國外,前端最新動(dòng)態(tài)就開源許可證風(fēng)波進(jìn)行回復(fù)數(shù)周前,基金會(huì)決定禁止旗下項(xiàng)目使用,因?yàn)槠湓跇?biāo)準(zhǔn)的許可證之外添加了專利聲明此舉引發(fā)了社區(qū)的廣泛討論,希望能夠更新其開源許可證。 showImg(https://segmentfault.com/img/remote/1460000010777089); 前端每周清單第 27 期:React Patent License 回復(fù),Sho...

    jeffrey_up 評論0 收藏0
  • 幫你讀懂preact源碼(一)

    摘要:是一個(gè)最小的庫,但由于其對尺寸的追求,它的很多代碼可讀性比較差,市面上也很少有全面且詳細(xì)介紹的文章,本篇文章希望能幫助你學(xué)習(xí)的源碼。建議與源碼一起閱讀本文。 作為一名前端,我們需要深入學(xué)習(xí)react的運(yùn)行機(jī)制,但是react源碼量已經(jīng)相當(dāng)龐大,從學(xué)習(xí)的角度,性價(jià)比不高,所以學(xué)習(xí)一個(gè)react mini庫是一個(gè)深入學(xué)習(xí)react的一個(gè)不錯(cuò)的方法。 preact是一個(gè)最小的react mi...

    XboxYan 評論0 收藏0
  • 談?wù)?em>React事件機(jī)制和未來(react-events)

    摘要:另外第三方也可以通過的事件插件機(jī)制來合成自定義事件,盡管很少人這么做。抽象跨平臺事件機(jī)制。打算干預(yù)事件的分發(fā)。事件是的一個(gè)自定義事件,旨在規(guī)范化表單元素的變動(dòng)事件。 showImg(https://segmentfault.com/img/remote/1460000019961124?w=713&h=307); 當(dāng)我們在組件上設(shè)置事件處理器時(shí),React并不會(huì)在該DOM元素上直接綁定...

    TNFE 評論0 收藏0
  • 2017-10-27 前端日報(bào)

    摘要:前端日報(bào)精選裝飾器場景實(shí)戰(zhàn)配置之后端渲染理解同步異步和事件循環(huán)編寫高性能注意點(diǎn)線性漸變實(shí)現(xiàn)虛線等簡單實(shí)用圖形中文服務(wù)端渲染開發(fā)指南個(gè)人文章系列之事件類型個(gè)人文章使用必記掘金簡介掘金性能大亂斗前端雜談中簡單的數(shù)據(jù)圖形化 2017-10-27 前端日報(bào) 精選 JS 裝飾器(Decorator)場景實(shí)戰(zhàn)webpack配置之后端渲染JavaScript:理解同步、異步和事件循環(huán)編寫高性能js注...

    Jeffrrey 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<