Flutter 笔记 | Flutter 核心原理(七)The key is the key!

news2024/10/6 12:32:29

题外话

本文这个标题其实让我想起了电影《蝙蝠侠大战超人:正义黎明》中的一句话,在该片中,从未来穿越回来的闪电侠对蝙蝠侠发出警告: It’s Lois! She’s the key!

在这里插入图片描述

【布鲁斯,是露易丝!露易丝是关键!】
【你是对的,你对他的看法一直都是对的……】
【我来得太早了吗?我来得太早了……】
【敬畏他!敬畏他!】
【你一定要找到我们!你必须要找到我们!……】

这是闪电侠借助神速力从未来的某个时间点穿越回来,他是来警告蝙蝠侠的。从前几句话可以推断出,在未来的时间线里,因为露易丝出事了,超人可能成为了某种可怕的存在,正义联盟根本没人能拦得住他,所以闪电侠只能回到过去告诉战友们提前做好准备,不能让超人堕落成这种样子。不料他穿越回来的时候发现蝙蝠侠还不认识他,所以他就知道自己来得太早了(因为这个时间点正义联盟还没有建立)。他知道只有正义联盟有能力保护好超人和他的家人,所以他最后恳求蝙蝠侠一定要找到他们并组建正义联盟。这段剧情应该是改编自漫画和游戏《不义联盟:人间之神》。

在这里插入图片描述

剧情回忆: 在电影《蝙蝠侠大战超人:正义黎明》中,正当蝙蝠侠暴揍大超之时,超人提到了玛莎,他让蝙蝠侠去救玛莎,愤怒的蝙蝠侠质问超人为什么要提起这个名字,这时一旁的超人女友及时赶到,她告诉蝙蝠侠那是他母亲的名字。因为蝙蝠侠的妈妈也叫玛莎,由于蝙蝠侠当年自己还是年幼的时候因为没能救下自己妈妈,而亲眼目睹了自己的母亲玛莎·韦恩在自己面前遭遇劫匪杀害,所以当超人叫蝙蝠侠救玛莎时,就想到了自己妈妈。而这时,蝙蝠侠也想起了之前闪电侠对他的警告,顿时恍然大悟,这才明白过来,自己差点犯下大错,假如他没能救出超人的母亲玛莎,那么之后超人将会黑化,世界就会变成蝙蝠侠之前梦境中的那般由黑化的超人所统治,世上再也无人能够阻止超人。

好了,下面开始进入本文正题。


为什么 key 非常重要

我们先看一个例子,现在有一个自定义的 FancyButton 组件,代码如下:

import 'dart:math';
import 'package:flutter/material.dart';

class FancyButton extends StatefulWidget {
  const FancyButton({Key? key, required this.onPressed, required this.child})
      : super(key: key);

  final VoidCallback onPressed;
  final Widget child;

  @override
  State<FancyButton> createState() => _FancyButtonState();
}

class _FancyButtonState extends State<FancyButton> {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.all(_getColors()),
      ),
      onPressed: widget.onPressed,
      child: Padding(
        padding: const EdgeInsets.all(30),
        child: widget.child,
      ),
    );
  }

  // 以下代码用于生成随机背景色,以确保每个Button的背景色不同
  Color _getColors() {
    return _buttonColors.putIfAbsent(this, () => _colors[next(0, 5)]); // map中不存在就放入map, 否则直接返回
  }

  final Map<_FancyButtonState, Color> _buttonColors =
      {}; // 注意,这里使用了一个Map保存当前State对应的Color
  final Random _random = Random();
  int next(int min, int max) => min + _random.nextInt(max - min);
  final List<Color> _colors = [
    Colors.blue,
    Colors.green,
    Colors.orange,
    Colors.purple,
    Colors.amber
  ];
}

FancyButton 很简单,只是包装了一下ElevatedButton组件,它在内部会自己管理背景色,为了使每个 FancyButton 的背景色不同,这里使用了随机数来获取一个随机的颜色,并且使用Map来缓存颜色,这样下次可直接从Map中获取而不用每次计算。

下面基于 Flutter 默认的计数器示例应用使用上面的 FancyButton 进行改造,我们在页面上添加两个 FancyButton 按钮,点击的时候分别用来加减counter值,另外我们添加一个“Swap”按钮,点击的时候可以交换页面上的两个 FancyButton,页面要实现的静态效果如下:

在这里插入图片描述

实现代码如下:

class FancyButtonPage extends StatefulWidget {
  const FancyButtonPage({Key? key}) : super(key: key);

  @override
  State<FancyButtonPage> createState() => _FancyButtonPageState();
}

class _FancyButtonPageState extends State<FancyButtonPage> {
  int counter = 0;
  bool _reversed = false;

  void resetCounter() {
    setState(() => counter = 0);
    swapButton();
  }

  void swapButton() {
    setState(() => _reversed = !_reversed);
  }

  @override
  Widget build(BuildContext context) {
    final incrementButton = FancyButton(
        onPressed: () => setState(() => counter++),
        child: const Text(
          "Increment",
          style: TextStyle(fontSize: 20),
        ));
    final decrementButton = FancyButton(
        onPressed: () => setState(() => counter--),
        child: const Text(
          "Decrement",
          style: TextStyle(fontSize: 20),
        ));
    List<Widget> buttons = [incrementButton, decrementButton];
    if (_reversed) {
      buttons = buttons.reversed.toList();
    }
    return Scaffold(
      appBar: AppBar(title: const Text("FancyButton")),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const SizedBox(height: 30),
          Text("$counter", style: const TextStyle(fontSize: 22)),
          const SizedBox(height: 30),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: buttons,
          ),
          const SizedBox(height: 30),
          ElevatedButton(
              onPressed: resetCounter, child: const Text("Swap Button"))
        ],
      ),
    );
  }
}

