flutter开发防止widget rebuild终极解决办法

2019-09-0507:42:37APP与小程序开发Comments2,872 views字数 6262阅读模式

flutter是借鉴了前端框架React的思想而开发的框架,有很多相似之处,也有看不到的不一样,我目前感受最深的就是flutter无所不在的rebuild,那么有办法阻止rebuild吗?有!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

在widget前面加const

这个办法确实可以,一劳永逸,但是你一旦加了const,你这个widget就永远不会更新了,除非你是在写静态页面,否则你最好不要用它文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

把你的组件写成 “叶子"组件

参考flutter文档 就是把那你的组件都定义成叶子,树的最底层,然后你在叶子组件内部更改状态,这样叶子之间互不影响,emm,在我看来这样子跟react的状态提升的思想相反了,因为你为了互不影响,你不能把状态放到根节点,放到根节点,一调用setState那全部自组价就rebuild了,我一开始一直是用这个思路来解决rebuild的问题的, 比如使用StreamBuilder这个可以包裹你的组件,然后用流来触发StreamBuilder内部rebuild,通过StreamBuilder来隔绝外面的组件,这样写有个小缺点,我要额外写个流,还要关闭流,很啰嗦。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

使用其他的库,比如Provider

你可以看到Provider库的作者提供了一些Widget来减少rebuild,但是我感觉都不太简洁,易用 这些库的实现方法跟StreamBuilder差不多,都是通过一个Widget来隔绝其他Widget,让更新限制在内部,但是都有一个共同点,你要配合额外的外部变量去触发内部的更新文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

终极办法

用过react的人都知道,react的类组件有个很重要的生命周期叫shouldComponentUpdate,我们可以在组件内部重写这个声明周期来进行性能优化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

如何优化呢,就是对比组件的新旧props的属性的值是否一致,如果一致那组件就没必要更新. 那flutter有没有类似的生命周期呢?没有!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

flutter团队认为flutter的渲染速度已经够快了,并且flutter实际也有类似react 的diff算法来对比element是否需要更新,他们做了优化和缓存,因为更新flutter的element是很昂贵的操作,而rebuild Widget只是重新new 了一个widget的实例,就像只是执行了一段dart代码一样,没涉及到任何ui层的更改,而且他们也对新旧widget做了diff,通过diff widget来减少对element层的更改,不管怎样,只要没有导致element销毁,重建,一般不会影响什么性能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

但是通过谷歌和百度你还是能发现有人在搜索如何防止rebuild,这说明了市场还是有需求的。我个人认为,这个不叫过度优化,其实是有这个场景需要优化的,比如谷歌推荐的状态管理库Provider就提供了如何减少不必要的rebuild的方法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

话(我)不(想)多(吐)说(槽)了:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

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行代码 来看测试代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

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'),
          ),
        ],
      ),
    );
  }
}



复制代码

布局效果图:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

flutter开发防止widget rebuild终极解决办法
  • 我们定义了一个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则没有做优化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

  • 我们点击增加Product的按钮 increment Product,会触发增加productNum,而此时没有增加counter,所以被ShouldRebuild包裹的Counter并没有rebuild,而下面没有包裹的Counter就rebuild了 来看下gif:
flutter开发防止widget rebuild终极解决办法

原理揭秘

其实原理跟用const声明的widget一致,来看下flutter源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

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;
      }

...
}
复制代码

摘抄其中一部分, 第一个文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
   }
复制代码

这里是关键,flutter发现child.widget也就是老的widget和新的widget是同一个,引用一致的话就直接返回了child文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

如果发现不一致就走了这里文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

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方法肯定会执行了。 请看它做了什么事文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

@override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
复制代码

看到rebuild()就知道一定去执行build了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

其实看到 if (child.widget == newWidget) 我们也知道为什么 const Text()会让Text不会重复build,因为常量是一直不会变的文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

总结

有了这个Widget我们可以把状态都放在根组件,然后把页面拆分成多个子组件,然后用ShouldRebuild包裹子组件,同时设置rebuild的条件就可以阻止 不必要的 重渲染。你可以尽情的setState了,当然如果你的状态被多个组件使用,这时候你就需要状态管理了。 但是,可能有人会觉得是否过度优化,我个人觉得是否需要优化是根据你自己的情况定的,如果某天用户反馈你的页面卡顿,那你就需要优化,又或者你觉得rebuild影响到了你的功能,比如动画重复执行了,那你就需要阻止rebuild了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

github:shouldRebuild

作者:SuperMan一路向北文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/16090.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/16090.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定