# Element與BuildContext
### Element
在“Widget簡介”一節,我們介紹了Widget和Element的關系,我們知道最終的UI樹其實是由一個個獨立的Element節點構成。我們也知道了組件最終的Layout、渲染都是通過RenderObject來完成的,從創建到渲染的大體流程是:根據Widget生成Element,然后創建相應的RenderObject并關聯到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。
Element就是Widget在UI樹具體位置的一個實例化對象,大多數Element只有唯一的renderObject,但還有一些Element會有多個子節點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有Element的RenderObject構成一棵樹,我們稱之為渲染樹,即render tree。
Element的生命周期如下:
1. Framework 調用`Widget.createElement` 創建一個Element實例,記為`element`
2. Framework 調用 `element.mount(parentElement,newSlot)` ,mount方法中首先調用`elment`所對應Widget的`createRenderObject`方法創建與`element`相關聯的RenderObject對象,然后調用`element.attachRenderObject`方法將`element.renderObject`添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發生在Element樹結構發生變化時才需要重新attach)。插入到渲染樹后的`element`就處于“active”狀態,處于“active”狀態后就可以顯示在屏幕上了(可以隱藏)。
3. 當`element`父Widget的配置數據改變時,為了進行Element復用,Framework在決定重新創建Element前會先嘗試復用相同位置舊的element:調用Element對應Widget的`canUpdate()`方法,如果返回`true`,則復用舊Element,舊的Element會使用新的Widget配置數據更新,反之則會創建一個新的Element,不會復用。`Widget.canUpdate()`主要是判斷`newWidget`與`oldWidget`的`runtimeType`和`key`是否同時相等,如果同時相等就返回`true`,否則就會返回`false`。根據這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來禁止復用。
4. 當有父Widget的配置數據改變時,同時其`State.build`返回的Widget結構與之前不同,此時就需要重新構建對應的Element樹。為了進行Element復用,在Element重新構建前會先嘗試是否可以復用舊樹上相同位置的element,element節點在更新前都會調用其對應Widget的`canUpdate`方法,如果返回`true`,則復用舊Element,舊的Element會使用新Widget配置數據更新,反之則會創建一個新的Element。`Widget.canUpdate`主要是判斷`newWidget`與`oldWidget`的`runtimeType`和`key`是否同時相等,如果同時相等就返回`true`,否則就會返回`false`。根據這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。
5. 當有祖先Element決定要移除`element` 時(如Widget樹結構發生了變化,導致`element`對應的Widget被移除),這時該祖先Element就會調用`deactivateChild` 方法來移除它,移除后`element.renderObject`也會被從渲染樹中移除,然后Framework會調用`element.deactivate` 方法,這時`element`狀態變為“inactive”狀態。
6. “inactive”態的element將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定element,“inactive”態的element在當前動畫最后一幀結束前都會保留,如果在動畫執行結束后它還未能重新變成”active“狀態,Framework就會調用其`unmount`方法將其徹底移除,這時element的狀態為`defunct`,它將永遠不會再被插入到樹中。
7. 如果`element`要重新插入到Element樹的其它位置,如`element`或`element`的祖先擁有一個GlobalKey(用于全局復用元素),那么Framework會先將element從現有位置移除,然后再調用其`activate`方法,并將其`renderObject`重新attach到渲染樹。
看完Element的生命周期,可能有些讀者會有疑問,開發者會直接操作Element樹嗎?其實對于開發者來說,大多數情況下只需要關注Widget樹就行,Flutter框架已經將對Widget樹的操作映射到了Element樹上,這可以極大的降低復雜度,提高開發效率。但是了解Element對理解整個Flutter UI框架是至關重要的,Flutter正是通過Element這個紐帶將Widget和RenderObject關聯起來,了解Element層不僅會幫助讀者對Flutter UI框架有個清晰的認識,而且也會提高自己的抽象能力和設計能力。另外在有些時候,我們必須得直接使用Element對象來完成一些操作,比如獲取主題Theme數據,具體細節將在下文介紹。
### BuildContext
無論是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到底是什么呢,查看其定義,發現其是一個抽象接口類:
```
abstract class BuildContext {
...
}
```
那StatelessWidget和StatefulWidget的build方法傳入的context對象是哪個實現了BuildContext的類。我們順藤摸瓜,發現調用時發生在StatelessWidget和StatefulWidget對應的StatelessElement和StatefulElement的build方法中,以StatelessElement為例:
```
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
```
發現build傳遞的是this,很明顯了,這個BuildContext很可能就是Element類,查看Element類定義,發現Element類果然實現了BuildContext接口:
```
class Element extends DiagnosticableTree implements BuildContext {
...
}
```
至此真相大白,BuildContext就是Widget對應的Element,所以我們可以通過context在StatelessWidget和StatefulWidget的build方法中直接訪問Element對象。我們獲取主題數據的代碼`Theme.of(context)`內部正是調用了Element的`inheritFromWidgetOfExactType()`方法。
> 思考題:為什么build方法的參數不定義成Element對象,而要定義成BuildContext ?
### 進階
我們可以看到Element是Flutter UI框架內部連接Widget和RenderObject的紐帶,大多數時候開發者只需要關注Widget層即可,但是Widget層有時候并不能完全屏蔽Element細節,所以Framework在StatelessWidget和StatefulWidget中通過build方法參數將Element對象也傳遞給了開發者,這樣便可以在需要時直接操作Element對象。那么現在筆者提兩個問題,請讀者先自己思考一下:
1. 如果沒有Widget層,單靠Element層是否可以搭建起一個可用的UI框架?如果可以應該是什么樣子?
2. Flutter UI框架能不做成響應式嗎?
對于問題1,答案當然是肯定的,因為我們之前說過Widget樹只是Element樹的映射,我們完全可以直接通過Element來搭建一個UI框架。下面舉一個例子:
我們通過純粹的Element來模擬一個StatefulWidget的功能,假設有一個頁面,該頁面有一個按鈕,按鈕的文本是1-9 9個數,點擊一次按鈕,則對9個數隨機排一次序,代碼如下:
```
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方法不接收參數,這一點和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代碼中需要用到BuildContext的地方直接用`this`代替即可,如代碼注釋1處`Theme.of(this)`參數直接傳`this`即可,因為當前對象本身就是Element實例。
- 當`text`發生改變時,我們調用`markNeedsBuild()`方法將當前Element標記為dirty即可,標記為dirty的Element會在下一幀中重建。實際上,`State.setState()`在內部也是調用的`markNeedsBuild()`方法。
- 上面代碼中build方法返回的仍然是一個Widget,這是由于Flutter框架中已經有了Widget這一層,并且組件庫都已經是以Widget的形式提供了,如果在Flutter框架中所有組件都像示例的HomeView一樣以Element形式提供,那么就可以用純Element來構建UI了,HomeView的build方法返回值類型就可以是Element了。
如果我們需要將上面代碼在現有Flutter框架中跑起來,那么還是得提供一個”適配器“Widget將HomeView結合到現有框架中,下面CustomHome就相當于”適配器“:
```
class CustomHome extends Widget {
@override
Element createElement() {
return HomeView(this);
}
}
```
現在就可以將CustomHome添加到Widget樹了,我們在一個新路由頁創建它,最終效果如下:

點擊按鈕則按鈕文本會隨機排序。
對于問題2,答案當然也是肯定的,Flutter engine提供的dart API是原始且獨立的,這個與操作系統提供的API類似,上層UI框架設計成什么樣完全取決于設計者,完全可以將UI框架設計成Android風格或iOS風格,但這些事Google不會再去做,我們也沒必要再去搞這一套,這是因為響應式的思想本身是很棒的,之所以提出這個問題,是因為筆者認為做與不做是一回事,但知道能不能做是另一回事,這能反映出我們對知識的掌握程度。
### 總結
本節詳細的介紹了Element的生命周期,以及它與Widget、BuildContext的關系,也介紹了Element在Flutter UI系統中的角色和作用,我們將在下一節介紹Flutter UI系統中另一個重要的角色RenderObject。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示