运行后测试效果:

在这里插入图片描述

这时你会发现一个奇怪的现象:当按下交换按钮时,上面两个按钮确实会交换位置,但是只有按钮的文字被交换了,背景色却没有变,而点击按钮时对应的功能正常。也就是说,左边的按钮与交换前的背景颜色相同,即使按钮本身是不同的。这是为什么呢?这明显不是我们期望的,我们期望的是两个按钮可以“真正的”对调位置。但这只换了一半是几个意思?

Element Tree 和 State

在解释这个问题之前我们先来了解一下关于 Element TreeState 的几个概念:

  • State对象实际上是由 Element Tree 管理的。(确切的说是由StatefulElement创建并持有)

  • State 对象是长期存在的。与Widget不同,每当Widget重新渲染时,它们都不会被销毁和重新构建。

  • State 对象是可以被重用的

  • Element 引用了Widget。同时State 对象也会保存对Widget的引用,但这种持有不是永久的。

Element很简单,因为它们只包含元信息和对Widget的引用,但它们也知道如果一旦Widget发生更改,自己该如何更新对不同Widget的引用。

在这里插入图片描述
在这里插入图片描述

当Flutter决定在调用build方法进行重建和重新渲染时,一个element将会查找与它引用的前一个Widget完全相同的位置处的Widget

然后,它将决定Widget是否相同(若相同,它不需要做任何事情),或者Widget是否发生了变化,或者它是一个完全不同的Widget(若完全不同,它需要重新被渲染)。

但问题是Element是根据什么来判断更新的内容,它们只查看Widget上的几个属性:

  • 在运行时的确切类型(runtimeType)

  • 一个Widgetkey(如果有的话)

其实也就是 Flutter 执行 Build 重建流程中Element源码中 updateChild() 方法的逻辑:

// flutter/lib/src/widgets/framework.dart
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) { // Element
    if (newWidget == null) {
      if (child != null) {
        deactivateChild(child);
      }
      return null;
    } 
    final Element newChild;
    if (child != null) {
       ...
      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);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else { 
      newChild = inflateWidget(newWidget, newSlot);
    }  
    return newChild;
  }

该方法主要逻辑总结如下:

在这里插入图片描述

其中的Widget.canUpdate()方法的源码如下:

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

本例中交换的两个FancyButton对象是内存地址完全不同的两个实例对象,因此肯定会直接执行上面updateChild() 方法逻辑的情况2,排除情况1。也就是会执行 canUpdate 方法的逻辑判断。

我们知道,Widget Tree 只是 Element Tree 的映射,它只提供描述UI树的配置信息,而在本例中,这些Widget的颜色不在Widget的配置中;它们保存在对应WidgetState对象中。Element指向更新的Widget并显示新的配置,但仍然保留原始State对象。因此,当Element看到了插入到UI树的这个地方的新Widget,然后它就想:“emm,没有key,运行时类型仍然是FancyButton,所以我不需要更新我的引用。这是匹配我的State对象的正确的Widget。”

在这里插入图片描述

Widget keys

问题分析清楚了,那么解决这个问题的最简单的方案就是: key。终于来到了本文的正题,在处理集合中的Widget时,为它们提供key可以帮助Flutter了解相同类型的两个Widget何时实际上不同。这对于多子组件的Widget特别有用。通常,就像在我们上面的示例中一样,当一行或列中的所有子节点都是同一类型时,最好给Flutter一个额外的信息来区分这些子节点。

下面我们用UniqueKey来解决这个问题:

class FancyButtonPage extends StatefulWidget {
  const FancyButtonPage({Key? key}) : super(key: key);

  
  State<FancyButtonPage> createState() => _FancyButtonPageState();
}

class _FancyButtonPageState extends State<FancyButtonPage> {
  int counter = 0;
  bool _reversed = false;
  final List _buttonKeys = [UniqueKey(), UniqueKey()]; // add key

  void resetCounter() {
    setState(() => counter = 0);
    swapButton();
  }

  void swapButton() {
    setState(() => _reversed = !_reversed);
  }

  
  Widget build(BuildContext context) {
    final incrementButton = FancyButton(
        key: _buttonKeys.first, // add key
        onPressed: () => setState(() => counter++),
        child: const Text(
          "Increment",
          style: TextStyle(fontSize: 20),
        ));
    final decrementButton = FancyButton(
        key: _buttonKeys.last, // add key
        onPressed: () => setState(() => counter--),
        child: const Text(
          "Decrement",
          style: TextStyle(fontSize: 20),
        ));
    List<Widget> buttons = [incrementButton, decrementButton];
    if (_reversed) {
      buttons = buttons.reversed.toList();
    }
    return Scaffold(
      appBar: AppBar(title: const Text("FancyButton")),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const SizedBox(height: 30),
          Text("$counter", style: const TextStyle(fontSize: 22)),
          const SizedBox(height: 30),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: buttons,
          ),
          const SizedBox(height: 30),
          ElevatedButton(
              onPressed: resetCounter, child: const Text("Swap Button"))
        ],
      ),
    );
  }
}

重新运行后效果如下:

在这里插入图片描述

可以看到现在符合我们的预期了,按钮的背景和文字都会交换,功能也正常。

这下,我们终于知道在Flutter中创建Widget类时,编写构造函数的时候,为啥编译器总是提醒我们要添加一个名为key的参数了:

