新建一個flutter工程, 以flutter框架給我們自動生成的程式碼為例, 當我們點選按鈕更新記數_counter
時,最終是通過呼叫State<T>.setState
來更新檢視的:
setState(() {
_counter++;
})
複製程式碼
首先需要理解為什麼要setState
, 它表示當前節點的資料變更,通知檢視需要更新.更新哪個檢視? 持有當前這個State
例項的節點對應的檢視. 注意這個節點具體指的是Element
物件, Widget
只是建立了State
例項(_MyHomePageState createState()
),並沒有持有, 同樣State
又繼續建立了子檢視,也沒有持有子檢視(Widget build(BuildContext context)
), 持有State
的只有Element
. setState
的引數是一個方法執行體, 實現哪些資料的具體變更, 所以其實沒有設定所謂的狀態, 還不如叫notifyChanges
來的明晰.
其次需要理解檢視如何更新. 像Text
那個控制元件, 文字是作為建構函式的引數直接傳給控制元件的, 根本連類似setText
的方法也沒有! 所以顯示出來的資料要更新除了新建檢視物件外沒有別的辦法!
這裡就體現了flutter與傳統移動端介面開發的巨大不同: 檢視是通過新建檢視物件來完成更新的. 以往的介面開發中檢視物件都是一個比較重比較大的物件, 檢視要避免冗餘, 要儘量複用, 不要頻繁建立. 但在flutter中就不是這樣了, 代表檢視物件的Widget
是輕量物件, 它不持有State
, 也不持有Widget
, 所有檢視物件都是通過build
這種建立型關係建立. 所以開發過程中也要堅決避免自定義的Widget
持有資料, 因為Widget
物件會被很快替換掉.
有了上述兩點就能明白setState
之後發生了什麼: 當前_MyHomePageState
的Widget build(BuildContext context)
方法會被呼叫, 於是生成了新的Scaffold
物件,連帶著AppBar,FloatingActionButton,Column
一干控制元件其中自然包括我們需要展示的Text
物件, 這時傳入的文字是更新過後的_counter
,於是檢視得以更新.
只是想更新一個個小小的文字框就不得不重新建立整個檢視?!
對, 目前的機制就是這樣. 那隨著檢視層次加深, 介面互動複雜,這種重新建立型操作就沒有一點問題? 畢竟物件再小也有開銷, 那麼多物件累積起來,也可能造成建立過程的消耗.於是我們的問題終於來了: 有沒有方法可以只更新部分檢視?
縮小一下更新範圍不就得了? 現在的更新範圍大是因為_MyHomePageState.build
被呼叫返回了整個檢視, 而_MyHomePageState
對應的檢視是MyHomePage
. 所以建立一個State<Text>
, build
返回Text
控制元件例項, 再將這個State<Text>
持有, 資料變更時呼叫State<Text>
.setState()`不就可以達到目的?
這個想法符合flutter本身的機制, 但問題就是誰來建立這個State<Text>
? 如前文所述, 首先只有StatefulWidget
才能建立State例項, 其次必須是父節點建立這個State<Text>
. 但示例中Text
的父節點Column
首先就不是StatefulWidget
; 就算是了, 我們還要宣告Widget類繼承Column
覆蓋build
方法, 再宣告State類繼承State<Text>
, 煩都煩死了. 那如果從Text
向上找一個StatefulWidget
, 建立的時候是Text
的一個祖先節點, 存在一點冗餘可以接受呢? 這個想法實踐上一點也不可行, 且不說有個特定檢視物件的查詢過程, 上面所說的各種類宣告一點也沒有減少, 所以這個路子是沒法搞的.
所以還是從setState
原始碼入手, 看一個節點到底是如何更新檢視的.
State.setState
Element.markNeedsBuild
Element._dirty = true;
BuildOwner.scheduleBuildFor
BuildOwner._dirtyElements.add
Element._inDirtyList = true;
複製程式碼
過程比想象的簡單, 最後僅僅是將Element節點標識成dirty並加入到了BuildOwner的_dirtyElements列表裡. 從Element角度看setState
這個名稱似乎也沒有錯, 不過它是相對Element
說的, 具體設定的是Element
的dirty
狀態. 那我們只需找到Text
對應的Element節點並呼叫一下它的markNeedsBuild
不就ok了? 所以先要找到Text
這個Widget節點對應的Element節點.
在以前的建樹流程中說過Element節點結構像掛鉤, Element只有parent沒有children, 要找子節點需要像Element.visitChildren
那樣傳遞一個訪問者來進行遍歷, 而判斷條件自然就是Element持有的Widget是否是我們需要更新的Widget, 於是有:
static Element findChild(Element e, Widget w) {
Element child;
void visit(Element element) {
if (w == element.widget)
child = element;
else
element.visitChildren(visit);
}
visit(e);
return child;
}
複製程式碼
但是對找到的element設定markNeedsBuild
竟然不起作用! 查了半天原因, 才明白還是把建樹流程搞混了, markNeedsBuild
僅讓當前Element節點的build被呼叫, 建立的是當前節點的子節點檢視物件, 而我們現在需要的是把當前子節點持有的檢視物件替換掉('檢視更新是通過建立新的Widget物件'), 同時不能重新建立當前Element節點及其子節點. 而Element.update(Widget)
正是這個作用!! 如果說inflateWidget
是初始化Element節點樹, 那update
正是在樹建立成功後進行更新操作. 於是有
onPressed: () {
_counter++;
Element e = findChild(context as Element, title);
if (e != null) {
e.update(title);
}
},
複製程式碼
因為要找節點, 所以用了一個title
持有了Text
, 以方便在onTap()
的上下文中作查詢引數.
但這樣也是不對的! 這裡存在2個問題:
- 檢視物件沒有更新. 我們需要展示的是一個新的_counter相關的文字, 因此需要的是一個新的檢視物件, 現在傳入的還是老的檢視物件,等於什麼也沒更新...
- 直接呼叫
Element.update
是有異常的, 跟蹤了一下發現一個標識狀態的資料_debugStateLockLevel
不對, 原來要在BuildOwner.lockState
中執行才可以.
這裡囉裡八嗦的寫這一坨是想表明一個的新想法的實現是環環相扣關聯細節的, 很多時候思路是對的, 但細節實現錯誤導致半途而廢, 行百里者半九十!
還是上完整程式碼, findChild
前面已定義就不再貼了:
import 'package:flutter/foundation.dart'
import 'package:flutter/material.dart';
import 'utils/ElementUtils.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Pages'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
Widget title = new Text(
'another times: $_counter',
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
title,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counter++;
Element e = findChild(context as Element, title);
if (e != null) {
title = new Text(
'another times: $_counter',
);
e.owner.lockState(() {
e.update(title);
});
}
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
複製程式碼
現在只是重新建立了僅僅一個檢視哦, 它不快都不行~!
然而還是需要考慮一下這麼做的缺點或者劣勢是什麼
首先, 明顯的存在一個查詢操作, 這是由Element機制決定的, 遍歷只能通過訪問者模式, 時間複雜度O(n), 能不能避免這個查詢或者建立Widget到Element的對映? 也可以, 但是至少要查詢一次,因為建立widget的時候Element可能還沒建立或者還沒有關聯, 只有Element樹建立完成之後才能查的到.
其次, 如果一個操作涉及多個檢視的更新, 我們不得不持有多個widget, 並查詢多個widget對應的element, 還是有多個查詢操作, 這麼麻煩還不如全部新建呢.
所以只能視情況而定, 沒有包打天下一勞永逸的方案, 合適的才是最好的!