flutter setState图解

1,431 阅读3分钟

一言不合setState!

setState可以分为两个部分:

  1. 将element标脏
  2. 渲染时将所有脏element都rebuild,且将自己的child进行update

先看第一步

去掉assert之后的setState只有两步:

  • 调用放进来的function
  • 将当前element标记needBuild
//class State
@protected
  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element!.markNeedsBuild();
  }

接着看markNeedsBuild,同样去掉assert,同样只有两步:

  • 将dirty置为true
  • 将自己作为参数,调用BuildOwner.scheduleBuildFor
//class Element
void markNeedsBuild() {
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
  }

这个owner从哪来?owner又是啥?scheduleBuildFor是啥作用?,我们看下Element的mount方法:

//class Element
@mustCallSuper
  void mount(Element? parent, dynamic newSlot) {
    _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
      _owner = parent.owner;
    final Key? key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
  }

可以看到入参是parent(即该节点的父节点),newSlot(即该节点即将插入的插槽),mount方法完成了:

  • 链接了当前节点和它的父节点
  • 并且插入插槽
  • 接着承接了父节点传下来的「owner」

找到BuildOwner类的注释:

/// class BuildOwner
/// Manager class for the widgets framework.
///
/// This class tracks which widgets need rebuilding, and handles other tasks
/// that apply to widget trees as a whole, such as managing the inactive element
/// list for the tree and triggering the "reassemble" command when necessary
/// during hot reload when debugging.
///
/// The main build owner is typically owned by the [WidgetsBinding], and is
/// driven from the operating system along with the rest of the
/// build/layout/paint pipeline.
///
/// Additional build owners can be built to manage off-screen widget trees.
///
/// To assign a build owner to a tree, use the
/// [RootRenderObjectElement.assignOwner] method on the root element of the
/// widget tree.

简单点总结就是「BuildOwner主要用于widget的rebuild,并且由WidgetBingding所有」,接下来找到WidgetBinding

/// class WidgetBinding
/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;

    assert(() {
      _debugAddStackFilters();
      return true;
    }());

    // Initialization of [_buildOwner] has to be done after
    // [super.initInstances] is called, as it requires [ServicesBinding] to
    // properly setup the [defaultBinaryMessenger] instance.
    _buildOwner = BuildOwner();
    buildOwner!.onBuildScheduled = _handleBuildScheduled;
    window.onLocaleChanged = handleLocaleChanged;
    window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
    FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
  }
}

可以在initInstances()方法里面看到实例化了BuildOwner

视角继续追踪BuildOwner.scheduleBuildFor方法(即Element.markNeedsBuild里面的主要代码):

	///class BuildOwner
	/// Adds an element to the dirty elements list so that it will be rebuilt
  /// when [WidgetsBinding.drawFrame] calls [buildScope].
  void scheduleBuildFor(Element element) {
    if (element._inDirtyList) {
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }

最后几行代码,才是真的,将element标脏_dirtyElements.add(element);element._inDirtyList = true;

最后一个方法BuildOwner.buildScope,先看关键的注释,其他省略:

	/// Establishes a scope for updating the widget tree, and calls the given
  /// `callback`, if any. Then, builds all the elements that were marked as
  /// dirty using [scheduleBuildFor], in depth order.

机翻:

在一定的范围内更新widget树,并且调用他们的callback,然后将所有通过scheduleBuildFor标记成脏的element通过深度顺序,进行build。

即,是在这里发出了rebuild的指令

我们再看下源码:

void buildScope(Element context, [ VoidCallback? callback ]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        Element? debugPreviousBuildTarget;
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        try {
          _dirtyElements[index].rebuild();
        }finally {
      for (final Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
    }

保留了关键的代码块,可以看到在buildScope中,遍历了脏的eleement,调用他们的rebuild方法,然后mark回干净。

小结:在WidgetsBinding初始化时,会创建一个BuildOwner对象。每一个Element在挂载上树时(即调用mount方法),都会将上述提到的BuildOwner传递给自己的子节点,每次setState调用的时候,element就把自己add进BuildOwner._dirtyElements中,等下一帧渲染时,BuildOwner.buildScope就会遍历调用所有脏的Element.rebuild

第二步:

setState是如何调用子节点的rebuild的?

Element.rebuild方法源码:

	/// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been
  /// called to mark this element dirty, by [mount] when the element is first
  /// built, and by [update] when the widget has changed.
  void rebuild() {
    if (_lifecycleState != _ElementLifecycle.active || !_dirty)
      return;
    Element? debugPreviousBuildTarget;
    performRebuild()
  }

只有一个关键代码,调用performRebuild,接着看performRebuild源码。

StatefulWidget和StatelessWidget都override了createElement方法,分别返回StatefulElement和StatelessElement,我们看他们的父类ComponentElement,ComponentElement.performRebuild方法:

	/// 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
  void performRebuild() {
    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);
    Widget? built;
    built = build();
    debugWidgetBuilderValue(widget, built);
    _child = updateChild(_child, built, slot);
    if (!kReleaseMode && debugProfileBuildsEnabled)
      Timeline.finishSync();
  }
  • 调用了build方法,获得了built
  • 调用updateChild方法,更新_child

接着看Element.updateChild方法

	/// 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
  Element? updateChild(Element? child, Widget? newWidget, dynamic 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.
      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);
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }

    return newChild;
  }

这个方法的备注非常详细。

  • 当newWidget==null&&child==null时,return null。因为该组件原本为null,然后执行build之后还是生成null,那只能返回null
  • 当newWidget==null&&child≠null时,将当前child的自己deactivate掉。因为执行build生成了null,要替换掉原来≠null的child,那么就要把child以及它的所有子节点都要失效否则会一直占着
  • 当newWidget≠null&&child==null时,那就通过inflateWidget获取newWidget的element,然后返回该element。因为执行build生成的≠null,但是原本child为null,自然使用newWidget的。
  • 当newWidget≠null&&child≠null时,需要让child的配置更新为当前的新widget。如果新widget可以用于更新child,那么,更新。否则,删除孩子,使用新配置生成新孩子。(是否可以更新child,由Widget.canUpdate方法来判断)

可以看到在Element.updateChild中,关键的点在于child.update(newWidget);也就是Element.update由于Elementupdate方法是空的,我们直接看StatefulElement.update源码:

void update(StatefulWidget newWidget) {
    super.update(newWidget);
    final StatefulWidget oldWidget = state._widget!;
    // We mark ourselves as dirty before calling didUpdateWidget to
    // let authors call setState from within didUpdateWidget without triggering
    // asserts.
    _dirty = true;
    state._widget = widget as StatefulWidget;
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final dynamic debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
    } finally {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
    }
    rebuild();
  }

可以看到StatefulElement.update里面也调用了rebuild。到这里,一切都能串起来了。

首先是element挂载(mount时,获取parent传下来的buildOwner)到树上,然后在setState的时候,把自己标记脏(BuildOwner._dirtyElements.add),接着是等待下一次渲染来临时,BuildOwner会对_dirtyElements中每一个element都调用rebuild方法,而rebuild方法的关键,就是Element.performRebuild。在Element.performRebuild里面,更新了自己(调用了自己的build),还更新了自己的孩子(调用了Element.updateChild),接着Element.updateChild的核心是child.update。最后查看StatefulElement.update方法,其实最后还是调用了Element.rebuild

一句话总结就是,父组件setState时,既更新自己,又更新孩子,而孩子又执行和父组件一样的动作,一直往树的最底层执行。所以,在做setState时,如果能降低刷新的成本,会更好,比如使用局部刷新组件ValueNotifier等。

最后附上流程图

image.png

图例对应的源代码:

class ColorBox extends StatefulWidget {
  const ColorBox({Key? key, required this.color}) : super(key: key);
  final Color color;

  @override
  State<ColorBox> createState() => _ColorBoxState();
}

class _ColorBoxState extends State<ColorBox> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        count++;
        setState(() {});
      },
      child: Container(
        color: widget.color,
        width: 100,
        height: 100,
        child: Center(
          child: Text(
            count.toString(),
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {

  List<Widget> widgetList = [
    ColorBox(
      color: Colors.yellow,
    ),
    ColorBox(color: Colors.blue)
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("key test"),
        ),
        body: Container(
          padding: EdgeInsets.all(20),
          child: Column(
            children: [...widgetList],
          ),
        ),
        floatingActionButton: GestureDetector(
          onTap: () {
            widgetList = widgetList.reversed.toList();
            setState(() {});
          },
          child: Icon(
            Icons.add,
          ),
        ));
  }
}

图例对应的UI: image.png

第一次写,有疑问或错误的地方欢迎指出

参考:juejin.cn/post/690599…