摘要:組件渲染首先我們來了解組件返回的虛擬是怎么渲染為真實(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,使得_dirty為true,但因?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ù)雜,將vnode的attributes和chidlren的屬性賦值到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ù)createComponent與setComponentProps的內(nèi)部細(xì)節(jié)。
關(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ù)props與context分別對應(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傳入props和context,那么inst中的props和context的屬性為undefined,通過強(qiáng)制調(diào)用Component.call(inst, props, context)可以給inst中props、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)的渲染。
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、context與mountAll的含義從名字就可以看出來,值得注意地是參數(shù)opts,代表的是不同的刷新模式:
NO_RENDER: 不進(jìn)行渲染
SYNC_RENDER: 同步渲染
FORCE_RENDER: 強(qiáng)制刷新渲染
ASYNC_RENDER: 異步渲染
首先如果組件component中_disable屬性為true時(shí)則直接退出,否則將屬性_disable置為true,其目的相當(dāng)于一個(gè)鎖,保證修改過程的原子性。如果傳入組件的屬性props中存在ref與key,則將其分別緩存在組件的__ref與__key,并將其從props將其刪除。
組件實(shí)例中的base中存放的是之前組件實(shí)例對應(yīng)的真實(shí)dom節(jié)點(diǎn),如果不存在該屬性,說明是該組件的初次渲染,如果組件中定義了生命周期函數(shù)(鉤子函數(shù))componentWillMount,則在此處執(zhí)行。如果不是首次執(zhí)行,如果存在生命周期函數(shù)componentWillReceiveProps,則需要將最新的props與context作為參數(shù)調(diào)用componentWillReceiveProps。然后分別將當(dāng)前的屬性context與props緩存在組件的preContext與prevProps屬性中,并將context與props屬性更新為最新的context與props。最后將組件的_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(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í)例中的props、context、state屬性表示的是最新的所要渲染的組件實(shí)例屬性。而對應(yīng)的preProps、preContext、preState代表的是渲染之前上一個(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.state、this.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、prevState、prevContext的屬性,以及將_dirty屬性置為false。需要注意的是只有_dirty為false才會(huì)被放入更新隊(duì)列,然后_dirty會(huì)被置為true,這樣組件實(shí)例就不會(huì)被多次放入更新隊(duì)列。
如果沒有跳過更新的過程(即skip為false),則執(zhí)行到第二段代碼。首先執(zhí)行組件實(shí)例的render函數(shù)(相比于React中的render函數(shù),Preact中的render函數(shù)執(zhí)行時(shí)傳入了參數(shù)props、state、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è)置組件的ref和key等,以及調(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)的是diff的dom參數(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)HOC1、HOC2與compoent的base屬性都指向最后的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í)行。
在第八段代碼中,如果diffLevel為0并且isChild為false時(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í)肯定componentRoot是false,diffLevel表示渲染的層次,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函數(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í)renderComponent的opt參數(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
摘要:本系列文章將重點(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的秘密...
摘要:新聞熱點(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...
摘要:是一個(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...
摘要:另外第三方也可以通過的事件插件機(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元素上直接綁定...
摘要:前端日報(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注...
閱讀 2423·2021-11-16 11:44
閱讀 1891·2021-10-12 10:12
閱讀 2185·2021-09-22 15:22
閱讀 3018·2021-08-11 11:17
閱讀 1513·2019-08-29 16:53
閱讀 2661·2019-08-29 14:09
閱讀 3483·2019-08-29 14:03
閱讀 3311·2019-08-29 11:09