在这里插入图片描述

如果你不加,它会不厌其烦的给出提示和警告来提醒你。另外,我们发现这个key推荐的规范写法是定义为可空类型的,这意味着我们创建Widget组件时,并不总是需要传入一个key。

在Flutter中,Key一般用来做唯一标识,所以Key是不能重复使用的。前面提到,Element在更新的时候,决定要不要复用主要是通过判断组件的类型 && key值是否一致。因此,当各组件的类型不同的时候,类型已经足够用来区分不同的组件了,此时我们可以不必使用key但是如果同时存在多个同一类型的控件的时候,此时类型已经无法作为区分的条件了,我们就需要使用到key。

LocalKey 和 GlobalKey

Flutter 中的 key 主要分为两大类: LocalKeyGlobalKey

  • 局部 Key(LocalKey)ValueKey、ObjectKey、UniqueKey
  • 全局 Key(GlobalKey)GlobalKey、GlobalObjectKey

1. Global Keys

  • GlobalKey 用于管理状态和在 Widget 树中移动 Widget。例如,您可以在一个Widget中使用一个GlobalKey,它将显示一个复选框,并在多个页面上使用该Widget。这个key告诉框架使用该Widget的相同实例。因此,当您导航到不同的页面以查看该复选框时,它的选中状态将保持不变。如果你在A页选中了它,它会在B页也被选中。

  • GlobalObjectKey:全局 Object key,可以根据对象生成全局key,和ObjectKey有点类似。

使用 GlobalKey 必须保证全局唯一不重复,需要留意的一点的是使用它也有缺点,那就是性能损耗,因为它需要总是保持全局可用状态占有资源。

2. Local Keys

  • ValueKey<T>当要添加key的对象具有某种不变的唯一属性时,ValueKey是最好的选择。例如,在一个 todo 列表应用程序中,每个显示 todo 的 Widget 可能都有一个常量和唯一的 todo 文本。或者说后端接口返回给我们的业务类中有某种唯一的业务id属性,就可以拿来使用其创建ValueKey<T>

  • ObjectKey使用对象创建key,当对象具有相同的类型,但其属性值不同时,适合使用ObjectKey。例如在一个电子商务应用程序中考虑一个名为“产品”的对象:两个产品可以有相同的标题(两个不同的卖家可以出售球芽甘蓝)。一个卖家可以有多种产品。产品的独特之处在于产品名称和卖方名称的组合。所以,key是一个传递到ObjectKey中的文字对象。例如:
    在这里插入图片描述

  • UniqueKey: 如果要向集合的子项添加key,而子项直到创建后才知道它们的值,则可以使用UniqueKey。或者说当我们不知道如何指定ValueKeyObjectKey的时候就可以直接使用UniqueKey

  • PageStorageKey: 这是一个用于存储页面信息的专门键,如滚动位置。

使用 GlobalKey 解决状态丢失问题

LocalKeyGlobalKey 另一个比较明显的区别就是使用 LocalKey 在屏幕旋转之后页面的状态会丢失,例如以下代码:

import 'package:flutter/material.dart';

class LocalKeyPage extends StatefulWidget {
  const LocalKeyPage({Key? key}) : super(key: key);

  
  State<LocalKeyPage> createState() => _LocalKeyPageState();
}

class _LocalKeyPageState extends State<LocalKeyPage> {
  List<Widget> list = [
    const Box(
      key: ValueKey('1'),
      color: Colors.red,
    ),
    Box(
      key: UniqueKey(), //唯一值 每次运行的时候会随机生成
      color: Colors.yellow,
    ),
    const Box(key: ObjectKey(Box(color: Colors.blue)), color: Colors.blue)
  ];
  
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: () =>setState(() => list.shuffle()), //shuffle:打乱list元素的顺序
      ),
      appBar: AppBar(title: const Text('LocalKey')),
      body: Center(
        child: MediaQuery.of(context).orientation==Orientation.portrait?Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: list,
        ):Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: list,
        ),
      ),
    );
  }
}

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

  
  State<Box> createState() => _BoxState();
}

class _BoxState extends State<Box> {
  int _count = 0;
  
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: ElevatedButton(
        style: ButtonStyle(
            backgroundColor: MaterialStateProperty.all(widget.color)),
        onPressed: () {
          setState(() {
            _count++;
          });
        },
        child: Text(
          "$_count",
          style: Theme.of(context).textTheme.headline2,
        ),
      ),
    );
  }
}

上面代码中在设备竖屏时使用一个Column组件来展示列表,而在设备横屏时使用一个 Row组件来展示列表。运行效果如下:

在这里插入图片描述

使用 GlobalKey 则可以避免这个问题,修改代码如下:

class GlobalKeyPage extends StatefulWidget {
  const GlobalKeyPage({Key? key}) : super(key: key);

  
  State<GlobalKeyPage> createState() => _GlobalKeyPagePageState();
}

final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();

class _GlobalKeyPagePageState extends State<GlobalKeyPage> {
  List<Widget> list = [];

  
  void initState() {
    super.initState();
    list = [
      Box(
        key: _globalKey1,
        color: Colors.red,
      ),
      Box(
        key: _globalKey2,
        color: Colors.yellow,
      ),
      Box(key: _globalKey3, color: Colors.blue)
    ];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: () => setState(() => list.shuffle()), // shuffle:打乱list元素的顺序
      ),
      appBar: AppBar(title: const Text('LocalKey')),
      body: Center(
        child: MediaQuery.of(context).orientation == Orientation.portrait
            ? Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: list,
              )
            : Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: list,
              ),
      ),
    );
  }
}

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

  
  State<Box> createState() => _BoxState();
}

