新建一个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, 仍是有多个查询操做, 这么麻烦还不如所有新建呢.
因此只能视状况而定, 没有包打天下一劳永逸的方案, 合适的才是最好的!