不久前,我看到一篇關(guān)于性能優(yōu)化的文章模叙,提到應(yīng)該使用類Widget 替代函數(shù)返回的 Widget歇拆,理由是 "框架不知道函數(shù),但是可以看到類"。這個(gè)說法讓我感到困惑故觅。后來厂庇,我的同事在性能優(yōu)化的分享中也提到了這個(gè)問題,同事指出系統(tǒng)源碼中使用的大多是類Widget输吏,這更加加深了我的困惑权旷,因?yàn)樵创a中也存在函數(shù) Widget,而一些復(fù)雜的組件评也,例如 Scaffold炼杖、Container、ButtonStyleButton盗迟、TabBarView 中的 build 函數(shù)中坤邪,有大量的判斷邏輯,這里面有許多用局部變量來簡化嵌套代碼罚缕,我理解的這些代碼與函數(shù) Widget 并沒有什么本質(zhì)區(qū)別艇纺。我和同事討論了這個(gè)問題,但沒有得出一個(gè)令人滿意的解釋邮弹。之后黔衡,我查閱了一些相關(guān)的討論和視頻(鏈接在下面),但仍未得出結(jié)論腌乡。只能自己動(dòng)手實(shí)踐來探究這個(gè)問題盟劫。
源碼才是解釋一切最有效的手段
通過framework類可以知道Widget 與 Element 具體調(diào)用的流程,看一下里面performRebuild方法(簡化了無關(guān)討論內(nèi)容的代碼):
void performRebuild() {
Widget? built = build();
_child = updateChild(_child, built, slot);
}
這種方法能夠揭示 widget 和 Element 的更新過程与纽。在此過程中侣签,系統(tǒng)首先調(diào)用 build() 方法,將所有 widget 打包成一個(gè) widget急迂。然后影所,系統(tǒng)遍歷該新 widget,以更新現(xiàn)有的 Element 樹和 Widget 樹僚碎。更新過程的代碼如下:
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
final Element newChild;
if (child != null) {
// 判斷新舊 widget是否是同屬于StatefulWidget或StatelessWidget猴娩,一般只有熱重載時(shí)才有可能變成 false
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
/// 非繼承于RenderObjectElement的Element更新Element 上的_widget 參數(shù)并繼續(xù)子組件查找更新
/// 繼承于RenderObjectElement的Element除了更新了_widget還更新了 RenderObject
void update(covariant Widget newWidget) {
_widget = newWidget;
}
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Key? key = newWidget.key;
if (key is GlobalKey) {
// 如果 key 是GlobalKey,需要將這個(gè)key 所對(duì)應(yīng)的Element 移動(dòng)到現(xiàn)在位置后再去重新刷新
final Element? newChild = _retakeInactiveElement(key, newWidget);
newChild._activateWithParent(this, newSlot);
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild!;
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
更新過程的代碼表明勺阐,除了 child 和 newWidget 為空以及 key 變化外卷中,Element 的更新僅取決于新舊 Widget 的層級(jí)和類型。只有 Widget 的層級(jí)或類型發(fā)生變化皆看,才會(huì)導(dǎo)致 Element 重新創(chuàng)建仓坞。因此,無論使用類Widget 還是返回 Widget 的函數(shù)腰吟,在更新之前都會(huì)被組合成一個(gè) Widget无埃,并且不會(huì)導(dǎo)致 Widget 的層級(jí)或類型發(fā)生變化徙瓶。在 RenderObject 樹中,只有繼承了 RenderObjectWidget 的組件的 Element 才會(huì)調(diào)用 mount 方法以創(chuàng)建新的 RenderObject嫉称。只要 Element 保持不變侦镇,RenderObject 的結(jié)構(gòu)也將保持不變。因此织阅,從這個(gè)角度來看壳繁,類Widget 和函數(shù) Widget 沒有本質(zhì)區(qū)別。
在 Dart 層面荔棉,類Widget 和函數(shù) Widget 之間存在一些差別闹炉。例如,類Widget 支持 const润樱,而函數(shù) Widget 則不支持渣触。在一些開源的 Flutter 小項(xiàng)目中,大多數(shù)采用類Widget壹若,其中的數(shù)據(jù)通常寫在本地且不變嗅钻。因此,它們通常使用 const店展,以優(yōu)化項(xiàng)目性能养篓。然而,在實(shí)際項(xiàng)目中赂蕴,數(shù)據(jù)通常來自后端柳弄,這些數(shù)據(jù)是動(dòng)態(tài)變化的。即使將組件封裝為類Widget概说,其調(diào)用方的參數(shù)也會(huì)變化语御,因此不能使用 const 進(jìn)行修飾。
綜上所述席怪,函數(shù) Widget 并不是不能使用的,可以在項(xiàng)目中適當(dāng)?shù)奈恢檬褂煤瘮?shù) Widget纤控,我總結(jié)出的使用規(guī)則如下:
- 可能出現(xiàn)復(fù)用的Widget使用statelessWidget或者statefulWidget進(jìn)行封裝挂捻,這個(gè)是遵循Flutter的通用規(guī)則。
- statelessWidget頁面里面船万,不需要變化的部分可以使用函數(shù)進(jìn)行模塊的拆分刻撒,需要變化的部分可以使用statefulWidget進(jìn)行封裝。
- statefulWidget頁面里面耿导,不需要變化且不需要外部變量的部分声怔,需要使用statelessWidget封裝且用const修飾,不需要單獨(dú)變化且需要外部變量的部分可以使用函數(shù)進(jìn)行模塊的拆分舱呻,需要單獨(dú)變化的部分使用statefulWidget封裝醋火。