class _BoxState extends State<Box> {
  int _count = 0;
  
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: ElevatedButton(
        style: ButtonStyle(
            backgroundColor: MaterialStateProperty.all(widget.color)),
        onPressed: () {
          setState(() {
            _count++;
          });
        },
        child: Text(
          "$_count",
          style: Theme.of(context).textTheme.headline2,
        ),
      ),
    );
  }
}

运行效果:

在这里插入图片描述

之所以 GlobalKey 在屏幕旋转后依然能够保持状态,是因为 Flutter 对使用 GlobalKey 创建的Widget所对应的Element节点重新挂载到 Element Tree 时进行了复用逻辑(后文会分析)。

使用 GlobalKey 来获取 State 对象

在 Flutter 中要获取 StatefulWidget 组件的 State 对象,一种方法是使用 context.findAncestorStateOfType<T>() 方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。例如,下面是实现打开 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

注意:

  • 一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。

  • 但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个 of 静态方法 来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见。

所以,上面示例中的获取方式 Flutter SDK 也提供了一个Scaffold.of方法,我们可以直接使用它:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state = Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
    },
    child: Text('打开抽屉菜单2'),
  );
}),

又比如我们想显示 snackbar 的话可以通过下面代码调用:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("我是SnackBar")),
      );
    },
    child: Text('显示SnackBar'),
  );
}),

在 Flutter 中另一种获取 StatefulWidget 组件的 State 对象的方法就是使用 GlobalKey 了,我们可以通过 globalKey.currentState 来获取子组件的状态,执行子组件的方法,同样的,globalKey.currentWidget可以获取子组件的Widget对象,而 globalKey.currentContext可以获取子组件的context对象。下面是一个使用示例:

class GlobalKeyExample extends StatefulWidget {
  const GlobalKeyExample({Key? key}) : super(key: key);

  
  State createState() => _GlobalKeyExampleState();
}

final GlobalKey<GlobalKeyTestState> _globalKey =
    GlobalKey<GlobalKeyTestState>();

class _GlobalKeyExampleState extends State<GlobalKeyExample> {
  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        GlobalKeyTest(key: _globalKey), // 子组件指定 key 使用 globalKey
        ElevatedButton(
          child: const Text("Add"),
          onPressed: () {
            // 获取子组件 widget 的 state 对象,并执行其方法
            _globalKey.currentState?.addCount(20);

            // GlobalKeyTest wg = _globalKey.currentWidget as GlobalKeyTest;
            
            // _globalKey.currentContext!.findRenderObject();
            
            // 系统暴露state对象的范例
            // ScaffoldMessenger.of(context).showSnackBar(
            //   const SnackBar(content: Text("这是SnackBar")),
            // );
          },
        ),
      ],
    );
  }
}
// 子组件
class GlobalKeyTest extends StatefulWidget {
  const GlobalKeyTest({Key? key}) : super(key: key);
  
  GlobalKeyTestState createState() => GlobalKeyTestState();
}

class GlobalKeyTestState extends State<GlobalKeyTest> {
  int count = 0;

  addCount(int x) {
    setState(() {
      count = count + x;
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text(count.toString()),
      ],
    );
  }
}

运行效果:

在这里插入图片描述

Key 源码分析

Key在Flutter的源码中几乎无处不在,但在日常开发中鲜有涉及。用官方的话来说,Key的使用场景是:你需要将一系列类型相同并持有不同状态(State)的Widget进行增加、移除和排序

Key主要分为GlobalKeyLocalKey,关键类及其关系如图8-3所示。

图8-3 Key关键类及其关系

接下来将从源码的角度一窥Key的作用及原理。

GlobalKey 源码分析

GlobalKey 的注册代码如下:

// 代码清单5-3 flutter/packages/flutter/lib/src/widgets/framework.dart
 // Element
void mount(Element? parent, dynamic newSlot) {
  _parent = parent; // 对根节点而言,parent为null
  _slot = newSlot;
  _lifecycleState = _ElementLifecycle.active; // 更新状态
  _depth = _parent != null ? _parent!.depth + 1 : 1; // 树的深度
  if (parent != null) _owner = parent.owner; // 绑定BuildOwner
  final Key? key = widget.key;  // Global Key 注册 
  if (key is GlobalKey) { key._register(this); } // 见代码清单8-15
  _updateInheritance();  
}

_register的逻辑如代码清单8-15所示,即当前Element会被加入一个全局字段_registry中。

// 代码清单8-15 flutter/packages/flutter/lib/src/widgets/framework.dart
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}; // 全局注册表
Element? get _currentElement => _registry[this];
void _register(Element element) {
  _registry[this] = element; // this即GlobalKey子类的实例
}
void _unregister(Element element) {
  if (_registry[this] == element) _registry.remove(this); // 移除注册
}

那么,GlobalKey又是如何被使用的?在代码清单5-8中,当解析一个新的Widget并创建Element时会触发GlobalKey的逻辑,完整逻辑如代码清单8-16所示。

