背景
眾所周知,flutter是借鑑了前端框架React的思想而開發的框架,有很多相似之處,也有看不到的不一樣,我目前感受最深的就是flutter無所不在的rebuild,那麼有辦法阻止rebuild嗎?有!
在widget前面加const
這個辦法確實可以,一勞永逸,但是你一旦加了const,你這個widget就永遠不會更新了,除非你是在寫靜態頁面,否則你最好不要用它
把你的元件寫成 “葉子"元件
參考flutter文件
就是把那你的元件都定義成葉子,樹的最底層,然後你在葉子元件內部更改狀態,這樣葉子之間互不影響,emm,在我看來這樣子跟react的狀態提升的思想相反了,因為你為了互不影響,你不能把狀態放到根節點,放到根節點,一呼叫setState那全部自組價就rebuild了,我一開始一直是用這個思路來解決rebuild的問題的,
比如使用StreamBuilder
這個可以包裹你的元件,然後用流來觸發StreamBuilder內部rebuild,通過StreamBuilder來隔絕外面的元件,這樣寫有個小缺點,我要額外寫個流,還要關閉流,很囉嗦。
使用其他的庫,比如Provider
你可以看到Provider庫的作者提供了一些Widget來減少rebuild,但是我感覺都不太簡潔,易用 這些庫的實現方法跟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,因為常量是一直不會變的
總結
有了這個Widget我們可以把狀態都放在根元件,然後把頁面拆分成多個子元件,然後用ShouldRebuild包裹子元件,同時設定rebuild的條件就可以阻止 不必要的 重渲染。你可以盡情的setState了,當然如果你的狀態被多個元件使用,這時候你就需要狀態管理了。 但是,可能有人會覺得是否過度優化,我個人覺得是否需要優化是根據你自己的情況定的,如果某天使用者反饋你的頁面卡頓,那你就需要優化,又或者你覺得rebuild影響到了你的功能,比如動畫重複執行了,那你就需要阻止rebuild了。
github:shouldRebuild
如果覺得幫助到了你,請star一下吧