這是我參與8月更文挑戰的第7天,活動詳情查看:8月更文挑戰微信
上一篇咱們對比了 setState
和 ModelBinding
這兩種狀態管理的區別,從結果來看,setState
的方式的性能明顯低於 ModelBinding
這種使用 InheritedWidget
的方式。這是由於 setState
的時候,無論子組件有沒有依賴狀態數據,都會蔣所有子組件移除後重建。那麼 setState
這個過程作了什麼事情,會致使這樣的結果呢?本篇咱們經過 Flutter 的源碼來分析一下 setState
的過程。markdown
咱們先來看 setState 的定義,setState 定義在State<T extends StatefulWidget> with Diagnosticable
這個類中,也就是 StatefulWidget
或其子類的狀態類。方法體代碼很少,在執行業務代碼作了一些異常處理,具體的代碼咱們不貼了,主要是作了以下處理:app
setState
的回調方法不能爲空。dispose
掉,所以不能在 dispose
後調用 setState
。一般這會發生在定時器、動畫或異步回調的過程當中。這樣的調用可能會致使內存泄露。created
階段和沒有裝載階段(mounted
)不能夠調用 setState
,也就是不能在構造函數裏調用 setState
。一般應該在 initState
以後調用 setState
。setState
中執行異步操做,只能是同步操做。若是要執行異步操做應該咋 setState
以外進行調用。@protected
void setState(VoidCallback fn) {
// 省略異常處理代碼
_element!.markNeedsBuild();
}
複製代碼
最爲關鍵的就一行代碼:_element!.markNeedsBuild()
,從函數名稱來看就是標記元素須要構建。那麼這個_element
又是從哪來的?繼續挖!異步
咱們來看_element
的定義,_element
是一個 StatefulElement
對象,實際上,咱們還發現,在獲取BuildContext
的時候,返回的也是_element
。在獲取 BuildContext 的時候註釋是這麼說的:ide
The location in the tree where this widget builds ——widget構建的渲染樹的具體位置。函數
BuildContext
是一個抽象類,所以能夠推斷出 StatefulElement
其實是其接口實現類或子類。往上溯源,發現整個的類層級是下面這樣的,其中 Element
、ComponentElement
都是抽象類,而 markNeedsBuild
方法是在 Element
抽象類定義的。而對於 Element,官方的定義爲:工具
An instantiation of a Widget at a particular location in the tree. —— 在渲染樹中的 Widget 實例化對象。post
能夠理解爲Element
是將 Widget
配置和渲染樹作橋接的對象,也就是實際的渲染過程更多的是由 Element
來控制的。性能
classDiagram BuildContext <|.. Element DiagnosticableTree <|-- Element Element <|-- ComponentElement ComponentElement <|-- StatefulElement class Element { Element(Widget widget) +_sort(Element a, Element b) -reassemble() -markNeedsBuild() -get renderObject -updateChild(Element? child, Widget? newWidget, dynamic newSlot) -mount(Element? parent, dynamic newSlot) -unmount() -update(covariant Widget newWidget) -detachRenderObject() -attachRenderObject(dynamic newSlot) -deactivateChild(Element child) -activate() -didChangeDependencies() -markNeedsBuild() -rebuild() -performRebuild() -Element? _parent -int _depth -Widget _widget -BuildOwner? _owner _ElementLifecycle _lifecycleState }
上面的圖咱們Element的關鍵屬性和方法列出來的。動畫
_depth
屬性:元素在組件樹中的層級,根節點的該值必須大於0。
_sort
方法:比較兩個Element
元素a和 b的層級,層級值(_depth
)越大,層級越深,顯示的層也就越靠前。
_parent
:父節點元素,可能爲空。
_widget
:配置元素的組件配置(實際上是 Widget
對象,Widget
自己是渲染元素的配置參數,並非真正渲染的元素)。
_owner
:管理元素聲明週期的對象。
_lifecycleState
:生命週期狀態屬性,默認是 initial
狀態。
獲取renderObject
的 get
方法:會遞歸調用返回元素及其子元素中須要渲染的對象(子元素是 RenderObjectElement
對象)。
reassemble
方法:從新裝配方法,只在 debug
階段會用到,例如熱重載的時候就會調用該方法。該方法處理將元素自身標記爲須要build
外(調用 markNeedsBuild
方法),還會遞歸遍歷所有子節點,調用子節點的 reassemble
方法。
updateChild
:這是渲染過程的核心方法,經過新的組件配置來更新指定的子元素。這裏存在四種組合:- 若是 child
爲空的話而 newWidget
不爲空,那麼就會建立一個新的元素來渲染:
child
不爲空,可是 newWidget
爲空,那就代表組件配置中已經沒有 child
這個元素了,所以須要移除它。child
的當前是否能夠更新(Widget.canUpdate
)來處理,若是能夠更新,那麼使用新的組件配置更新元素;不然咱們須要移除舊的元素,並使用新的組件配置建立一個新的元素。返回的結果也分三種狀況:
1. 若是建立了一個新的元素,則返回新構建的子元素。
2. 若是舊的元素被更新,返回更新後的子元素。
3. 若是子元素被移除,而沒有新的替換的話,返回null。
複製代碼
mount
方法:在新元素首次被建立的時候調用該方法,按照給定的插入位置(slot)將元素插入給定的父節點。調用該方法後,元素的狀態會從 initial
改成 active
。這裏還會將子元素的層級(_depth)設置爲父元素的層級+1。update
方法:當父節點使用新的配置組件(newWidget
)更改元素時,會調用該方法。要求新的配置類型和舊的保持一致。detachRenderObject
和 attachRenderObject
:分別對應從組件樹移除renderObject 和添加 RenderObject。deactivateChild
方法:將子元素加入到不活躍的元素列表,以後再從渲染樹中移除。activate
方法:狀態從inactive 切換到 active 時會調用,屬於生命週期函數。注意組件第一次掛載的時候不會調用這個方法,而是 mount 方法。deactivate
方法:狀態從 active 切換到 inactive 時會被調用,也就是元素被移入到不活躍列表的時候會被調用。。unmount
方法:狀態從 inactive 切換到defunct(再也不存在)狀態時調用,此時元素將脫離渲染樹,而且不再會在渲染樹存在。didChangeDependencies
:當元素的依賴發生改變的時候調用,該方法也會調用 markNeedBuild
方法。markNeedsBuild
方法:將元素標記爲 dirty
狀態,以便在渲染下一幀時重建元素。這個方法的核心是作了下面的事情:_dirty = true;
owner!.scheduleBuildFor(this)
複製代碼
rebuild
方法:當元素的 BuildOwner
對象調用 scheduleBuildFor
方法的時候,會調用 rebuild
方法來重建元素。首次裝載的時候是在 mount
方法中觸發,配置組件更改時會在 build
方法觸發。這個方法調用了 performRebuild
方法來重建元素。performRebuild
是一個有 Element 的字類實現的方法,也就是每一個元素具體怎麼重建由子類來決定。內容看着不少,咱們來理一下渲染的狀態流轉,這是一個元素的生命週期的狀態圖。組件會被移除出如今 deactivate
方法中,而觸發 deactivate
方法的是一個元素被移入到不活躍元素列表中。將元素移入到不活躍列表的方法是deactivateChild
,也就是父節點上的操做——當一個子元素再也不屬於父元素構建的渲染樹時,就會加入到不活躍的元素列表中。
graph LR createElement --> 初始化((initial)) 初始化((initial)) --mount--> 已裝載((mounted)) 已裝載((mounted)) --activate--> 活躍((active)) 活躍((active)) --deactivate--> 不活躍((inactive)) 不活躍((inactive))--unmount--> 再也不存在((defunct)) 再也不存在((defunct))--> dispose
performRebuild
方法如今咱們知道在 setState 的時候,實際會調用 performRebuild
方法來從新構建組件樹,那麼 performRebuild
方法作了什麼事情?在 Element 中,performRebuild 方法是個空方法,須要子類去實現。所以咱們去 StatefulElement 找找看,代碼以下:
@override
void performRebuild() {
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
複製代碼
還得往上找,那就是 ComponentElement
了,終於找着了!
@override
void performRebuild() {
// 省略調試的代碼
Widget? built;
try {
// ...
built = build();
// ...
} catch (e, stack) {
// ...
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
_dirty = false;
// ...
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
// 省略異常處理
}
// 省略調試代碼
}
複製代碼
這裏的關鍵在於調用了 build
方法和updateChild
方法。其中 經過 built = build()
獲取了最新的Widget
,因爲 build 方法從新構建了組件配置,所以會調用對應的 Widget 的構造函數和 build 方法。而後再調用 updateChild
方法更新子元素。如前所述,updateChild
更新子組件有三種組合。而咱們這裏_child
和 built
確定不爲空,那麼關鍵就在於 built
(Widget
對象)的 canUpdate
是否爲 true
。這個方法在 Widget 類定義:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
複製代碼
註釋說明是若是 Widget
的 key
沒有設置(通常不推薦給組件設置 key),那麼兩個組件的 runtimeType
一致就能夠更新。所以,實際上大部分狀況下返回的都是 true
。咱們調試更新代碼結果也是同樣,最終走到的是Element
的 updateChild
的這個分支:
// ...
else if (hasSameSuperclass &&
Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
}
複製代碼
由此咱們能夠推斷,setState
方法調用後確實會從新構建整個 Widget
,可是並不必定會將 Widget
配置的 Element
元素樹的每個元素都移除,而後用新的元素替換來從新渲染一遍。實際上咱們調試的時候打開 Flutter
的調試工具也能夠看到,實際上的Widget
對應的 Element
在點擊按鈕後並無發生改變。
雖然setState
的調用並無像 Widget
層那樣,在渲染控制層的 Element
那一層從新構建所有element
。可是,這並不表明 setState
的使用沒問題,首先,像以前篇章說的那樣,它會從新構建整個 Widget
樹,這會帶來性能損耗;其次,因爲整個 Widget
樹改變了,意味着整棵樹對應的渲染層Element
對象都會執行 update
方法,雖然不必定會從新渲染,可是這整棵樹的遍歷的性能開銷也很高。所以,從性能上考慮,仍是儘可能不要使用 setState
——除非,這個組件真的很簡單,並且下級組件沒有或者不多。
我是島上碼農,微信公衆號同名,這是Flutter 入門與實戰的專欄文章。
👍🏻:以爲有收穫請點個贊鼓勵一下!
🌟:收藏文章,方便回看哦!
💬:評論交流,互相進步!