Flutter核心原理Element與BuildContext

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的生命周期如下:

  1. Framework 調(diào)用Widget.createElement 創(chuàng)建一個(gè)Element實(shí)例,記為element
  2. 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)后就可以顯示在屏幕上了(可以隱藏)。
  3. 當(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主要是判斷newWidgetoldWidgetruntimeTypekey是否同時(shí)相等,如果同時(shí)相等就返回true,否則就會(huì)返回false。根據(jù)這個(gè)原理,當(dāng)我們需要強(qiáng)制更新一個(gè)Widget時(shí),可以通過指定不同的Key來避免復(fù)用。
  4. 當(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)。
  5. “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ì)再被插入到樹中。
  6. 如果element要重新插入到Element樹的其它位置,如elementelement的祖先擁有一個(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)知道,StatelessWidgetStatefulWidgetbuild方法都會(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ā)生在StatelessWidgetStatefulWidget對(duì)應(yīng)的StatelessElementStatefulElementbuild方法中,以StatelessElement為例:

class StatelessElement extends ComponentElement {
  ...
  @override
  Widget build() => widget.build(this);
  ...
}

發(fā)現(xiàn)build傳遞的參數(shù)是this,很明顯!這個(gè)BuildContext就是StatelessElement。同樣,我們同樣發(fā)現(xiàn)StatefulWidgetcontextStatefulElement。但StatelessElementStatefulElement本身并沒有實(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,所以我們可以通過contextStatelessWidgetStatefulWidgetbuild方法中直接訪問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在StatelessWidgetStatefulWidget中通過build方法參數(shù)又將Element對(duì)象也傳遞給了開發(fā)者,這樣一來,開發(fā)者便可以在需要時(shí)直接操作Element對(duì)象。那么現(xiàn)在筆者提兩個(gè)問題,請(qǐng)讀者先自己思考一下:

  1. 如果沒有widget層,單靠Element層是否可以搭建起一個(gè)可用的UI框架?如果可以應(yīng)該是什么樣子?
  2. 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)和在StatelessWidgetStatefulWidgetbuild(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í)的理解程度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容