// 代码清单5-8 flutter/packages/flutter/lib/src/widgets/framework.dart
Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Key? key = newWidget.key;
  if (key is GlobalKey) { ...... } // 见代码清单8-16
  final Element newChild = newWidget.createElement(); // 创建对应的Element
  newChild.mount(this, newSlot); // 由对应的Element实例继续子节点的挂载
  return newChild;
}
// 代码清单8-16 flutter/packages/flutter/lib/src/widgets/framework.dart
Element inflateWidget(Widget newWidget, dynamic newSlot) { // 见代码清单5-8
  assert(newWidget != null);
  final Key? key = newWidget.key;
  if (key is GlobalKey) { // 当前Widget含有配置Key信息
    final Element? newChild = _retakeInactiveElement(key, newWidget); // 见代码清单8-17
    if (newChild != null) { // 若能找到Key对应的Element,则复用
      newChild._activateWithParent(this, newSlot); // 见代码清单8-19
      // 得到目标Element,基于它进行更新
      final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
      assert(newChild == updatedChild); // 检查确实是同一个Element对象
      return updatedChild!; 
    } // 如果找不到,仍会进入下面的逻辑,新建一个Element节点并挂载
  } // if
  final Element newChild = newWidget.createElement();
  newChild.mount(this, newSlot);  
  return newChild;
}

由以上逻辑可知,当新的Widget存在GlobalKey时,会尝试通过_retakeInactiveElement获取其对应的Element对象并复用;否则会创建一个新的Element实例并挂载到Element Tree中。

首先分析Element对象的取出逻辑,如代码清单8-17所示。

// 代码清单8-17 flutter/packages/flutter/lib/src/widgets/framework.dart
Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
  final Element? element = key._currentElement; // 即key._registry[this]
  if (element == null) return null;
  if (!Widget.canUpdate(element.widget, newWidget)) // 见代码清单5-48
    return null; // 正常情况下,Key相同的Widget,其类型应该相同
  final Element? parent = element._parent;
  if (parent != null) { // 从原来的位置卸载此Element,即从Element Tree中移除
    parent.forgetChild(element); // 登记到_forgottenChildren字段
    parent.deactivateChild(element);
  }
  assert(element._parent == null);
  owner!._inactiveElements.remove(element); // 移除,避免被finalizeTree方法清理
  return element;
}
 // MultiChildRenderObjectElement
void forgetChild(Element child) {
  _forgottenChildren.add(child); // 用于代码清单8-21的相关逻辑
  super.forgetChild(child);
}

以上逻辑首先取出当前Key对应的Element对象,然后将其从原来的节点卸载,一般是同一轮 Build 流程中被复用的节点尚未被遍历到,但GlobalKey的复用已经触发的结果,最后将其从_inactiveElements列表中移除,避免在清理阶段被回收。

每当一个节点从Element Tree中移除时,其就会被加入_inactiveElements列表,如代码清单8-18所示。

// 代码清单8-18 flutter/packages/flutter/lib/src/widgets/framework.dart

void deactivateChild(Element child) { 
  child._parent = null;
  child.detachRenderObject(); // 从Render Tree中移除对应节点
  owner!._inactiveElements.add(child); // 登记该节点,如果在清理阶段该节点仍在本列表中,则清理释放
}

在代码清单8-16的inflateWidget方法中,当取出可复用的Element对象后,需要将其重新挂载到Element Tree,该逻辑通过_activateWithParent方法实现,如代码清单8-19所示。

// 代码清单8-19 flutter/packages/flutter/lib/src/widgets/framework.dart
void _activateWithParent(Element parent, dynamic newSlot) {
  assert(_lifecycleState == _ElementLifecycle.inactive); // 状态检查,只有inactive节点才会触发
  _parent = parent; // 更新相关成员字段
  _updateDepth(_parent!.depth);
  _activateRecursively(this); // 递归调用每个Element子节点的activate方法
  attachRenderObject(newSlot); // 更新Render Tree
  assert(_lifecycleState == _ElementLifecycle.active); // 状态检查
}
static void _activateRecursively(Element element) {
  assert(element._lifecycleState == _ElementLifecycle.inactive);
  element.activate(); // 见代码清单8-20,此时会触发_lifecycleState的更新
  assert(element._lifecycleState == _ElementLifecycle.active);
  element.visitChildren(_activateRecursively);
}

以上逻辑主要是初始化当前Element节点的相关字段,让其对应Element Tree中的新位置。最后递归调用每个子节点的activate方法,如代码清单8-20所示。

// 代码清单8-20 flutter/packages/flutter/lib/src/widgets/framework.dart

