首先歡迎你們關注個人掘金帳號和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
,使得_dirty
爲true
,但由於該屬性的存在,只會使得組件僅有一次纔會被放入更新隊列。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
的邏輯並不複雜,將vnode
的attributes
和chidlren
的屬性賦值到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類型一致。
setComponentProps
,至關於基於組件的實例進行修改渲染,而後組件實例中的base
屬性即爲最新的dom
節點。unmountComponent
)。接着咱們經過調用函數createComponent
建立當前虛擬dom對應的組件實例,而後調用函數setComponentProps
去建立組件實例的dom
節點,最後若是當前的dom
與以前的dom
元素不相同時,將以前的dom
回收(recollectNodeTree
函數在diff的文章中已經介紹)。 其實若是以前就閱讀過Preact的diff算法的同窗來講,其實整個組件大體渲染的流程咱們已經清楚了,可是若是想要更深層次的瞭解其中的細節咱們必須去深究函數createComponent
與setComponentProps
的內部細節。
關於函數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
主要做用就是建立組件實例。參數props
與context
分別對應的是組件的中屬性和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
傳入props
和context
,那麼inst
中的props
和context
的屬性爲undefined
,經過強制調用Component.call(inst, props, context)
能夠給inst
中props
、context
進行初始化賦值。
若是組件中不存在render
函數,說明該函數是PFC(Pure Function Component)類型,便是純函數組件。這時直接調用函數Component
建立實例,實例的constructor
屬性設置爲傳入的函數。因爲實例中不存在render
函數,則將doRender
函數做爲實例的render
屬性,doRender
函數會將Ctor
的返回的虛擬dom做爲結果返回。
而後咱們從組件回收的共享池中那拿到同類型組件的實例,從其中取出該實例以前渲染的實例(nextBase
),而後將其賦值到咱們的新建立組件實例的nextBase
屬性上,其目的就是爲了能基於此DOM元素進行渲染,以更少的代價進行相關的渲染。
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
)。函數的參數component
、props
、context
與mountAll
的含義從名字就能夠看出來,值得注意地是參數opts
,表明的是不一樣的刷新模式:
NO_RENDER
: 不進行渲染SYNC_RENDER
: 同步渲染FORCE_RENDER
: 強制刷新渲染ASYNC_RENDER
: 異步渲染 首先若是組件component
中_disable
屬性爲true
時則直接退出,不然將屬性_disable
置爲true
,其目的至關於一個鎖,保證修改過程的原子性。若是傳入組件的屬性props
中存在ref
與key
,則將其分別緩存在組件的__ref
與__key
,並將其從props
將其刪除。
組件實例中的base
中存放的是以前組件實例對應的真實dom節點,若是不存在該屬性,說明是該組件的初次渲染,若是組件中定義了生命週期函數(鉤子函數)componentWillMount
,則在此處執行。若是不是首次執行,若是存在生命週期函數componentWillReceiveProps
,則須要將最新的props
與context
做爲參數調用componentWillReceiveProps
。而後分別將當前的屬性context
與props
緩存在組件的preContext
與prevProps
屬性中,並將context
與props
屬性更新爲最新的context
與props
。最後將組件的_disable
屬性置回false
。
若是組件更新的模式爲NO_RENDER
,則不須要進行渲染。若是是同步渲染(SYNC_RENDER
)或者是首次渲染(base
屬性爲空),則執行函數renderComponent
,其他狀況下(例如setState
觸發的異步渲染ASYNC_RENDER
)均執行函數enqueueRender
(enqueueRender
函數將在setState
處分析)。在函數的最後,若是存在ref
函數,則將組件實例做爲參數調用ref
函數。在這裏咱們能夠顯然能夠看出在Preact中是不支持React的中字符串類型的ref
屬性,不過這個也並不重要,由於React自己也不推薦使用字符串類型的ref
屬性,並表示可能會在未來版本中廢除這一屬性。
接下來咱們還須要瞭解renderComponent
函數(很是冗長)與enqueueRender
函數的做用:
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
實例中的props
、context
、state
屬性表示的是最新的所要渲染的組件實例屬性。而對應的preProps
、preContext
、preState
表明的是渲染以前上一個狀態組件實例屬性。變量isUpdate
表明的是當前是處於組件更新的過程仍是組件渲染的過程(mount),咱們經過以前組件實例是否對應存在真實DOM節點來判斷,若是存在則認爲是更新的過程,不然認爲是渲染(mount)過程。nextBase
表示能夠基於此DOM元素進行修改(可能來源於上一次渲染或者是回收以前同類型的組件實例),以尋求最小的渲染代價。
組件實例中的_component
屬性表示的組件的子組件,僅僅只有當組件返回的是組件時(也就是當前組件爲高階組件),纔會存在。變量skip
用來標誌是否須要跳過更新的過程(例如: 生命週期函數shouldComponentUpdate
返回false
)。
component.base
存在,說明該組件以前對應的真實dom元素,說明組件處於更新的過程。要將props
、state
、context
替換成以前的previousProps
、previousState
、previousContext
,這是由於在生命週期函數shouldComponentUpdate
、componentWillUpdate
中的this.props
、this.state
、this.context
仍然是更新前的狀態。若是不是強制刷新(FORCE_RENDER
)並存在生命週期函數shouldComponentUpdate
,則以最新的props
、state
、context
做爲參數執行shouldComponentUpdate
,若是返回的結果爲false
代表要跳過這次的刷新過程,即置標誌位skip
爲true。不然若是生命週期shouldComponentUpdate
返回的不是false
(說明若是不返回值或者其餘非false
的值,都會執行更新),則查看生命週期函數componentWillUpdate
是否存在,存在則執行。最後則將組件實例的props
、state
、context
替換成最新的狀態,並置空組件實例中的prevProps
、prevState
、prevContext
的屬性,以及將_dirty
屬性置爲false
。須要注意的是隻有_dirty
爲false
纔會被放入更新隊列,而後_dirty
會被置爲true
,這樣組件實例就不會被屢次放入更新隊列。skip
爲false
),則執行到第二段代碼。首先執行組件實例的render
函數(相比於React中的render
函數,Preact中的render
函數執行時傳入了參數props
、state
、context
),執行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
來設置組件的ref
和key
等,以及調用組件的相關生命週期函數(例如:componentWillMount
),須要注意的是這裏的調用模式是NO_RENDER
,不會進行渲染。而在下一句調用renderComponent(inst, SYNC_RENDER, mountAll, true)
去同步地渲染子組件。因此咱們就要注意爲何在調用函數setComponentProps
時沒有采用SYNC_RENDER
模式,SYNC_RENDER
模式也自己就會觸發renderComponent
去渲染組件,其緣由就是爲了在調用renerComponent
賦予isChild
值爲true
,這個標誌量的做用咱們後面能夠看到。調用完renderComponent
以後,inst.base
中已是咱們子組件渲染的真實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
函數的形參和實參:cbase
對應的是diff
的dom
參數,表示用來渲染的VNode以前的真實dom。能夠看到若是以前是組件類型,那麼cbase
值爲undefined
,咱們就須要從新開始渲染。不然咱們就能夠在以前的渲染基礎上更新以尋求最小的更新代價。rendered
對應diff
中的vnode
參數,表示須要渲染的虛擬dom節點。context
對應diff
中的context
參數,表示組件的context
屬性。mountAll || !isUpdate
對應的是diff
中的mountAll
參數,表示是不是從新渲染DOM節點而不是基於以前的DOM修改,!isUpdate
表示的就是非更新狀態。initialBase && initialBase.parentNode
對應的是diff
中的parent
參數,表示的是當前渲染節點的父級節點。diff
函數的第六個參數爲componentRoot
,實參爲true
表示的是當前diff
是以組件中render
函數的渲染內容的形式調用,也能夠說當前的渲染內容是屬於組件類型的。 咱們知道idiff
函數返回的是虛擬dom對應渲染後的真實dom節點,因此變量base
存儲的就是本次組件渲染的真實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
表明自定義組件。你會發現HOC1
、HOC2
與compoent
的base
屬性都指向最後的DOM元素,而DOM元素的中的_component
是指向HOC1
的組價實例的。看懂了這個你就能明白爲何會存在下面這個循環語句,其目的就是爲了給父組件賦值正確的base
屬性以及爲DOM節點的_component
屬性賦值正確的組件實例。
mounts
(unshift
方法存入,pop
方法取出,實質上是至關於隊列的方式,而且子組件先於父組件存儲隊列mounts
,所以能夠保證正確的調用順序),方便在後期調用組件對應相似於componentDidMount
生命週期函數和其餘的操做。若是沒有跳過更新過程(skip === false
),則在此時調用組件對應的生命週期函數componentDidUpdate
。而後若是存在組件存在_renderCallbacks
屬性(存儲對應的setState
的回調函數,由於setState
函數實質也是經過renderComponent
實現的),則在此處將其彈出並執行。diffLevel
爲0
而且isChild
爲false
時,對應執行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
函數調用時確定componentRoot
是false
,diffLevel
表示渲染的層次,diffLevel
回減到0說明已經要結束diff的調用,因此在使用preact.render
渲染的最後確定會使用上面的代碼去調用函數flushMounts
。可是若是其中某個已經渲染的組件經過setState
或者forceUpdate
的方式致使了從新渲染而且導致子組件建立了新的實例(好比先後兩次返回了不一樣的組件類型),這時,就會採用第一種方式在調用flushMounts
函數。
對於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
函數:
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
。其實renderComponent
的opt
參數不傳入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
類型。
extend(Component.prototype,{ //....... forceUpdate(callback) { if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback); renderComponent(this, FORCE_RENDER); } //...... });
執行forceUpdate
所須要作的就是將回調函數放入組件實例中的_renderCallbacks
屬性並調用函數renderComponent
強制刷新當前的組件。須要注意的是,咱們渲染的模式是FORCE_RENDER
強制刷新,與其餘的模式到的區別就是不須要通過生命週期函數shouldComponentUpdate
的判斷,直接進行刷新。
至此咱們已經看完了Preact中的組件相關的代碼,可能並無對每個場景都進行講解,可是我也儘可能嘗試去覆蓋全部相關的部分。代碼相對比較長,看起來也常常使人頭疼,有時候爲了搞清楚某個變量的部分不得不數次回顧。可是你會發現你屢次地、反覆性的閱讀、仔細地推敲,代碼的含義會逐漸清晰。書讀百遍其義自見,其實對代碼來講也是同樣的。文章如有不正確的地方,歡迎指出,共同窗習。