摘要:組件渲染首先我們來了解組件返回的虛擬是怎么渲染為真實,來看一下的組件是如何構(gòu)造的可能我們會想當然地認為組件的構(gòu)造函數(shù)定義將會及其復雜,事實上恰恰相反,的組件定義代碼極少。
前言
首先歡迎大家關(guān)注我的掘金賬號和Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現(xiàn),能堅持下去也是靠的是自己的熱情和大家的鼓勵。
之前分享過幾篇關(guān)于React的文章:
React技術(shù)內(nèi)幕: key帶來了什么
React技術(shù)內(nèi)幕: setState的秘密
其實我在閱讀React源碼的時候,真的非常痛苦。React的代碼及其復雜、龐大,閱讀起來挑戰(zhàn)非常大,但是這卻又擋不住我們的React的原理的好奇。前段時間有人就安利過Preact,千行代碼就基本實現(xiàn)了React的絕大部分功能,相比于React動輒幾萬行的代碼,Preact顯得別樣的簡潔,這也就為了我們學習React開辟了另一條路。本系列文章將重點分析類似于React的這類框架是如何實現(xiàn)的,歡迎大家關(guān)注和討論。如有不準確的地方,歡迎大家指正。
在前兩篇文章
從preact了解一個類React的框架是怎么實現(xiàn)的(一): 元素創(chuàng)建
從Preact了解一個類React的框架是怎么實現(xiàn)的(二): 元素diff
我們分別了解了Preact中元素創(chuàng)建以及diff算法,其中就講到了組件相關(guān)一部分內(nèi)容。對于一個類React庫,組件(Component)可能是最需要著重分析的部分,因為編寫類React程序的過程中,我們幾乎都是在寫一個個組件(Component)并將其組合起來形成我們所需要的應用。下面我們就從頭開始了解一下Preact中的組件是怎么實現(xiàn)的。
首先我們來了解組件返回的虛擬dom是怎么渲染為真實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() {} });
可能我們會想當然地認為組件Component的構(gòu)造函數(shù)定義將會及其復雜,事實上恰恰相反,Preact的組件定義代碼極少。組件的實例屬性僅僅有四個:
_dirty: 用來表示存在臟數(shù)據(jù)(即數(shù)據(jù)與存在的對應渲染不一致),例如多次在組件實例調(diào)用setState,使得_dirty為true,但因為該屬性的存在,只會使得組件僅有一次才會被放入更新隊列。
context: 組件的context屬性
props: 組件的props屬性
state: 組件的state屬性
通過extends方法(原理類似于ES6中的Object.assign或者Underscore.js中的_.extends),我們給組件的構(gòu)造函數(shù)的原型中創(chuàng)建一下幾個方法:
setState: 與React的setState相同,用來更新組件的state
forceUpdate: 與React的forceUpdate相同,立刻同步重新渲染組件
render: 返回組件的渲染內(nèi)容的虛擬dom,此處函數(shù)體為空
所以當我們編寫組件(Component)類繼承preact.Component時,也就僅僅只能繼承上述的方法和屬性,這樣所以對于用戶而言,不僅提供了及其簡潔的API以供使用,而且最重要的是我們將組件內(nèi)部的邏輯封裝起來,與用戶相隔離,避免用戶無意間修改了組件的內(nèi)部實現(xiàn),造成不必要的錯誤。
對于閱讀過從Preact了解一個類React的框架是怎么實現(xiàn)的(二): 元素diff的同學應該還記的preact所提供的render函數(shù)調(diào)用了內(nèi)部的diff函數(shù),而diff實際會調(diào)用idiff函數(shù)(更詳細的可以閱讀第二篇文章):
從上面的圖中可以看到,在idiff函數(shù)內(nèi)部中在開始如果vnode.nodeName是函數(shù)(function)類型,則會調(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)化成真實dom。參數(shù)分別是:
dom: 組件對應的真實dom節(jié)點
vnode: 組件的虛擬dom節(jié)點
context: 組件的中的context屬性
mountAll: 表示組件的內(nèi)容需要重新渲染而不是基于上一次渲染內(nèi)容進行修改。
為了方便分析,我們將函數(shù)分解成幾個部分,依次分析:
第一段代碼: dom是組件對應的真實dom節(jié)點(如果未渲染,則為undefined),在dom節(jié)點中的_component屬性是組件實例的緩存。isDirectOwner用來指示用來標識原dom節(jié)點對應的組件類型是否與當前虛擬dom的組件類型相同。然后使用函數(shù)getNodeProps來獲取虛擬dom節(jié)點的屬性值。
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的邏輯并不復雜,將vnode的attributes和chidlren的屬性賦值到props,然后如果存在組件中存在defaultProps的話,將defaultProps存在的屬性并且對應props不存在的屬性賦值進入了props中,并將props返回。
第二段代碼: 如果當前的dom節(jié)點對應的組件類型與當前虛擬dom對應的組件類型不一致時,會向上在父組件中查找到與虛擬dom節(jié)點類型相同的組件實例(但也有可能不存在)。其實這個只是針對于高階組件,假設(shè)有高階組件的順序:
HOC => component => DOM元素
上面HOC代表高階組件,返回組件component,然后組件component渲染DOM元素。在Preact,這種高階組件與返回的子組件之間存在屬性標識,即HOC的組件實例中的_component指向compoent的組件實例而組件component實例的_parentComponent屬性指向HOC實例。我們知道,DOM中的屬性_component指向的是對應的組件實例,需要注意的是在上面的例子中DOM對應的_component指向的是HOC實例,而不是component實例。如果理解了上面的部分,就能理解為什么會存在這個循環(huán)了,其目的就是為了找到最開始渲染該DOM的高階組件(防止某些情況下dom對應的_component屬性指代的實例被修改),然后再判斷該高階組件是否與當前的vnode類型一致。
第三段代碼: 如果存在當前虛擬dom對應的組件實例存在,則直接調(diào)用函數(shù)setComponentProps,相當于基于組件的實例進行修改渲染,然后組件實例中的base屬性即為最新的dom節(jié)點。
第四段代碼: 我們先不具體關(guān)心某個函數(shù)的具體實現(xiàn)細節(jié),只關(guān)注代碼邏輯。首先如果之前的dom節(jié)點對應存在組件,并且虛擬dom對應的組件類型與其不相同時,則卸載之前的組件(unmountComponent)。接著我們通過調(diào)用函數(shù)createComponent創(chuàng)建當前虛擬dom對應的組件實例,然后調(diào)用函數(shù)setComponentProps去創(chuàng)建組件實例的dom節(jié)點,最后如果當前的dom與之前的dom元素不相同時,將之前的dom回收(recollectNodeTree函數(shù)在diff的文章中已經(jīng)介紹)。
其實如果之前就閱讀過Preact的diff算法的同學來說,其實整個組件大致渲染的流程我們已經(jīng)清楚了,但是如果想要更深層次的了解其中的細節(jié)我們必須去深究函數(shù)createComponent與setComponentProps的內(nèi)部細節(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就可以實現(xiàn)回收一個組件以供以后重復利用。在函數(shù)collectComponent中通過組件名(component.constructor.name)分類將可重用的組件緩存在緩存池中。
函數(shù)createComponent主要作用就是創(chuàng)建組件實例。參數(shù)props與context分別對應的是組件的中屬性和context(與React一致),而Ctor組件則是需要創(chuàng)建的組件類型(函數(shù)或者是類)。我們知道如果我們的組件定義用ES6定義如下:
class App extends Component{}
我們知道class僅僅只是一個語法糖,上面的代碼使用ES5去實現(xiàn)相當于:
function App(){} App.prototype = Object.create(Component.prototype, { constructor: { value: App, enumerable: true, writable: true, configurable: true } });
如果你對ES5中的Object.create也不熟悉的話,我簡要的介紹一下,Object.create的作用就是實現(xiàn)原型繼承(Prototypal Inheritance)來實現(xiàn)基于已有對象創(chuàng)建新對象。Object.create的第一個參數(shù)就是所要繼承的原型對象,第二個參數(shù)就是新對象定義額外屬性的對象(類似于Object.defineProperty的參數(shù)),如果要我自己實現(xiàn)一個簡單的Object.create函數(shù)我們可以這樣寫:
function create(prototype, ...obj){ function F(){} F.prototype = prototype; return Object.defineProperties(new F(), ...obj); }
現(xiàn)在你肯定知道了如果你的組件繼承了Preact中的Component的話,在原型中一定存在render方法,這時候通過new創(chuàng)建Ctor的實例inst(實例中已經(jīng)含有了你自定義的render函數(shù)),但是如果沒有給父級構(gòu)造函數(shù)super傳入props和context,那么inst中的props和context的屬性為undefined,通過強制調(diào)用Component.call(inst, props, context)可以給inst中props、context進行初始化賦值。
如果組件中不存在render函數(shù),說明該函數(shù)是PFC(Pure Function Component)類型,即是純函數(shù)組件。這時直接調(diào)用函數(shù)Component創(chuàng)建實例,實例的constructor屬性設(shè)置為傳入的函數(shù)。由于實例中不存在render函數(shù),則將doRender函數(shù)作為實例的render屬性,doRender函數(shù)會將Ctor的返回的虛擬dom作為結(jié)果返回。
然后我們從組件回收的共享池中那拿到同類型組件的實例,從其中取出該實例之前渲染的實例(nextBase),然后將其賦值到我們的新創(chuàng)建組件實例的nextBase屬性上,其目的就是為了能基于此DOM元素進行渲染,以更少的代價進行相關(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è)置屬性(props),其中props通常來源于JSX中的屬性(attributes)。函數(shù)的參數(shù)component、props、context與mountAll的含義從名字就可以看出來,值得注意地是參數(shù)opts,代表的是不同的刷新模式:
NO_RENDER: 不進行渲染
SYNC_RENDER: 同步渲染
FORCE_RENDER: 強制刷新渲染
ASYNC_RENDER: 異步渲染
首先如果組件component中_disable屬性為true時則直接退出,否則將屬性_disable置為true,其目的相當于一個鎖,保證修改過程的原子性。如果傳入組件的屬性props中存在ref與key,則將其分別緩存在組件的__ref與__key,并將其從props將其刪除。
組件實例中的base中存放的是之前組件實例對應的真實dom節(jié)點,如果不存在該屬性,說明是該組件的初次渲染,如果組件中定義了生命周期函數(shù)(鉤子函數(shù))componentWillMount,則在此處執(zhí)行。如果不是首次執(zhí)行,如果存在生命周期函數(shù)componentWillReceiveProps,則需要將最新的props與context作為參數(shù)調(diào)用componentWillReceiveProps。然后分別將當前的屬性context與props緩存在組件的preContext與prevProps屬性中,并將context與props屬性更新為最新的context與props。最后將組件的_disable屬性置回false。
如果組件更新的模式為NO_RENDER,則不需要進行渲染。如果是同步渲染(SYNC_RENDER)或者是首次渲染(base屬性為空),則執(zhí)行函數(shù)renderComponent,其余情況下(例如setState觸發(fā)的異步渲染ASYNC_RENDER)均執(zhí)行函數(shù)enqueueRender(enqueueRender函數(shù)將在setState處分析)。在函數(shù)的最后,如果存在ref函數(shù),則將組件實例作為參數(shù)調(diào)用ref函數(shù)。在這里我們可以顯然可以看出在Preact中是不支持React的中字符串類型的ref屬性,不過這個也并不重要,因為React本身也不推薦使用字符串類型的ref屬性,并表示可能會在將來版本中廢除這一屬性。
接下來我們還需要了解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(); }
為了方便閱讀,我們將代碼分成了八個部分,不過為了更方便的閱讀代碼,我們首先看一下函數(shù)開始處的變量聲明:
所要渲染的component實例中的props、context、state屬性表示的是最新的所要渲染的組件實例屬性。而對應的preProps、preContext、preState代表的是渲染之前上一個狀態(tài)組件實例屬性。變量isUpdate代表的是當前是處于組件更新的過程還是組件渲染的過程(mount),我們通過之前組件實例是否對應存在真實DOM節(jié)點來判斷,如果存在則認為是更新的過程,否則認為是渲染(mount)過程。nextBase表示可以基于此DOM元素進行修改(可能來源于上一次渲染或者是回收之前同類型的組件實例),以尋求最小的渲染代價。
組件實例中的_component屬性表示的組件的子組件,僅僅只有當組件返回的是組件時(也就是當前組件為高階組件),才會存在。變量skip用來標志是否需要跳過更新的過程(例如: 生命周期函數(shù)shouldComponentUpdate返回false)。
第一段代碼: 如果存在component.base存在,說明該組件之前對應的真實dom元素,說明組件處于更新的過程。要將props、state、context替換成之前的previousProps、previousState、previousContext,這是因為在生命周期函數(shù)shouldComponentUpdate、componentWillUpdate中的this.props、this.state、this.context仍然是更新前的狀態(tài)。如果不是強制刷新(FORCE_RENDER)并存在生命周期函數(shù)shouldComponentUpdate,則以最新的props、state、context作為參數(shù)執(zhí)行shouldComponentUpdate,如果返回的結(jié)果為false表明要跳過此次的刷新過程,即置標志位skip為true。否則如果生命周期shouldComponentUpdate返回的不是false(說明如果不返回值或者其他非false的值,都會執(zhí)行更新),則查看生命周期函數(shù)componentWillUpdate是否存在,存在則執(zhí)行。最后則將組件實例的props、state、context替換成最新的狀態(tài),并置空組件實例中的prevProps、prevState、prevContext的屬性,以及將_dirty屬性置為false。需要注意的是只有_dirty為false才會被放入更新隊列,然后_dirty會被置為true,這樣組件實例就不會被多次放入更新隊列。
如果沒有跳過更新的過程(即skip為false),則執(zhí)行到第二段代碼。首先執(zhí)行組件實例的render函數(shù)(相比于React中的render函數(shù),Preact中的render函數(shù)執(zhí)行時傳入了參數(shù)props、state、context),執(zhí)行render函數(shù)的返回值rendered則是組件實例對應的虛擬dom元素(VNode)。如果組件存在函數(shù)getChildContext,則生成當前需要傳遞給子組件的context。我們從代碼extend(extend({}, context), component.getChildContext())可以看出,如果父組件存在某個context屬性并且當前組件實例中getChildContext函數(shù)返回的context也存在相同的屬性時,那么當前組件實例getChildContext返回的context中的屬性會覆蓋父組件的context中的相同屬性。
接下來到第三段代碼,childComponent是組件實例render函數(shù)返回的虛擬dom的類型(rendered.nodeName),如果childComponent的類型為函數(shù),說明該組件為高階組件(High Order Component),如果你不了解高階組件,可以戳這篇文章。如果是高階組件的情況下,首先通過getNodeProps函數(shù)獲得虛擬dom中子組件的屬性。如果組件存在子組件的實例并且子組件實例的構(gòu)造函數(shù)與當前組件返回的子組件虛擬dom類型相同(inst.constructor===childComponent)而且前后的key值相同時(childProps.key==inst.__key),僅需要以同步渲染(SYNC_RENDER)的模式遞歸調(diào)用函數(shù)setComponentProps來更新子組件的屬性props。之所以這樣是因為如果滿足前面的條件說明,前后兩次渲染的子組件對應的實例不發(fā)生改變,僅改變傳入子組件的參數(shù)(props)。這時子組件僅需要根據(jù)當前最新的props對應渲染真實dom即可。否則如果之前的子組件實例的構(gòu)造函數(shù)與當前組件返回的子組件虛擬dom類型不相同時或者根據(jù)key值標定兩個組件實例不相同時,則需要渲染的新的子組件,不僅需要調(diào)用createComponent創(chuàng)建子組件的實例(createComponent(childComponent, childProps, context))并為當前的子組件和組件設(shè)置相關(guān)關(guān)系(即_component、_parentComponent屬性)而且用toUnmount指示待卸載的組件實例。然后通過調(diào)用setComponentProps來設(shè)置組件的ref和key等,以及調(diào)用組件的相關(guān)生命周期函數(shù)(例如:componentWillMount),需要注意的是這里的調(diào)用模式是NO_RENDER,不會進行渲染。而在下一句調(diào)用renderComponent(inst, SYNC_RENDER, mountAll, true)去同步地渲染子組件。所以我們就要注意為什么在調(diào)用函數(shù)setComponentProps時沒有采用SYNC_RENDER模式,SYNC_RENDER模式也本身就會觸發(fā)renderComponent去渲染組件,其原因就是為了在調(diào)用renerComponent賦予isChild值為true,這個標志量的作用我們后面可以看到。調(diào)用完renderComponent之后,inst.base中已經(jīng)是我們子組件渲染的真實dom節(jié)點。
在第四段代碼中,處理的是當前組件需要渲染的虛擬dom類型是非組件類型(即普通的DOM元素)。首先賦值cbase = initialBase,我們知道initialBase來自于initialBase = isUpdate || nextBase,也就是說如果當前是更新的模式,則initialBase等于isUpdate,即為上次組件渲染的內(nèi)容。否則,如果組件實例存在nextBase(從回收池得到的DOM結(jié)構(gòu)),也可以基于其進行修改,總的目的是為了以更少的代價去渲染。如果之前的組件渲染的是函數(shù)類型的元素(即組件),但現(xiàn)在卻渲染的是非函數(shù)類型的,賦值toUnmount = initialChildComponent,用來存儲之后需要卸載的組件,并且由于cbase對應的是之前的組件的dom節(jié)點,因此就無法使用了,需要賦值cbase = null以使得重新渲染。而component._component = null目的就是切斷之前組件間的父子關(guān)系,畢竟現(xiàn)在返回的都不是組件。如果是同步渲染(SYNC_RENDER),則會通過調(diào)用idiff函數(shù)去渲染組件返回的虛擬dom(詳情見第二篇文章diff)。我們來看看調(diào)用idiff函數(shù)的形參和實參:
cbase對應的是diff的dom參數(shù),表示用來渲染的VNode之前的真實dom??梢钥吹饺绻笆墙M件類型,那么cbase值為undefined,我們就需要重新開始渲染。否則我們就可以在之前的渲染基礎(chǔ)上更新以尋求最小的更新代價。
rendered對應diff中的vnode參數(shù),表示需要渲染的虛擬dom節(jié)點。
context對應diff中的context參數(shù),表示組件的context屬性。
mountAll || !isUpdate對應的是diff中的mountAll參數(shù),表示是否是重新渲染DOM節(jié)點而不是基于之前的DOM修改,!isUpdate表示的就是非更新狀態(tài)。
initialBase && initialBase.parentNode對應的是diff中的parent參數(shù),表示的是當前渲染節(jié)點的父級節(jié)點。
diff函數(shù)的第六個參數(shù)為componentRoot,實參為true表示的是當前diff是以組件中render函數(shù)的渲染內(nèi)容的形式調(diào)用,也可以說當前的渲染內(nèi)容是屬于組件類型的。
我們知道idiff函數(shù)返回的是虛擬dom對應渲染后的真實dom節(jié)點,所以變量base存儲的就是本次組件渲染的真實DOM元素。
代碼第五部分: 如果組件前后返回的虛擬dom節(jié)點對應的真實DOM節(jié)點不相同,或者前后返回的虛擬DOM節(jié)點對應的前后組件實例不一致時,則在父級的DOM元素中將之前的DOM節(jié)點替換成當前對應渲染的DOM節(jié)點(baseParent.replaceChild(base, initialBase)),如果沒有需要卸載的組件實例,則調(diào)用函數(shù)recollectNodeTree回收該DOM節(jié)點。否則如果之前組件渲染的是函數(shù)類型的元素,但需要廢棄,則調(diào)用函數(shù)unmountComponent進行卸載(調(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ù)實例中的_disable置為true表示組件禁用,如果組件存在生命周期函數(shù)componentWillUnmount進行調(diào)用。然后遞歸調(diào)用函數(shù)unmountComponent遞歸卸載組件。如果之前組件渲染的DOM節(jié)點,并且最外層節(jié)點存在ref函數(shù),則以參數(shù)null執(zhí)行(和React保持一致,ref函數(shù)會執(zhí)行兩次,第一次是mount會以DOM元素或者組件實例回調(diào),第二次是unmount會回調(diào)null表示卸載)。然后將DOM元素存入nextBase用以回收。調(diào)用removeNode函數(shù)作用是將base節(jié)點的父節(jié)點脫離出來。函數(shù)removeChildren的目的是用遞歸遍歷所有的子DOM元素,回收節(jié)點(之前的文章已經(jīng)介紹過,其中就涉及到子元素的ref調(diào)用)。最后如果組件本身存在ref屬性,則直接以null為參數(shù)調(diào)用。
代碼第六部分:component.base = base用來將當前的組件渲染的dom元素存儲在組件實例的base屬性中。下面的代碼我們先舉個例子,假如有如下的結(jié)構(gòu):
HOC1 => HOC2 => component => DOM元素
其中HOC代表高階組件,component代表自定義組件。你會發(fā)現(xiàn)HOC1、HOC2與compoent的base屬性都指向最后的DOM元素,而DOM元素的中的_component是指向HOC1的組價實例的??炊诉@個你就能明白為什么會存在下面這個循環(huán)語句,其目的就是為了給父組件賦值正確的base屬性以及為DOM節(jié)點的_component屬性賦值正確的組件實例。
在第七段代碼中,如果是非更新模式,則需要將當前組件存入mounts(unshift方法存入,pop方法取出,實質(zhì)上是相當于隊列的方式,并且子組件先于父組件存儲隊列mounts,因此可以保證正確的調(diào)用順序),方便在后期調(diào)用組件對應類似于componentDidMount生命周期函數(shù)和其他的操作。如果沒有跳過更新過程(skip === false),則在此時調(diào)用組件對應的生命周期函數(shù)componentDidUpdate。然后如果存在組件存在_renderCallbacks屬性(存儲對應的setState的回調(diào)函數(shù),因為setState函數(shù)實質(zhì)也是通過renderComponent實現(xiàn)的),則在此處將其彈出并執(zhí)行。
在第八段代碼中,如果diffLevel為0并且isChild為false時,對應執(zhí)行flushMounts函數(shù)
function flushMounts() { let c; while ((c=mounts.pop())) { if (c.componentDidMount) c.componentDidMount(); } }
其實flushMounts也是非常的簡單,就是將隊列mounts中取出組件實例,然后如果存在生命周期函數(shù)componentDidMount,則對應執(zhí)行。
其實如果閱讀了之前diff的文章的同學應該記得在diff函數(shù)中有:
function diff(dom, vnode, context, mountAll, parent, componentRoot) { //...... if (!--diffLevel) { // ...... if (!componentRoot) flushMounts(); } }
上面有兩處調(diào)用函數(shù)flushMounts,一個是在renderComponent內(nèi)部①,一個是在diff函數(shù)②。那么在什么情況下觸發(fā)上下兩段代碼呢?首先componentRoot表示的是當前diff是不是以組件中渲染內(nèi)容的形式調(diào)用(比如組件中render函數(shù)返回HTML類型的VNode),那么preact.render函數(shù)調(diào)用時肯定componentRoot是false,diffLevel表示渲染的層次,diffLevel回減到0說明已經(jīng)要結(jié)束diff的調(diào)用,所以在使用preact.render渲染的最后肯定會使用上面的代碼去調(diào)用函數(shù)flushMounts。但是如果其中某個已經(jīng)渲染的組件通過setState或者forceUpdate的方式導致了重新渲染并且致使子組件創(chuàng)建了新的實例(比如前后兩次返回了不同的組件類型),這時,就會采用第一種方式在調(diào)用flushMounts函數(shù)。
setState對于Preact的組件而言,state是及其重要的部分。其中涉及到的API為setState,定義在函數(shù)Component的原型中,這樣所有的繼承于Component的自定義組件實例都可以引用到函數(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接受兩個參數(shù): 新的state以及state更新后的回調(diào)函數(shù),其中state既可以是對象類型的部分對象,也可以是函數(shù)類型。首先使用函數(shù)extend生成當前state的拷貝prevState,存儲之前的state的狀態(tài)。然后如果
state類型為函數(shù)時,將函數(shù)的生成值覆蓋進入state,否則直接將新的state覆蓋進入state,此時this.state已經(jīng)成為了新的state。如果setState存在第二個參數(shù)callback,則將其存入實例屬性_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;
我們可以看到當組件實例中的_dirty屬性為false時,會將屬性_dirty置為true,并將其放入items中。當更新隊列第一次被items時,則延遲異步執(zhí)行函數(shù)rerender。這個延遲異步函數(shù)在支持Promise的瀏覽器中,會使用Promise.resolve().then,否則會使用setTimeout。
rerender函數(shù)就是將items中待更新的組件,逐個取出,并對其執(zhí)行renderComponent。其實renderComponent的opt參數(shù)不傳入ASYNC_RENDER,而是傳入undefined兩者之間并無區(qū)別。唯一要注意的是:
//renderComponent內(nèi)部 if (initialBase || opts===SYNC_RENDER) { base = diff(//...; }
我們渲染過程一定是要執(zhí)行diff,那就說明initialBase一定是個非假值,這也是可以保證的。
initialBase = isUpdate || nextBase
其實因為之前組件已經(jīng)渲染過,所以是可以保證isUpdate一定為非假值,因為isUpdate = component.base并且component.base是一定存在的并且為上次渲染的內(nèi)容。大家可能會擔心如果上次組件render函數(shù)返回的是null該怎么辦?其實閱讀過第二篇文章的同學應該知道在idiff函數(shù)內(nèi)部
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); } //...... });
執(zhí)行forceUpdate所需要做的就是將回調(diào)函數(shù)放入組件實例中的_renderCallbacks屬性并調(diào)用函數(shù)renderComponent強制刷新當前的組件。需要注意的是,我們渲染的模式是FORCE_RENDER強制刷新,與其他的模式到的區(qū)別就是不需要經(jīng)過生命周期函數(shù)shouldComponentUpdate的判斷,直接進行刷新。
結(jié)語至此我們已經(jīng)看完了Preact中的組件相關(guān)的代碼,可能并沒有對每一個場景都進行講解,但是我也盡量嘗試去覆蓋所有相關(guān)的部分。代碼相對比較長,看起來也經(jīng)常令人頭疼,有時候為了搞清楚某個變量的部分不得不數(shù)次回顧。但是你會發(fā)現(xiàn)你多次地、反復性的閱讀、仔細地推敲,代碼的含義會逐漸清晰。書讀百遍其義自見,其實對代碼來說也是一樣的。文章若有不正確的地方,歡迎指出,共同學習。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/89337.html
摘要:本系列文章將重點分析類似于的這類框架是如何實現(xiàn)的,歡迎大家關(guān)注和討論。作為一個極度精簡的庫,函數(shù)是屬于本身的。 前言 首先歡迎大家關(guān)注我的掘金賬號和Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現(xiàn),能堅持下去也是靠的是自己的熱情和大家的鼓勵?! ≈胺窒磉^幾篇關(guān)于React的文章: React技術(shù)內(nèi)幕: key帶來了什么 React技術(shù)內(nèi)幕: setState的秘密...
摘要:新聞熱點國內(nèi)國外,前端最新動態(tài)就開源許可證風波進行回復數(shù)周前,基金會決定禁止旗下項目使用,因為其在標準的許可證之外添加了專利聲明此舉引發(fā)了社區(qū)的廣泛討論,希望能夠更新其開源許可證。 showImg(https://segmentfault.com/img/remote/1460000010777089); 前端每周清單第 27 期:React Patent License 回復,Sho...
摘要:是一個最小的庫,但由于其對尺寸的追求,它的很多代碼可讀性比較差,市面上也很少有全面且詳細介紹的文章,本篇文章希望能幫助你學習的源碼。建議與源碼一起閱讀本文。 作為一名前端,我們需要深入學習react的運行機制,但是react源碼量已經(jīng)相當龐大,從學習的角度,性價比不高,所以學習一個react mini庫是一個深入學習react的一個不錯的方法。 preact是一個最小的react mi...
摘要:另外第三方也可以通過的事件插件機制來合成自定義事件,盡管很少人這么做。抽象跨平臺事件機制。打算干預事件的分發(fā)。事件是的一個自定義事件,旨在規(guī)范化表單元素的變動事件。 showImg(https://segmentfault.com/img/remote/1460000019961124?w=713&h=307); 當我們在組件上設(shè)置事件處理器時,React并不會在該DOM元素上直接綁定...
摘要:前端日報精選裝飾器場景實戰(zhàn)配置之后端渲染理解同步異步和事件循環(huán)編寫高性能注意點線性漸變實現(xiàn)虛線等簡單實用圖形中文服務(wù)端渲染開發(fā)指南個人文章系列之事件類型個人文章使用必記掘金簡介掘金性能大亂斗前端雜談中簡單的數(shù)據(jù)圖形化 2017-10-27 前端日報 精選 JS 裝飾器(Decorator)場景實戰(zhàn)webpack配置之后端渲染JavaScript:理解同步、異步和事件循環(huán)編寫高性能js注...
閱讀 2478·2021-11-16 11:44
閱讀 2015·2021-10-12 10:12
閱讀 2306·2021-09-22 15:22
閱讀 3074·2021-08-11 11:17
閱讀 1573·2019-08-29 16:53
閱讀 2715·2019-08-29 14:09
閱讀 3535·2019-08-29 14:03
閱讀 3406·2019-08-29 11:09