导语
作为一个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状态发生变化时,会调用。
触发刷新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成功调用存在三个前提:
- 当前State的生命周期不是defunct
- 当前State处于create状态下且需要mounted
- 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使用的条件又做了什么。