Element
我們知道最終的UI樹其實(shí)是由一個(gè)個(gè)獨(dú)立的Element節(jié)點(diǎn)構(gòu)成。最終的Layout、渲染都是通過RenderObject來完成的,從創(chuàng)建到渲染的大體流程是:根據(jù)Widget生成Element,然后創(chuàng)建相應(yīng)的RenderObject并關(guān)聯(lián)到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。
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樹。

現(xiàn)在我們重點(diǎn)看一下Element,Element的生命周期如下:
- Framework 調(diào)用
Widget.createElement創(chuàng)建一個(gè)Element實(shí)例,記為element - Framework 調(diào)用
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)后就可以顯示在屏幕上了(可以隱藏)。 - 當(dāng)有父Widget的配置數(shù)據(jù)改變時(shí),同時(shí)其
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í),可以通過指定不同的Key來避免復(fù)用。 - 當(dāng)有祖先Element決定要移除
element時(shí)(如Widget樹結(jié)構(gòu)發(fā)生了變化,導(dǎo)致element對(duì)應(yīng)的Widget被移除),這時(shí)該祖先Element就會(huì)調(diào)用deactivateChild方法來移除它,移除后element.renderObject也會(huì)被從渲染樹中移除,然后Framework會(huì)調(diào)用element.deactivate方法,這時(shí)element狀態(tài)變?yōu)椤癷nactive”狀態(tài)。 - “inactive”態(tài)的element將不會(huì)再顯示到屏幕。為了避免在一次動(dòng)畫執(zhí)行過程中反復(fù)創(chuàng)建、移除某個(gè)特定element,“inactive”態(tài)的element在當(dāng)前動(dòng)畫最后一幀結(jié)束前都會(huì)保留,如果在動(dòng)畫執(zhí)行結(jié)束后它還未能重新變成“active”狀態(tài),F(xiàn)ramework就會(huì)調(diào)用其
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ì)有疑問,開發(fā)者會(huì)直接操作Element樹嗎?其實(shí)對(duì)于開發(fā)者來說,大多數(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正是通過Element這個(gè)紐帶將Widget和RenderObject關(guān)聯(lián)起來,了解Element層不僅會(huì)幫助讀者對(duì)Flutter UI框架有個(gè)清晰的認(rèn)識(shí),而且也會(huì)提高自己的抽象能力和設(shè)計(jì)能力。另外在有些時(shí)候,我們必須得直接使用Element對(duì)象來完成一些操作,比如獲取主題Theme數(shù)據(jù),具體細(xì)節(jié)將在下文介紹。
BuildContext
我們已經(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è)祖先RenderObjec
那么BuildContext到底是什么呢,查看其定義,發(fā)現(xiàn)其是一個(gè)抽象接口類:
abstract class BuildContext {
...
}
那這個(gè)context對(duì)象對(duì)應(yīng)的實(shí)現(xiàn)類到底是誰呢?我們順藤摸瓜,發(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本身并沒有實(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,所以我們可以通過context在StatelessWidget和StatefulWidget的build方法中直接訪問Element對(duì)象。我們獲取主題數(shù)據(jù)的代碼Theme.of(context)內(nèi)部正是調(diào)用了Element的inheritFromWidgetOfExactType()方法。
進(jìn)階
我們可以看到Element是Flutter UI框架內(nèi)部連接widget和RenderObject的紐帶,大多數(shù)時(shí)候開發(fā)者只需要關(guān)注widget層即可,但是widget層有時(shí)候并不能完全屏蔽Element細(xì)節(jié),所以Framework在StatelessWidget和StatefulWidget中通過build方法參數(shù)又將Element對(duì)象也傳遞給了開發(fā)者,這樣一來,開發(fā)者便可以在需要時(shí)直接操作Element對(duì)象。那么現(xiàn)在筆者提兩個(gè)問題,請(qǐng)讀者先自己思考一下:
- 如果沒有widget層,單靠
Element層是否可以搭建起一個(gè)可用的UI框架?如果可以應(yīng)該是什么樣子? - Flutter UI框架能不做成響應(yīng)式嗎?
對(duì)于問題1,答案當(dāng)然是肯定的,因?yàn)槲覀冎罢f過widget樹只是Element樹的映射,我們完全可以直接通過Element來搭建一個(gè)UI框架。下面舉一個(gè)例子:
我們通過純粹的Element來模擬一個(gè)StatefulWidget的功能,假設(shè)有一個(gè)頁面,該頁面有一個(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í)例。當(dāng)
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()方法。上面代碼中build方法返回的仍然是一個(gè)widget,這是由于Flutter框架中已經(jīng)有了widget這一層,并且組件庫都已經(jīng)是以widget的形式提供了,如果在Flutter框架中所有組件都像示例的
HomeView一樣以Element形式提供,那么就可以用純Element來構(gòu)建UI了HomeView的build方法返回值類型就可以是Element了。
如果我們需要將上面代碼在現(xiàn)有Flutter框架中跑起來,那么還是得提供一個(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è)新路由頁創(chuàng)建它,最終效果如下如圖所示:


對(duì)于問題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ì)再去做,我們也沒必要再去搞這一套,這是因?yàn)轫憫?yīng)式的思想本身是很棒的,之所以提出這個(gè)問題,是因?yàn)楣P者認(rèn)為做與不做是一回事,但知道能不能做是另一回事,這能反映出我們對(duì)知識(shí)的理解程度。