背景
众所周知,flutter是借鉴了前端框架React的思想而开发的框架,有很多相似之处,也有看不到的不一样,我目前感受最深的就是flutter无所不在的rebuild,那么有办法阻止rebuild吗?
在widget前面加const
这个办法确实可以,一劳永逸,但是你一旦加了const,你这个widget就永远不会更新了,除非你是在写静态页面,否则你最好不要用它
把你的组件写成 “叶子"组件
参考flutter文档
就是把那你的组件都定义成叶子,树的最底层,然后你在叶子组件内部更改状态,这样叶子之间互不影响,emm,在我看来这样子跟react的状态提升的思想相反了,因为你为了互不影响,你不能把状态放到根节点,放到根节点,一调用setState那全部自组价就rebuild了,我一开始一直是用这个思路来解决rebuild的问题的,
比如使用StreamBuilder
这个可以包裹你的组件,然后用流来触发StreamBuilder内部rebuild,通过StreamBuilder来隔绝外面的组件,这样写有个小缺点,我要额外写个流,还要关闭流,很啰嗦。
使用其他的库,比如Provider
这些库的实现方法跟StreamBuilder差不多,都是通过一个Widget来隔绝其他Widget,让更新限制在内部,但是都有一个共同点,你要配合额外的外部变量去触发内部的更新
终极办法
用过react的人都知道,react的类组件有个很重要的生命周期叫shouldComponentUpdate
,我们可以在组件内部重写这个声明周期来进行性能优化。
如何优化呢,就是对比组件的新旧props的属性的值是否一致,如果一致那组件就没必要更新.
那flutter有没有类似的生命周期呢?没有!
flutter团队认为flutter的渲染速度已经够快了,并且flutter实际也有类似react 的diff算法来对比element是否需要更新,他们做了优化和缓存,因为更新flutter的element是很昂贵的操作,而rebuild Widget只是重新new 了一个widget的实例,就像只是执行了一段dart代码一样,没涉及到任何ui层的更改,而且他们也对新旧widget做了diff,通过diff widget来减少对element层的更改,不管怎样,只要没有导致element销毁,重建,一般不会影响什么性能。
但是通过谷歌和百度你还是能发现有人在搜索如何防止rebuild,这说明了市场还是有需求的。我个人认为,这个不叫过度优化,其实是有这个场景需要优化的,比如谷歌推荐的状态管理库Provider就提供了如何减少不必要的rebuild的方法
话(我)不(想)多(吐)说(槽)了:
library should_rebuild_widget;
import 'package:flutter/material.dart';
typedef ShouldRebuildFunction<T> = bool Function(T oldWidget, T newWidget);
class ShouldRebuild<T extends Widget> extends StatefulWidget {
final T child;
final ShouldRebuildFunction<T> shouldRebuild;
ShouldRebuild({@required this.child, this.shouldRebuild}):assert((){
if(child == null){
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary('ShouldRebuild widget: builder must be not null')]
);
}
return true;
}());
@override
_ShouldRebuildState createState() => _ShouldRebuildState<T>();
}
class _ShouldRebuildState<T extends Widget> extends State<ShouldRebuild> {
@override
ShouldRebuild<T> get widget => super.widget;
T oldWidget;
@override
Widget build(BuildContext context) {
final T newWidget = widget.child;
if (this.oldWidget == null || (widget.shouldRebuild == null ? true : widget.shouldRebuild(oldWidget, newWidget))) {
this.oldWidget = newWidget;
}
return oldWidget;
}
}
就是这几行代码,不到40行代码
来看测试代码:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:should_rebuild_widget/should_rebuild_widget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
@override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int productNum = 0;
int counter = 0;
_incrementCounter(){
setState(() {
++counter;
});
}
_incrementProduct(){
setState(() {
++productNum;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
constraints: BoxConstraints.expand(),
child: Column(
children: <Widget>[
ShouldRebuild<Counter>(
shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
child: Counter(counter: counter,onClick: _incrementCounter,title: '我是优化过的Counter',) ,
),
Counter(
counter: counter,onClick: _incrementCounter,title: '我是未优化过的Counter',
),
Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
RaisedButton(
onPressed: _incrementProduct,
child: Text('increment Product'),
)
],
),
),
),
);
}
}
class Counter extends StatelessWidget {
final VoidCallback onClick;
final int counter;
final String title;
Counter({this.counter,this.onClick,this.title});
@override
Widget build(BuildContext context) {
Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
return AnimatedContainer(
duration: Duration(milliseconds: 500),
color:color,
height: 150,
child:Column(
children: <Widget>[
Text(title,style: TextStyle(fontSize: 30),),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('counter = ${this.counter}',style: TextStyle(fontSize: 43,color: Colors.white),),
],
),
RaisedButton(
color: color,
textColor: Colors.white,
elevation: 20,
onPressed: onClick,
child: Text('increment Counter'),
),
],
),
);
}
}
布局效果图:
- 我们定义了一个Counter组件,Counter在build的过程中会改变自己的背景色,每次执行build都会随机生成背景色,以便我们观察组件是否build。另外Counter接收父组件传过来的值counter,并展示,还接收一个title,来区分不同的Counter名字
- 看这里的代码
Column(
children: <Widget>[
ShouldRebuild<Counter>(
shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
child: Counter(counter: counter,onClick: _incrementCounter,title: '我是优化过的Counter',),
),
Counter(
counter: counter,onClick: _incrementCounter,title: '我是未优化过的Counter',
),
Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
RaisedButton(
onPressed: _incrementProduct,
child: Text('increment Product'),
)
],
)
我们上面的Counter被ShouldRebuild包裹,同时shouldRebuild参数传入了自定义的条件当这个Counter接收的counter不一致时才rebuild,如果新老Counter对比发现counter一致那就不rebuild,
而下面的Counter则没有做优化。
- 我们点击增加Product的按钮
increment Product
,会触发增加productNum,而此时没有增加counter,所以被ShouldRebuild包裹的Counter并没有rebuild,而下面没有包裹的Counter就rebuild了
来看下gif:
原理揭秘
其实原理跟用const声明的widget一致,来看下flutter源码
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (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;
}());
return child;
}
...
}
摘抄其中一部分,
第一个
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
这里是关键,flutter发现child.widget也就是老的widget和新的widget是同一个,引用一致的话就直接返回了child
如果发现不一致就走了这里
if (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;
}());
return child;
}
这里如果可以更新,就会走child.update(),这个方法一旦走了,那build方法肯定会执行了。
请看它做了什么事
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
看到rebuild()就知道一定去执行build了。
其实看到 if (child.widget == newWidget) 我们也知道为什么 const Text()会让Text不会重复build,因为常量是一直不会变的
github:shouldRebuild
如果觉得帮助到了你,请star一下吧
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。