StatelessWidget、StatefulWidget中的build

1,422 阅读9分钟

前言

Flutter中构建页面, 需要用到Widget组件, Widget又分为无状态组件StatelessWidget, 有状态组件StatefulWidget。

无状态组件StatelessWidget,就是说一旦这个Widget创建完成,状态就不允许再变动。

有状态组件StatefulWidget,就是说当前Widget创建完成之后,还可以对当前Widget做更改,可以通过setState函数来刷新当前Widget来达到有状态。

Flutter启动runApp经历了啥文章中, 我们知道, runApp拿到开发者自己的MyApp做为rootWidget, 生成根Widget、根Element、根RenderObject。在程序创建上面三个根节点的同时, 也创建了rootWidget的节点, 因为rootWidget节点可能是无状态的StatelessWidget, 也有可能是有状态的StatefulWidget 。接下来, 我们一起看看这两种状态Widget的加载流程, 特别是根据Widget首次生成Element的过程和build(BuildContext context)的加载时机 。

runApp首次生成MyApp简要流程.png

StatelessWidget

那么, 无状态组件StatelessWidget的创建流程是怎样的呢?StatelessWidget的源码如下:

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key? key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatelessElement createElement() => StatelessElement(this);

  considerations.
  @protected
  Widget build(BuildContext context);
}

由StatelessWidget源码, 我们可以知道:

  • StatelessWidget是个抽象类, 继承Widget。
  • 提供了一个构造器方法const StatelessWidget({ Key? key }) : super(key: key)
  • 重写了创建Element方法createElement
  • 提供了一个开发者常见的Widget build(BuildContext context)方法。

StatelessElement

widget实例调用createElement, 传入当前widget对象, 创建elment对象。接下来, 我们看一下StatelessElement源码:

/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

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

从上面的源码注释、源码、及继承关系可以看出:

  • An [Element] that uses a [StatelessWidget] as its configuration. 这个使用StatelessWidget对象提供的信息做为StatelessElement的配置。
  • StatelessElement继承ComponentElement
  • StatelessElement的初始化方法, 接收外部传进来的StatelessWidget对象。
  • 当前StatelessElement对象持有外部传进来的widget对象
  • 重写了ComponentElement提供的build方法。当element对象执行这个build方法时, 实际上外部传进来的widget对象, 调用了widget.build(this);方法, 也就是我们在StatelessWidget需要实现的Widget build(BuildContext context)方法。
  • 重写了Element提供的update方法, 更新widget时, 调用。

从上面的源码分析知道一个非常重点的知识, 即我们在StatelessWidget类中必须实现的Widget build(BuildContext context)方法, 其中BuildContext context就是element对象。那么, 我常见的build方法是什么时机触发的呢?带着问题, 我们一起分析下ComponentElement源码。

ComponentElement

abstract class ComponentElement extends Element {
  /// Creates an element that uses the given widget as its configuration.
  ComponentElement(Widget widget) : super(widget);

  Element? _child;

  bool _debugDoingBuild = false;
  @override
  bool get debugDoingBuild => _debugDoingBuild;

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_lifecycleState == _ElementLifecycle.active);
    _firstBuild();
    assert(_child != null);
  }

  void _firstBuild() {
    rebuild();
  }

  /// Calls the [StatelessWidget.build] method of the [StatelessWidget] object
  /// (for stateless widgets) or the [State.build] method of the [State] object
  /// (for stateful widgets) and then updates the widget tree.
  ///
  /// Called automatically during [mount] to generate the first build, and by
  /// [rebuild] when the element needs updating.
  @override
  @pragma('vm:notify-debugger-on-exception')
  void performRebuild() {
    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);

    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
    Widget? built;
    try {
      assert(() {
        _debugDoingBuild = true;
        return true;
      }());
      built = build();
      assert(() {
        _debugDoingBuild = false;
        return true;
      }());
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      _debugDoingBuild = false;
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
      _child = updateChild(null, built, slot);
    }

    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.finishSync();
  }

  /// Subclasses should override this function to actually call the appropriate
  /// `build` function (e.g., [StatelessWidget.build] or [State.build]) for
  /// their widget.
  @protected
  Widget build();

  @override
  void visitChildren(ElementVisitor visitor) {
    if (_child != null)
      visitor(_child!);
  }

  @override
  void forgetChild(Element child) {
    assert(child == _child);
    _child = null;
    super.forgetChild(child);
  }
}