void activate() {
  final bool hadDependencies = // 是否存在依赖,详见8.2节
    (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfied
    Dependencies;
  _lifecycleState = _ElementLifecycle.active; // 更新状态
  _dependencies?.clear(); // 清理原来的依赖
  _hadUnsatisfiedDependencies = false;
  _updateInheritance(); // 更新可用依赖集合,见代码清单8-14
  if (_dirty) owner!.scheduleBuildFor(this); // 如有必要,请求刷新
  if (hadDependencies) didChangeDependencies(); // 通知依赖发生变化,见代码清单8-13
}

_hadUnsatisfiedDependencies字段表示当前依赖未被处理,因为找不到对应类型的InheritedElement。当Element被重新挂载到Element Tree时,如果存在依赖的变化,则最终会调用didChangeDependencies,对StatefulElement来说,会触发State的对应生命周期回调。

Element节点被彻底卸载时,如代码清单8-8所示,会完成GlobalKey的清理工作。

// 代码清单8-8 flutter/packages/flutter/lib/src/widgets/framework.dart
 // StatefulElement 
void unmount() {
  super.unmount();
  state.dispose(); // 触发dispose回调
  state._element = null;
}
 
void unmount() { // Element
  final Key? key = _widget.key;
  if (key is GlobalKey) {
    key._unregister(this); // 取消key的注册
  }
  _lifecycleState = _ElementLifecycle.defunct;
}

LocalKey 源码分析

相较于GlobalKeyLocalKey的生效范围只在同一个Element节点的子节点之间,因而其逻辑也更加隐晦。不会像GlobalKey那样“明目张胆”地存在于Build流程中。由于LocalKey作用的范围是节点下面的各个子节点,所以其逻辑必然和MultiChildRenderObjectElement这个Element的子类有关系,MultiChildRenderObjectElement的子节点更新逻辑如代码清单8-21所示。

// 代码清单8-21 flutter/packages/flutter/lib/src/widgets/framework.dart
 // MultiChildRenderObjectElement
void update(MultiChildRenderObjectWidget newWidget) {
  super.update(newWidget); // 见代码清单5-49
  assert(widget == newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren: 
    _forgottenChildren);
  _forgottenChildren.clear(); // 本次更新结束,重置
}

以上逻辑主要调用RenderObjectElementupdateChildren方法,如代码清单8-22所示。其中,_forgottenChildren字段表示的是因为被GlobalKey使用而排除在LocalKey的复用之外的节点,而_forgottenChildren列表的注册逻辑在代码清单8-17的forgetChild方法中,由MultiChildRenderObjectElement实现。

updateChildren方法将开始真正的子节点更新逻辑,如代码清单8-22所示。

// 代码清单8-22 flutter/packages/flutter/lib/src/widgets/framework.dart
List<Element> updateChildren(List<Element> oldChildren,
    List<Widget> newWidgets, { Set<Element>? forgottenChildren }) {
  Element? replaceWithNullIfForgotten(Element child) { // 被GlobalKey索引的节点返回null
    return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
  } // GlobalKey的优先级高于LocalKey,所以这里返回null,避免在两处复用
  int newChildrenTop = 0; // 新Element列表的头部索引
  int oldChildrenTop = 0; // 旧Element列表的头部索引
  int newChildrenBottom = newWidgets.length - 1;  // 新Element列表的尾部索引
  int oldChildrenBottom = oldChildren.length - 1;  // 旧Element列表的尾部索引
  final List<Element> newChildren = oldChildren.length == newWidgets.length ?
      oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance, 
      growable: false);
  Element? previousChild; // 见代码清单 8-23 ~ 代码清单8-27
  return newChildren;
}

updateChildren的主要职责是根据旧的Element子节点列表oldChildren和新的Widget子节点列表newWidgets来更新当前Element节点的子树,即newChildren。当新旧子节点的结束数目相同时会直接基于原来的列表更新,否则会新建一个列表。这里之所以只在长度相等时才复用原来的列表,主要是因为更新算法的机制不适合处理长度不等的情况,与其增加逻辑的复杂度,不如直接新建一个列表。下面以图8-4的过程为例详细分析,这里为了方便演示,虽然新旧列表长度相同,仍然分开表示。

图8-4 updateChildren示意

下面正式分析updateChildren方法的更新逻辑,图8-4中第1阶段如代码清单8-23所示。

// 代码清单8-23 flutter/packages/flutter/lib/src/widgets/framework.dart
// 更新两个列表的头部索引和尾部索引,分别定位到第1个不可复用的Element节点
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= 
    newChildrenBottom)) {
  final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
  final Widget newWidget = newWidgets[newChildrenTop];
  assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active); 
  if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break; 
  final Element newChild = // 完成Element节点的更新
    updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop, 
    previousChild))!;
  assert(newChild._lifecycleState == _ElementLifecycle.active);
  newChildren[newChildrenTop] = newChild; // 加入newChildren列表
  previousChild = newChild;
  newChildrenTop += 1;
  oldChildrenTop += 1; // 处理下一个
}
// 更新尾部索引,但是不加入newChildren列表,逻辑大致同上
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= 
    newChildrenBottom)) {
  final Element? oldChild = replaceWithNullIfForgotten(oldChildren
     [oldChildrenBottom]);
  final Widget newWidget = newWidgets[newChildrenBottom];
  assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
  if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break;
  oldChildrenBottom -= 1;
  newChildrenBottom -= 1; // 只更新索引
}

第 1 阶段,新旧列表的头部指针会同步扫描,可以直接基于Widget更新的节点完成更新;尾部索引同理进行扫描,但不会直接更新,而只是记录位置。这里之所以不直接更新是为了保证执行的顺序,否则在输出日志等场景下会变得非常不可控。

经过第 1 阶段之后,剩下未扫描的节点在顺序上已经无法对应。在图8-4所示的第 2 阶段中将扫描这些节点,并记录有LocalKey的节点,如代码清单8-24所示。

// 代码清单8-24 flutter/packages/flutter/lib/src/widgets/framework.dart
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
  oldKeyedChildren = <Key, Element>{};
  while (oldChildrenTop <= oldChildrenBottom) { // 开始扫描oldChildren的剩余节点
    final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
    assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
    if (oldChild != null) { // 没有被GlobalKey使用
      if (oldChild.widget.key != null) // 存在Key
        oldKeyedChildren[oldChild.widget.key!] = oldChild; // 记录,以备复用
      else
        deactivateChild(oldChild); // 直接移出Element Tree
    }
    oldChildrenTop += 1;
  } // while
}

以上逻辑遍历oldChildren剩下的节点,如果replaceWithNullIfForgotten返回不为null,说明没有被GlobalKey使用,那么LocalKey可以将其加入自己的临时索引oldKeyedChildren

在图8-4所示的第 3 阶段更新newChildren的剩余元素,如果自身的Key可以在oldKeyedChildren中找到对应的索引,则直接复用,如代码清单8-25所示。

