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