ComponentElement继承Element, 我们可以知道如下几个重点知识:

  • ComponentElement对象在mount时, element对象的生命周期即处于_ElementLifecycle.active 活跃状态, 之后调用了_firstBuild方法。
  • _firstBuild调用了Element提供的rebuild方法。
  • ComponentElement重写了Element提供的performRebuild方法。
  • Element的源码可知, Element的生命周期处于_ElementLifecycle.active活跃状态后, Element对象会去执行performRebuild()
  • performRebuild方法, 执行了build方法。
  • build方法调用了StatelessWidget提供的widget.build(this)方法, 返回widget对象。也就是开发者常打交道的方法。

由上面重点知识, 我们了解了StatelessWidgetbuild方法的执行过程, 并且知道build(BuildContext context)context就是element对象。

特别地, 我们需要重中之重地关注ComponentElement重写了void performRebuild()方法。这个方法的官方注释, 清晰可见地说明, 它会去取唤醒StatelessWidget对象中的build方法、State.build中的build方法, 去更新它们的tree。同时, 它在element对象mount, 并处于活跃状态状态后, 会自动地产生第一次build过程。以后当elemnt需要更新时, 调用rebuild, 会执行这个performRebuild方法。

Element

由此观之, 源码的调用顺序是, Element.rebuild() -> ComponentElement.performRebuild() -> ComponentElement.build(),那么 Element.build()  是谁调用的?

这个在它的注释中有写: Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor],也就是由 BuildOwner 调用的(BuildOwner 在 element tree 中是唯一的,它保存在 WidgetsBinding.instance.buildOwner 中)。

那么这个 BuildOwner.scheduleBuildFor(..) 是如何触发的呢?答案在Element.markNeedsBuild(..)

我们看下Element源码:

abstract class Element extends DiagnosticableTree implements BuildContext {
    