// 代码清单8-25 flutter/packages/flutter/lib/src/widgets/framework.dart
while (newChildrenTop <= newChildrenBottom) { // 还有Widget节点未处理
  Element? oldChild;
  final Widget newWidget = newWidgets[newChildrenTop];
  if (haveOldChildren) { // 存在可复用的Element节点
    final Key? key = newWidget.key;
    if (key != null) {
      oldChild = oldKeyedChildren![key];
      if (oldChild != null) {
        if (Widget.canUpdate(oldChild.widget, newWidget)) {
          oldKeyedChildren.remove(key);
        } else { // 无法基于新的Widget进行更新,放弃复用
          oldChild = null; 
        }
      }
    } // if
  } // if
  assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
  final Element newChild = // 计算新的Element节点,见代码清单5-7和代码清单5-47
    updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop, 
        previousChild))!;
  newChildren[newChildrenTop] = newChild;
  previousChild = newChild;
  newChildrenTop += 1;
} // while

以上逻辑主要是newChildren中间部分Element节点的更新,这些节点会优先通过LocalKey 复用。

在图8-4所示的第 4 阶段和第 5 阶段重置尾部索引的位置,并完成剩余节点的更新,如代码清单8-26所示。

// 代码清单8-26 flutter/packages/flutter/lib/src/widgets/framework.dart
assert(oldChildrenTop == oldChildrenBottom + 1); // 检查索引位置
assert(newChildrenTop == newChildrenBottom + 1);
assert(newWidgets.length - newChildrenTop == oldChildren.length - oldChildrenTop);
newChildrenBottom = newWidgets.length - 1; // 重置尾部索引,以便更新
oldChildrenBottom = oldChildren.length - 1;
// 开始更新newChildren的尾部,代码清单8-23中已经确认过可复用
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= 
    newChildrenBottom)) {
  final Element oldChild = oldChildren[oldChildrenTop];
  final Widget newWidget = newWidgets[newChildrenTop];
  final Element newChild = // 更新Element节点
    updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop, 
        previousChild))!;
  newChildren[newChildrenTop] = newChild;
  previousChild = newChild;
  newChildrenTop += 1;
  oldChildrenTop += 1;
}

至此,新的Element子树已经生成,但是oldKeyedChildren中可能还存有未命中Key的元素,需要释放,如代码清单8-27所示。

// 代码清单8-27 flutter/packages/flutter/lib/src/widgets/framework.dart
if (haveOldChildren && oldKeyedChildren!.isNotEmpty) { // oldKeyedChildren有未被
                                                       // 复用的节点
  for (final Element oldChild in oldKeyedChildren.values) {
    if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
      deactivateChild(oldChild); // 彻底移除Element Tree
  }
}

以上就是LocalKey的作用过程,它不像GlobalKey那样在代码中有明显的痕迹,却在无形中提高了Element Tree更新的效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/610297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python增量备份:保障数据安全,提高效率

Python增量备份&#xff1a;保障数据安全&#xff0c;提高效率 现今&#xff0c;越来越多的企业和个人都需要使用计算机存储数据&#xff0c;这些数据可能是各种各样的&#xff0c;例如文件、数据库、邮件等等&#xff0c;这些数据的安全保障是至关重要的。 正是基于这样的背…

chatgpt赋能python:Python多行转一行:最简便的方法

Python多行转一行&#xff1a;最简便的方法 Python是一个既神奇又神奇&#xff08;fully magical&#xff09;的编程语言。但有时候&#xff0c;我们用Python编写的程序会产生多行代码&#xff0c;而需要把这些多行代码转换为一行以便于复制和粘贴。这种情况下&#xff0c;我们…

chatgpt赋能python:Python备份数据库——保障数据安全,防止数据丢失

Python备份数据库——保障数据安全&#xff0c;防止数据丢失 在现代社会&#xff0c;数据被称为新的石油&#xff0c;数据的意义愈发重要。数据的丢失可能会导致不可挽回的损失,给企业造成巨大的财务损失&#xff0c;更为重要的是企业形象的损失。备份数据是保护数据最好的途径…

chatgpt赋能python:Python奇偶求和函数

Python奇偶求和函数 Python是一种简单易学、高效的编程语言&#xff0c;具有丰富的库和工具&#xff0c;让程序员能够快速编写出各种各样的应用程序。Python中的函数可以让程序更加模块化&#xff0c;更加易于维护和扩展。其中&#xff0c;奇偶求和函数是一个很好的例子&#…

chatgpt赋能python:Python培训:成为一名高薪Python工程师

Python培训&#xff1a;成为一名高薪Python工程师 Python已成为当今最流行的编程语言之一&#xff0c;它广泛应用于人工智能、机器学习、数据科学和Web开发等各个领域。许多公司都在寻找经验丰富的Python工程师来开发他们的产品。如果你想成为一名高薪Python工程师&#xff0c…

创客匠人CEO蒋洪波:用门店思维做直播

互联网时代&#xff0c;转型线上做知识付费成为教育培训行业的主流&#xff0c;直播教学成为新型的教学模式受到了广泛认可。很多老师在线下培训深耕多年&#xff0c;知识储备丰富&#xff0c;但想要转型线上又缺少方法&#xff0c;缺少去改变的欲望&#xff0c;怕转型做线上直…

区块链的基本介绍

目录 1、简介 2、区块链的分类 2.1 公有链 2.2 联盟链 2.3 私有链 3、区块链特征 4、区块链结构 5、区块链对记账权利的分配方式 5.1 POW 5.2 PoS 5.3 DPoS 6、Defi、NFT、 gameFi 7、DAPP 7.1 DAPP 的核心要素 8、比特币 8.1 比特币简介 8.2 比特币数字签名…

