在“Widget 簡(jiǎn)介”一節(jié),我們介紹了 Widget 和 Element 的關(guān)系,我們知道最終的 UI 樹其實(shí)是由一個(gè)個(gè)獨(dú)立的 Element 節(jié)點(diǎn)構(gòu)成。我們也說(shuō)過(guò)組件最終的 Layout、渲染都是通過(guò)RenderObject
來(lái)完成的,從創(chuàng)建到渲染的大體流程是:根據(jù) Widget 生成 Element,然后創(chuàng)建相應(yīng)的RenderObject
并關(guān)聯(lián)到Element.renderObject
屬性上,最后再通過(guò)RenderObject
來(lái)完成布局排列和繪制。
Element 就是 Widget 在 UI 樹具體位置的一個(gè)實(shí)例化對(duì)象,大多數(shù) Element 只有唯一的renderObject
,但還有一些 Element 會(huì)有多個(gè)子節(jié)點(diǎn),如繼承自RenderObjectElement
的一些類,比如MultiChildRenderObjectElement
。最終所有 Element 的 RenderObject 構(gòu)成一棵樹,我們稱之為”Render Tree“即”渲染樹“??偨Y(jié)一下,我們可以認(rèn)為 Flutter的UI 系統(tǒng)包含三棵樹:Widget 樹、Element 樹、渲染樹。他們的依賴關(guān)系是:Element 樹根據(jù) Widget 樹生成,而渲染樹又依賴于Element 樹,如圖14-0所示。
現(xiàn)在我們重點(diǎn)看一下 Element,Element 的生命周期如下:
Widget.createElement
創(chuàng)建一個(gè) Element 實(shí)例,記為element
element.mount(parentElement,newSlot)
,mount 方法中首先調(diào)用element
所對(duì)應(yīng) Widget 的createRenderObject
方法創(chuàng)建與element
相關(guān)聯(lián)的 RenderObject 對(duì)象,然后調(diào)用element.attachRenderObject
方法將element.renderObject
添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發(fā)生在 Element 樹結(jié)構(gòu)發(fā)生變化時(shí)才需要重新attach)。插入到渲染樹后的element
就處于“active”狀態(tài),處于“active”狀態(tài)后就可以顯示在屏幕上了(可以隱藏)。State.build
返回的 Widget 結(jié)構(gòu)與之前不同,此時(shí)就需要重新構(gòu)建對(duì)應(yīng)的 Element 樹。為了進(jìn)行 Element 復(fù)用,在 Element 重新構(gòu)建前會(huì)先嘗試是否可以復(fù)用舊樹上相同位置的 element,element 節(jié)點(diǎn)在更新前都會(huì)調(diào)用其對(duì)應(yīng) Widget 的canUpdate
方法,如果返回true
,則復(fù)用舊 Element,舊的 Element 會(huì)使用新 Widget 配置數(shù)據(jù)更新,反之則會(huì)創(chuàng)建一個(gè)新的 Element。Widget.canUpdate
主要是判斷newWidget
與oldWidget
的runtimeType
和key
是否同時(shí)相等,如果同時(shí)相等就返回true
,否則就會(huì)返回false
。根據(jù)這個(gè)原理,當(dāng)我們需要強(qiáng)制更新一個(gè) Widget 時(shí),可以通過(guò)指定不同的 Key 來(lái)避免復(fù)用。element
時(shí)(如 Widget 樹結(jié)構(gòu)發(fā)生了變化,導(dǎo)致element
對(duì)應(yīng)的 Widget 被移除),這時(shí)該祖先 Element 就會(huì)調(diào)用deactivateChild
方法來(lái)移除它,移除后element.renderObject
也會(huì)被從渲染樹中移除,然 后 Framework 會(huì)調(diào)用element.deactivate
方法,這時(shí)element
狀態(tài)變?yōu)椤癷nactive”狀態(tài)。unmount
方法將其徹底移除,這時(shí) element 的狀態(tài)為defunct
,它將永遠(yuǎn)不會(huì)再被插入到樹中。element
要重新插入到 Element 樹的其它位置,如element
或element
的祖先擁有一個(gè)GlobalKey(用于全局復(fù)用元素),那么 Framework 會(huì)先將 element 從現(xiàn)有位置移除,然后再調(diào)用其activate
方法,并將其renderObject
重新 attach 到渲染樹。看完 Element 的生命周期,可能有些讀者會(huì)有疑問(wèn),開發(fā)者會(huì)直接操作 Element 樹嗎?其實(shí)對(duì)于開發(fā)者來(lái)說(shuō),大多數(shù)情況下只需要關(guān)注 Widget 樹就行,F(xiàn)lutter 框架已經(jīng)將對(duì) Widget 樹的操作映射到了Element樹上,這可以極大的降低復(fù)雜度,提高開發(fā)效率。但是了解 Element 對(duì)理解整個(gè) Flutter UI 框架是至關(guān)重要的,F(xiàn)lutter 正是通過(guò) Element 這個(gè)紐帶將 Widget 和 RenderObject 關(guān)聯(lián)起來(lái),了解 Element 層不僅會(huì)幫助讀者對(duì) Flutter UI 框架有個(gè)清晰的認(rèn)識(shí),而且也會(huì)提高自己的抽象能力和設(shè)計(jì)能力。另外在有些時(shí)候,我們必須得直接使用 Element 對(duì)象來(lái)完成一些操作,比如獲取主題 Theme 數(shù)據(jù),具體細(xì)節(jié)將在下文介紹。
我們已經(jīng)知道,StatelessWidget
和StatefulWidget
的build
方法都會(huì)傳一個(gè)BuildContext
對(duì)象:
Widget build(BuildContext context) {}
我們也知道,在很多時(shí)候我們都需要使用這個(gè)context
做一些事,比如:
Theme.of(context) //獲取主題
Navigator.push(context, route) //入棧新路由
Localizations.of(context, type) //獲取Local
context.size //獲取上下文大小
context.findRenderObject() //查找當(dāng)前或最近的一個(gè)祖先RenderObject
那么BuildContext
到底是什么呢,查看其定義,發(fā)現(xiàn)其是一個(gè)抽象接口類:
abstract class BuildContext {
...
}
那這個(gè)context
對(duì)象對(duì)應(yīng)的實(shí)現(xiàn)類到底是誰(shuí)呢?我們順藤摸瓜,發(fā)現(xiàn)build
調(diào)用是發(fā)生在StatelessWidget
和StatefulWidget
對(duì)應(yīng)的StatelessElement
和StatefulElement
的build
方法中,以StatelessElement
為例:
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
發(fā)現(xiàn)build
傳遞的參數(shù)是this
,很明顯!這個(gè)BuildContext
就是StatelessElement
。同樣,我們同樣發(fā)現(xiàn)StatefulWidget
的context
是StatefulElement
。但StatelessElement
和StatefulElement
本身并沒(méi)有實(shí)現(xiàn)BuildContext
接口,繼續(xù)跟蹤代碼,發(fā)現(xiàn)它們間接繼承自Element
類,然后查看Element
類定義,發(fā)現(xiàn)Element
類果然實(shí)現(xiàn)了BuildContext
接口:
class Element extends DiagnosticableTree implements BuildContext {
...
}
至此真相大白,BuildContext
就是 widget 對(duì)應(yīng)的Element
,所以我們可以通過(guò)context
在StatelessWidget
和StatefulWidget
的build
方法中直接訪問(wèn)Element
對(duì)象。我們獲取主題數(shù)據(jù)的代碼Theme.of(context)
內(nèi)部正是調(diào)用了Element的dependOnInheritedWidgetOfExactType()
方法。
思考題:為什么 build 方法的參數(shù)不定義成 Element 對(duì)象,而要定義成 BuildContext ?
我們可以看到 Element 是 Flutter UI 框架內(nèi)部連接 widget 和RenderObject
的紐帶,大多數(shù)時(shí)候開發(fā)者只需要關(guān)注 widget 層即可,但是 widget 層有時(shí)候并不能完全屏蔽Element
細(xì)節(jié),所以 Framework 在StatelessWidget
和StatefulWidget
中通過(guò)build
方法參數(shù)又將Element
對(duì)象也傳遞給了開發(fā)者,這樣一來(lái),開發(fā)者便可以在需要時(shí)直接操作Element
對(duì)象。那么現(xiàn)在筆者提兩個(gè)問(wèn)題,請(qǐng)讀者先自己思考一下:
Element
層是否可以搭建起一個(gè)可用的 UI 框架?如果可以應(yīng)該是什么樣子?
對(duì)于問(wèn)題1,答案當(dāng)然是肯定的,因?yàn)槲覀冎罢f(shuō)過(guò) widget 樹只是Element
樹的映射,我們完全可以直接通過(guò) Element 來(lái)搭建一個(gè) UI 框架。下面舉一個(gè)例子:
我們通過(guò)純粹的 Element 來(lái)模擬一個(gè)StatefulWidget
的功能,假設(shè)有一個(gè)頁(yè)面,該頁(yè)面有一個(gè)按鈕,按鈕的文本是一個(gè)9位數(shù),點(diǎn)擊一次按鈕,則對(duì)9個(gè)數(shù)隨機(jī)排一次序,代碼如下:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
@override
Widget build() {
Color primary=Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: FlatButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //點(diǎn)擊后將該Element標(biāo)記為dirty,Element將會(huì)rebuild
},
),
),
);
}
}
build
方法不接收參數(shù),這一點(diǎn)和在StatelessWidget
和StatefulWidget
中build(BuildContext)
方法不同。代碼中需要用到BuildContext
的地方直接用this
代替即可,如代碼注釋1處Theme.of(this)
參數(shù)直接傳this
即可,因?yàn)楫?dāng)前對(duì)象本身就是Element
實(shí)例。text
發(fā)生改變時(shí),我們調(diào)用markNeedsBuild()
方法將當(dāng)前 Element 標(biāo)記為 dirty 即可,標(biāo)記為 dirty 的 Element 會(huì)在下一幀中重建。實(shí)際上,State.setState()
在內(nèi)部也是調(diào)用的markNeedsBuild()
方法。HomeView
一樣以Element
形式提供,那么就可以用純Element
來(lái)構(gòu)建UI了HomeView
的 build 方法返回值類型就可以是Element
了。
如果我們需要將上面代碼在現(xiàn)有 Flutter 框架中跑起來(lái),那么還是得提供一個(gè)“適配器”widget 將HomeView
結(jié)合到現(xiàn)有框架中,下面CustomHome
就相當(dāng)于“適配器”:
class CustomHome extends Widget {
@override
Element createElement() {
return HomeView(this);
}
}
現(xiàn)在就可以將CustomHome
添加到 widget 樹了,我們?cè)谝粋€(gè)新路由頁(yè)創(chuàng)建它,最終效果如下如圖14-1和14-2(點(diǎn)擊后)所示:
點(diǎn)擊按鈕則按鈕文本會(huì)隨機(jī)排序。
對(duì)于問(wèn)題2,答案當(dāng)然也是肯定的,F(xiàn)lutter engine 提供的 dart API 是原始且獨(dú)立的,這個(gè)與操作系統(tǒng)提供的 API 類似,上層 UI 框架設(shè)計(jì)成什么樣完全取決于設(shè)計(jì)者,完全可以將 UI 框架設(shè)計(jì)成 Android 風(fēng)格或 iOS 風(fēng)格,但這些事 Google 不會(huì)再去做,我們也沒(méi)必要再去搞這一套,這是因?yàn)轫憫?yīng)式的思想本身是很棒的,之所以提出這個(gè)問(wèn)題,是因?yàn)楣P者認(rèn)為做與不做是一回事,但知道能不能做是另一回事,這能反映出我們對(duì)知識(shí)的理解程度。
本節(jié)詳細(xì)的介紹了Element
的生命周期,以及它 Widget、BuildContext 的關(guān)系,也介紹了 Element 在 Flutter UI 系統(tǒng)中的角色和作用,我們將在下一節(jié)介紹 Flutter UI 系統(tǒng)中另一個(gè)重要的角色 RenderObject。
更多建議: