关于flutter:FluttersetState-能在-build-中直接调用吗

2次阅读

共计 2967 个字符,预计需要花费 8 分钟才能阅读完成。

setState() 能在 build() 中间接调用吗?答案是能也不能。

两种状况

来看一段简略的代码:

import 'package:flutter/material.dart';

class TestPage extends StatefulWidget {const TestPage({super.key});

  @override
  State<TestPage> createState() => _State();
}

class _State extends State<TestPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {setState(() {_count++;});

    return Scaffold(
      appBar: AppBar(title: const Text('测试页面'),
      ),
      body: Center(
        child: Text(
          '$_count',
          style: const TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

跑起来后代码不会报错,Text(‘$_count’) 显示后果是 1,看来 build() 调用 setState() 没啥问题呀。小改一下,来看看这个:

class _State extends State<TestPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('测试页面'),
      ),
      body: Center(
        child: Builder(builder: (context) {setState(() {_count++;});

            return Text(
              '$_count',
              style: const TextStyle(fontSize: 24),
            );
          }
        ),
      ),
    );
  }
}

改变次要是在 Text 下面加了一个 Builder,而后把 setState() 放在了 Builder 的 builder 中去调用。运行起来,后果呈现报错了:The following assertion was thrown building Builder(dirty): setState() or markNeedsBuild() called during build. 提醒在 Builder 的 build() 过程中呈现了断言谬误:build() 中不能调用 setState() 或 markNeedsBuild()。

这是什么状况呢,为什么第一种状况下能够在 build() 中调用 setState() 而第二种状况不行?上面来简略地剖析下其中蕴含的原理。

原理剖析

先说一下论断,在 build() 中间接调用 setState() 要满足一个前提条件:

如果以后有组件 A 处于 build() 中,那么 setState() 引起 rebuild 的组件必须是 A 或者 A 的子孙组件,不能是 A 的先人组件。

这是因为组件 build 的程序是从父到子,如果在子组件 build 的过程中执行 setState() 之类会引起父组件的从新 build 那就死循环必定是不行的。

接下来看下 Flutter 源码中是如何判断和管制的。setState() 的外部会调用 _element!.markNeedsBuild()markNeedsBuild() 中有如下代码:

void markNeedsBuild() {
  // ...
  
  // 前半部分,断言从新 build 是否满足下面说的前提。assert(() {if (owner!._debugBuilding) {assert(owner!._debugCurrentBuildTarget != null);
      assert(owner!._debugStateLocked);
      // _debugIsInScope() 用来判断是否满足前提条件。if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) {return true;}
      if (!_debugAllowIgnoredCallsToMarkNeedsBuild) {
        final List<DiagnosticsNode> information = <DiagnosticsNode>[ErrorSummary('setState() or markNeedsBuild() called during build.'),
          // ...
        ];
        // ...
      }
      // ...
    }());
    
  // ...
}

markNeedsBuild() 代码的前半部分有断言来解决是否满足下面说到的前提条件,_debugCurrentBuildTarget 就是以后正处于 build 状态的 element。_debugCurrentBuildTarget() 的内容如下:

bool _debugIsInScope(Element target) {
  Element? current = this;
  while (current != null) {if (target == current) {return true;}
    current = current._parent;
  }
  return false;
}

_debugIsInScope() 中的 this 就是调用 setState() 会引起 rebuild 的组件,target 就是以后正处于 build 的组件。其中的 while 循环会逐渐比对 current 及其父组件是否以后 build 的对象,找到了才会返回 true,否则就是 false。如果是 false,则前面的断言就会呈现谬误:setState() or markNeedsBuild() called during build.

如果以后有组件正在 build 那么决不能引起父组件的 rebuild,咱们来看下后面举例报错的第二种状况。Builder 是 TestPage 的子组件,Builder 的 builder 办法里调用的 setState 是 TestPage 上的,也就是在子组件的 build 过程中使父组件 rebuild 了,那么就会引起断言失败;而第一种状况下是在 TestPage 的 build 过程中调用 setState 使本人从新 rebuild,能够满足论断的前提,所以是能够调用的。

这里咱们能够接着想下在第一种状况下,组件本人的 build 过程中调用了 setState 引起了本人从新 rebuild 的时候不是也会死循环了吗?咱们接着看下 markNeedsBuild() 的后半局部代码,如果断言胜利后前面的逻辑:

void markNeedsBuild() {
  // ...
  // 前半部分是断言。if (dirty) {return;}
  _dirty = true;
  owner!.scheduleBuildFor(this);
}

这里能够看到组件在 build 过程中 markNeedsBuild() 会使组件变为 dirty 状态,这个时候在 build 中间接调用 setState 后发现曾经是 dirty 状态后会间接返回,而不会调度从新 build,所以就没有问题了。

总结

通过以上的剖析咱们晓得了 Flutter 是如何判断如果在 build 过程中间接调用 setState 是否非法的。当然咱们在写代码的时候是不会在 build() 中间接调用 setState 的,理解以上过程更有助于咱们排查问题和学习 Flutter 的运行原理。

正文完
 0