白话ES搜索相关性问题

之前使用es&#xff0c;更多的是使用term查询&#xff0c;和agg聚合分析。对相关性关注较少。实际上es擅长的是做模糊搜索&#xff0c;相关性搜索。 ES是一个开源的通用的检索工具&#xff0c;能满足百分之八十的需求。相关性这个问题&#xff0c;是一个非常有意思的问题&#…

卡尔曼滤波与组合导航原理笔记(一) 第一部分 滤波的基本概念、递推最小二乘

文章目录 一、滤波的基本概念1、传统数字滤波器2、现代控制中的状态观测器3、最优估计的含义4、温度估计的例子1.问题描述2.分析 二、递推最小二乘 课程链接&#xff1a;https://www.bilibili.com/video/BV11K411J7gp/?p1 参考书目&#xff1a;《捷联惯导算法与组合导航原理》…

搭建本地MQTT服务器实现局域网通信

在这里mqtt就不多做介绍了直接上手怎么搭建本地服务器 一-. 我们先下载一个emqx&#xff0c;我们可以去官网下载免费的 https://www.emqx.io/https://www.emqx.io/ 下载完成之后我们打开这个文件夹 然后进入bin目录在左上角输入cmd然后回车 如果操作成功会来到这个界面 在这…

不压缩打包layui

手动打包 下载layui源码&#xff08;当前版本2.6.4&#xff09;&#xff0c;并解压缩 下载地址&#xff1a;layui gitee 安装nodejs&#xff08;v10.24.1&#xff09; 下载链接 windows-x64 安装cnpm npm install -g cnpm -registryhttps://registry.npm.taobao.org全局安…

Real3D 动画书 jQuery 插件

Real3D FlipBook jQuery Plugin 插件 新 – 用于 REAL3D 动画书 JQUERY 插件的 PDF 到图像转换器 一种将 PDF 转换为图像和 JSON 的工具&#xff0c;用于创建带有链接、文本搜索和文本选择的优化 PDF 活页簿。 使用图像和 JSON 创建的 PDF 动画书加载页面的速度比标准 PDF 动画…

i.MX6ULL点灯

i.MX6ULL点灯 对应底板原理图找到对应的IO引脚号 CCGR寄存器共有七个&#xff0c;分别是0~6。 使能时钟&#xff0c;CCGR0~6这7个寄存器控制着6ULL所有外设时钟的使能。 为了简单&#xff0c;设置CCGR0~6这7个寄存器全部为0xFFFFFFFF&#xff0c;相当于使能所有外设时钟。 …

springboot项目外卖管理 day01-项目搭建以及后台登陆

文章目录 一、软件开发整体介绍1.1、软件开发流程1.2、角色分工1.3、软件环境1.4、技术选型1.5、功能架构1.6、角色 二、环境搭建2.1、数据库的创建2.2、创建springboot项目并添加依赖2.3、配置yml文件2.4、将前端页面配置进resource目录![在这里插入图片描述](https://img-blo…

ec-canvas 在小程序上的使用

文章目录 I. 前言echarts、ec-canvas&#xff0c;在小程序中进行数据可视化的意义 II. 安装ec-canvas1. 下载安装ec-canvas组件2. 配置组件参数 III. 初识ec-canvas1. echarts在微信小程序中的工作原理2. echarts小程序版的局限性与创新点3. 通过一个简单的示例了解ec-canvas的…

chatgpt赋能python:Python备份列表l:保护你的重要数据

Python备份列表l: 保护你的重要数据 当我们谈到数据的安全性时&#xff0c;备份是非常重要的。备份应该是在任何操作之前考虑的&#xff0c;因为在数据丢失或计算机崩溃时&#xff0c;我们需要在短时间内恢复数据。在这篇文章中&#xff0c;我们将讨论Python备份列表l。 什么…

【python技能树】python简介

1 Python定义 Python 是一种简单易学并且结合了解释性、编译性、互动性和面向对象的脚本语言。Python提供了高级数据结构&#xff0c;它的语法和动态类型以及解释性使它成为广大开发者的首选编程语言。 Python 是解释型语言&#xff1a; 开发过程中没有了编译这个环节。类似于…

Linux系统-Ubuntu安装指定版本的内核

Ubuntu安装指定版本的内核 以下演示 Linux 系统内核,手动安装的话可以安装所有指定版本的内核。 查看当前系统内核 uname -sr接下来以安装 5.13.0 内核为例 首先去 http://kernel.ubuntu.com/~kernel-ppa/mainline/找到内核版本为5.13.0 的链接 根据电脑64位处理器 选择 AM…

chatgpt赋能python:Python奇数和偶数和

Python奇数和偶数和 在Python编程中&#xff0c;奇数和偶数和是一个基本的概念。奇数和偶数是指整数的特定类型&#xff0c;其中奇数是指不能被2整除的正整数&#xff0c;偶数是指可以被2整除的正整数。本文将介绍Python中计算奇数和偶数和的方法。 计算奇数和偶数 要计算奇…

「QT」QT5程序设计目录

✨博客主页:何曾参静谧的博客 📌文章专栏:「QT」QT5程序设计 目录 📑【QT的基础知识篇】📑【QT的GUI编程篇】📑【QT的项目示例篇】📑【QT的网络编程篇】📑【QT的数据库编程篇】📑【QT的跨平台编程篇】📑【QT的高级编程篇】📑【QT的开发工具篇】📑【QT的调…