Flutter面试--Widget

1,048 阅读5分钟

导语

作为一个Flutter开发者多年,觉得自身毫无建树,技术看似学习很多,但是仔细回顾一团乱麻,借此通过面试角度记录回顾Flutter各种知识点,先从接触最多的Widget开始。加油!

Widget是什么?

源码注释中对于Widget介绍是:

Describes the configuration for an [Element].
描述[Element]的配置。

很简单但是也很抽象。因此我们先要理解Element是什么,才能知道Widget为何定位为Element的配置。而Element在源码中的注释为:

An instantiation of a [Widget] at a particular location in the tree.
在树中的特定位置实例化[Widget]。

对此可以知道Element是视图树上的具体实例化的存在,而且Element是Widget实例化的产物。 对于该问题也就有了答案:Widget是渲染树上具体实例Element的配置。

对于为何Widget定义为Element的配置?

最简单的方法可以看看Element的update方法

void update(covariant Widget newWidget) {
  _widget = newWidget;
}

去掉注释断言后代码就剩很简单的一句,在界面有变化时候会获取一个newWidget用来覆盖之前的Widget,因此Widget是一种抽象概念便于我们修改Element而出现的。因此Widget的修改变动成本比较低,每次我们修改Widget大部分时候不会导致Element重新创建,而仅仅是修改Element的某些参数,因此频繁的Widget变动对界面渲染并没带来很大的困扰和压力。

怎么修改Widget会导致Element更新?

可以在Element中updateChild中找到答案。简化代码后如下:

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  final Element newChild;
  if (child != null) {
    //判断类型是否一致,
    //StatefulElement对应StatefulWidget,
    //StatelessElement对应StatelessWidget
    hasSameSuperclass = oldElementClass == newWidgetClass;
    //当widget未改变时,直接复用
    if (hasSameSuperclass && child.widget == newWidget) {
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      //当widget经过canUpdate判断时,更新widget然后复用element
      child.update(newWidget);
      newChild = child;
    } else {
    //无法复用,进行销毁
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
  //创建新的
    newChild = inflateWidget(newWidget, newSlot);
  }
}

通过代码可以看到变更widget最终导致就4种结果,

  • 更新widget,但是复用老Element
  • 直接复用老Widget和Element
  • 销毁后创建新的
  • 直接进行创建流程

除了第一种情况,剩下三种情况都是比较容易理解,这里重点看看第一种情况,就是Widget中的canUpdate做了什么操作。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

可以看到Widget的类型和Key没发生变化的时候,canUpdate会返回true,将会导致Element进行复用。可以得到结论是:当weidget类型发生变化或者key发生变化时候都会导致Element重新创建。

认识Widget类型

聊起Widget肯定离不开StatelessWidget和StatefulWidget,这两种最常见的Widget属于组合类。 还有代理类:InheritedWidget,ParentDataWidget。绘制类:RenderObjectWidget
这里重点介绍StatefulWidget,源码注释中对于StatefulWidget的介绍是:

A widget that has mutable state.
具有可变状态的小部件。

个人理解为StatefulWidget相对StatelessWidget内部多维护了一个State对象,用于存储可变的状态数据。根据之前内容可以知道Widget只是Element的配置,属于可以随时更新抛弃的,因此导致某些需要记录状态场景下,单纯Widget和Element是无法实现,因此多引入了State这一类型。

StatefulWidget的生命周期

因此StatefulWidget相对StatelessWidget复杂很多,存在多个生命状态,这里画了一个简图可以简单了解StatefulWidget方法调用顺序以及对应Element的生命周期。

  • initState()  表示当前 State 将和一个 BuildContext 产生关联,但是此时BuildContext 没有完全装载完成,如果你需要在该方法中获取 BuildContext ,可以通过  WidgetsBinding.instance.addPostFrameCallback((timeStamp) { });在回到中获取 。
  • didChangeDependencies()  在 initState() 之后调用,当 State 对象的依赖关系InheritedWidget发生变化时,该方法被调用。
  • deactivate()  当 State 被暂时从视图树中移除时,会调用这个方法,同时页面切换时,也会调用。
  • dispose()  Widget 销毁了,在调用这个方法之前,总会先调用 deactivate()。
  • didUpdateWidge 当 widget 状态发生变化时,会调用。

截屏2024-07-04 09.56.50.png

触发刷新setState

Flutter的setState()方法是用于更新widget状态的,看看源码中具体是如何实现的。

void setState(VoidCallback fn) {
if (_debugLifecycleState == _StateLifecycle.defunct) 
    throw FlutterError
if (_debugLifecycleState == _StateLifecycle.created && !mounted) 
    throw FlutterError
final Object? result = fn() as dynamic;
assert(() {
  if (result is Future) {
    throw FlutterError
}());
_element!.markNeedsBuild();

这里可以看到setState成功调用存在三个前提:

  1. 当前State的生命周期不是defunct
  2. 当前State处于create状态下且需要mounted
  3. setState执行的方法不能是耗时Future类型

当条件都满足的时候会调用element的markNeedsBuild方法,接下来看看markNeedsBuild方法的实现。

void markNeedsBuild() {
if (dirty) {
  return;
}
_dirty = true;
owner!.scheduleBuildFor(this);

在去掉众多条件判断后,具体做了就是把_dirty置为true,然后调用owner的scheduleBuildFor方法传入了当前的element,而scheduleBuildFor中做的具体就是在owner中维护了一个_dirtyElements的List存储需要刷新标为dirty的elemnet,然后在下一帧界面构建的时候遍历去刷新构建新的界面。

至此对于Widget各个方面都有所了解,如果在面试中碰到以下问题应该都能回答了。

  • Widget的类型和作用。
  • StatelessWidget和StatefulWidget区别。
  • StatefulWidget的生命周期以及对应方法的使用。
  • setState使用的条件又做了什么。