    /// Update the given child with the given new configuration.
  ///
  /// This method is the core of the widgets system. It is called each time we
  /// are to add, update, or remove a child based on an updated configuration.
  ///
  /// The `newSlot` argument specifies the new value for this element's [slot].
  ///
  /// If the `child` is null, and the `newWidget` is not null, then we have a new
  /// child for which we need to create an [Element], configured with `newWidget`.
  ///
  /// If the `newWidget` is null, and the `child` is not null, then we need to
  /// remove it because it no longer has a configuration.
  ///
  /// If neither are null, then we need to update the `child`'s configuration to
  /// be the new configuration given by `newWidget`. If `newWidget` can be given
  /// to the existing child (as determined by [Widget.canUpdate]), then it is so
  /// given. Otherwise, the old child needs to be disposed and a new child
  /// created for the new configuration.
  ///
  /// If both are null, then we don't have a child and won't have a child, so we
  /// do nothing.
  ///
  /// The [updateChild] method returns the new child, if it had to create one,
  /// or the child that was passed in, if it just had to update the child, or
  /// null, if it removed the child and did not replace it.
  ///
  /// The following table summarizes the above:
  ///
  /// |                     | **newWidget == null**  | **newWidget != null**   |
  /// | :-----------------: | :--------------------- | :---------------------- |
  /// |  **child == null**  |  Returns null.         |  Returns new [Element]. |
  /// |  **child != null**  |  Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
  ///
  /// The `newSlot` argument is used only if `newWidget` is not null. If `child`
  /// is null (or if the old child cannot be updated), then the `newSlot` is
  /// given to the new [Element] that is created for the child, via
  /// [inflateWidget]. If `child` is not null (and the old child _can_ be
  /// updated), then the `newSlot` is given to [updateSlotForChild] to update
  /// its slot, in case it has moved around since it was last built.
  ///
  /// See the [RenderObjectElement] documentation for more information on slots.
  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      // When the type of a widget is changed between Stateful and Stateless via
      // hot reload, the element tree will end up in a partially invalid state.
      // That is, if the widget was a StatefulWidget and is now a StatelessWidget,
      // then the element tree currently contains a StatefulElement that is incorrectly
      // referencing a StatelessWidget (and likewise with StatelessElement).
      //
      // To avoid crashing due to type errors, we need to gently guide the invalid
      // element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
      // returns false which prevents us from trying to update the existing element
      // incorrectly.
      //
      // For the case where the widget becomes Stateful, we also need to avoid
      // accessing `StatelessElement.widget` as the cast on the getter will
      // cause a type error to be thrown. Here we avoid that by short-circuiting
      // the `Widget.canUpdate` check once `hasSameSuperclass` is false.
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && 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;
        }());
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
     
     /// 惊天秘密!!! 首次进入的时候, newChild为空。这里调用了inflateWidget方法, 根据外部传进来的widget(Myapp), 创建widget的elment节点并赋值给_child, 然后返回给根RenderObjectToWidgetElement持有。
      newChild = inflateWidget(newWidget, newSlot);
    }

    ....
    
    return newChild;
  }

  /// Add this element to the tree in the given slot of the given parent.
  ///
  /// The framework calls this function when a newly created element is added to
  /// the tree for the first time. Use this method to initialize state that
  /// depends on having a parent. State that is independent of the parent can
  /// more easily be initialized in the constructor.
  ///
  /// This method transitions the element from the "initial" lifecycle state to
  /// the "active" lifecycle state.
  ///
  /// Subclasses that override this method are likely to want to also override
  /// [update], [visitChildren], [RenderObjectElement.insertRenderObjectChild],
  /// [RenderObjectElement.moveRenderObjectChild], and
  /// [RenderObjectElement.removeRenderObjectChild].
  ///
  /// Implementations of this method should start with a call to the inherited
  /// method, as in `super.mount(parent, newSlot)`.
  @mustCallSuper
  void mount(Element? parent, Object? newSlot) {
    assert(_lifecycleState == _ElementLifecycle.initial);
    assert(widget != null);
    assert(_parent == null);
    assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    _parent = parent;
    _slot = newSlot;
    _lifecycleState = _ElementLifecycle.active;
    _depth = _parent != null ? _parent!.depth + 1 : 1;
    if (parent != null) {
      // Only assign ownership if the parent is non-null. If parent is null
      // (the root node), the owner should have already been assigned.
      // See RootRenderObjectElement.assignOwner().
      _owner = parent.owner;
    }
    assert(owner != null);
    final Key? key = widget.key;
    if (key is GlobalKey) {
      owner!._registerGlobalKey(key, this);
    }
    _updateInheritance();
  }

   ......
   
  /// Create an element for the given widget and add it as a child of this
  /// element in the given slot.
  ///
  /// This method is typically called by [updateChild] but can be called
  /// directly by subclasses that need finer-grained control over creating
  /// elements.
  ///
  /// If the given widget has a global key and an element already exists that
  /// has a widget with that global key, this function will reuse that element
  /// (potentially grafting it from another location in the tree or reactivating
  /// it from the list of inactive elements) rather than creating a new element.
  ///
  /// The `newSlot` argument specifies the new value for this element's [slot].
  ///
  /// The element returned by this function will already have been mounted and
  /// will be in the "active" lifecycle state.
  @protected
  @pragma('vm:prefer-inline')
  Element inflateWidget(Widget newWidget, Object? newSlot) {
    assert(newWidget != null);
    final Key? key = newWidget.key;
    if (key is GlobalKey) {
      final Element? newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        assert(newChild._parent == null);
        assert(() {
          _debugCheckForCycles(newChild);
          return true;
        }());
        newChild._activateWithParent(this, newSlot);
        final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild!;
      }
    }
    /// 惊天秘密!!! 拿到外部传进来的widget(MyApp), 创建widget(MyApp)的element, 并返回。
    final Element newChild = newWidget.createElement();
    /// 惊天秘密!!! 生成newChild后, 立即执行elment的mount方法, 挂载节点。
    newChild.mount(this, newSlot);
    assert(newChild._lifecycleState == _ElementLifecycle.active);
    return newChild;
  }
  
    void markNeedsBuild() {
      assert(_lifecycleState != _ElementLifecycle.defunct);
      if (_lifecycleState != _ElementLifecycle.active)
        return;
      assert(owner != null);
      assert(_lifecycleState == _ElementLifecycle.active);
      
      .......
      
      if (dirty)
        return;
        
     // 记脏, 也就是需要更新
      _dirty = true;
      
     // 可以看到把 element 自身加入到了 [BuildOwner] 中
      owner!.scheduleBuildFor(this);
    }

}

Element的markNeedsBuild方法里面, 执行调用了owner!.scheduleBuildFor(this);方法, 将element对象添加到了owner中。

BuildOwner

接下来, 我们看下BuildOwner的源码:

class BuildOwner {
    
    BuildOwner({ this.onBuildScheduled, FocusManager? focusManager }) :
        focusManager = focusManager ?? (FocusManager()..registerGlobalHandlers());

    /// Adds an element to the dirty elements list so that it will be rebuilt
    /// when [WidgetsBinding.drawFrame] calls [buildScope].
    void scheduleBuildFor(Element element) {
      assert(element != null);
      assert(element.owner == this);

      ......

      if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      // 表明element已经在_dirtyElements列表了
        _scheduledFlushDirtyElements = true;
        onBuildScheduled!();
      }

      // 加入到 dirty 列表
      _dirtyElements.add(element);
      element._inDirtyList = true;
    }


    void buildScope(Element context, [ VoidCallback callback ]) {
           // ...
        // 会先排序 _dirtyElements,排序规则是先判断深度
        // 深度浅的在前,深度深的在后。
        // 深度相同的判断是否有脏标记,有脏标记的在前,没有的在后
        _dirtyElements.sort(Element._sort);
        _dirtyElementsNeedsResorting = false;
        int dirtyCount = _dirtyElements.length;
        int index = 0;
        // 依次遍历 _dirtyElements,调用 rebuild()
        while (index < dirtyCount) {
          // 这里调用 rebuild
          _dirtyElements[index].rebuild();
          index += 1;
          // ...
        }
      }

}

到这里为止, markNeedsBuild(..)会让 element 自身加入到 BuildOwner 的  _dirtyElements 列表中。

根据 scheduleBuildFor 的注释,这个 _dirtyElement 是在 WidgetsBinding.drawFrame 触发 BuildOwner.buildScope(..)的时候消费的。

接下来我们来看下 buildScope(..) 函数,这个函数会先把  _dirtyElements 排下序,排序好之后依次调用 element.rebuild()

buildScope会调用 element 的 rebuild()

现在我们已经知道 Element.markNeedsBuild  element 变脏,然后在下一帧 drawFrame时候 rebuild

StatefulWidget中的setState()

那么 Element.markNeedsBuild 什么时候会触发呢?它触发的地方比较多,但是最常见的地方就是我们 StatefulWidget 中的 setState((){}):

abstract class State<T extends StatefulWidget> {
  @protected
  void setState(VoidCallback fn) {
    //...
    // 这里最后调用了 state 对应的 element,然后 markNeedsBuild
    _element.markNeedsBuild();
  }
}

以上源码的时序图如下:

StatelessWidget创建流程.png

总结

了解了StatelessWidget的源码, 我们可以知道, StatelessWidget实现的build(BuildContext context),这个context就是StatelessElement对象。

StatefulWidget组件中, setState时会触发Element.markNeedsBuild

参考资料

Flutter BuildContext 探究

Builder 构造器与 BuildContext 认知

谈谈 Flutter 的 build