新建一個flutter工程, 以flutter框架給咱們自動生成的代碼爲例, 當咱們點擊按鈕更新記數_counter
時,最終是經過調用State<T>.setState
來更新視圖的:bash
setState(() {
_counter++;
})
複製代碼
首先須要理解爲何要setState
, 它表示當前節點的數據變動,通知視圖須要更新.更新哪一個視圖? 持有當前這個State
實例的節點對應的視圖. 注意這個節點具體指的是Element
對象, Widget
只是建立了State
實例(_MyHomePageState createState()
),並無持有, 一樣State
又繼續建立了子視圖,也沒有持有子視圖(Widget build(BuildContext context)
), 持有State
的只有Element
. setState
的參數是一個方法執行體, 實現哪些數據的具體變動, 因此其實沒有設置所謂的狀態, 還不如叫notifyChanges
來的明晰.app
其次須要理解視圖如何更新. 像Text
那個控件, 文本是做爲構造函數的參數直接傳給控件的, 根本連相似setText
的方法也沒有! 因此顯示出來的數據要更新除了新建視圖對象外沒有別的辦法!框架
這裏就體現了flutter與傳統移動端界面開發的巨大不一樣: 視圖是經過新建視圖對象來完成更新的. 以往的界面開發中視圖對象都是一個比較重比較大的對象, 視圖要避免冗餘, 要儘可能複用, 不要頻繁建立. 但在flutter中就不是這樣了, 表明視圖對象的Widget
是輕量對象, 它不持有State
, 也不持有Widget
, 全部視圖對象都是經過build
這種建立型關係創建. 因此開發過程當中也要堅定避免自定義的Widget
持有數據, 由於Widget
對象會被很快替換掉.less
有了上述兩點就能明白setState
以後發生了什麼: 當前_MyHomePageState
的Widget build(BuildContext context)
方法會被調用, 因而生成了新的Scaffold
對象,連帶着AppBar,FloatingActionButton,Column
一干控件其中天然包括咱們須要展現的Text
對象, 這時傳入的文本是更新事後的_counter
,因而視圖得以更新.ide
只是想更新一個個小小的文本框就不得不從新建立整個視圖?!函數
對, 目前的機制就是這樣. 那隨着視圖層次加深, 界面交互複雜,這種從新建立型操做就沒有一點問題? 畢竟對象再小也有開銷, 那麼多對象累積起來,也可能形成建立過程的消耗.因而咱們的問題終於來了: 有沒有方法能夠只更新部分視圖?post
縮小一下更新範圍不就得了? 如今的更新範圍大是由於_MyHomePageState.build
被調用返回了整個視圖, 而_MyHomePageState
對應的視圖是MyHomePage
. 因此建立一個State<Text>
, build
返回Text
控件實例, 再將這個State<Text>
持有, 數據變動時調用State<Text>
.setState()`不就能夠達到目的?ui
這個想法符合flutter自己的機制, 但問題就是誰來建立這個State<Text>
? 如前文所述, 首先只有StatefulWidget
才能建立State實例, 其次必須是父節點建立這個State<Text>
. 但示例中Text
的父節點Column
首先就不是StatefulWidget
; 就算是了, 咱們還要聲明Widget類繼承Column
覆蓋build
方法, 再聲明State類繼承State<Text>
, 煩都煩死了. 那若是從Text
向上找一個StatefulWidget
, 建立的時候是Text
的一個祖先節點, 存在一點冗餘能夠接受呢? 這個想法實踐上一點也不可行, 且不說有個特定視圖對象的查找過程, 上面所說的各類類聲明一點也沒有減小, 因此這個路子是無法搞的.this
因此仍是從setState
源碼入手, 看一個節點究竟是如何更新視圖的.spa
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個問題:
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, 仍是有多個查詢操做, 這麼麻煩還不如所有新建呢.
因此只能視狀況而定, 沒有包打天下一勞永逸的方案, 合適的纔是最好的!