Element

在“Widget简介”一节,我们介绍了Widget和Element的关系,我们知道最终的UI树其实是由一个个独立的Element节点构成。我们也说过组件最终的Layout、渲染都是通过 RenderObject 来完成的,从创建到渲染的大体流程是:

  • 根据Widget生成Element
  • 然后创建相应的 RenderObject 并关联到 Element.renderObject 属性上
  • 最后再通过 RenderObject 来完成布局排列和绘制

    Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如 MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树,我们称之为”Render Tree“即”渲染树“。总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树、Element树、渲染树。他们的依赖关系是: Element树根据Widget树生成,而渲染树又依赖于Element树,如图所示:

Element与BuildContext - 图1

现在我们重点看一下Element,Element的生命周期如下

  1. Framework 调用 Widget.createElement 创建一个Element实例,记为 element
  2. Framework 调用 element.mount(parentElement,newSlot)
    • mount方法中首先调用 element 所对应Widget的 createRenderObject 方法创建与 element 相关联的RenderObject对象,
    • 然后调用 element.attachRenderObject 方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
  3. 当有父Widget的配置数据改变时,同时其 State.build 返回的Widget结构与之前不同,此时就需要重新构建对应的Element树
    • 为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的 element,element 节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate 主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。
  4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致 element 对应的Widget被移除),这时该祖先Element就会调用 deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用 element.deactivate 方法,这时element状态变为“inactive”状态。
  5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的 element 在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其 unmount 方法将其彻底移除,这时 element 的状态为defunct,它将永远不会再被插入到树中。
  6. 如果 element 要重新插入到Element树的其它位置,如element或element的祖先拥有一个 GlobalKey(用于全局复用元素),那么Framework会先将 element 从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

    看完Element的生命周期,可能有些读者会有疑问,开发者会直接操作Element树吗? 其实对于开发者来说,大多数情况下只需要关注Widget树就行,Flutter框架已经将对Widget树的操作映射到了Element树上,这可以极大的降低复杂度,提高开发效率。 但是了解Element对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element这个纽带将Widget和RenderObject关联起来,了解Element层不仅会帮助读者对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。另外在有些时候,我们必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据,具体细节将在下文介绍。

BuildContext

我们已经知道,StatelessWidget 和 StatefulWidget 的 build 方法都会传一个 BuildContext 对象:

  1. Widget build(BuildContext context) {}

我们也知道,在很多时候我们都需要使用这个 context 做一些事,比如:

  1. Navigator.push(context, route) //入栈新路由
  2. Theme.of(context) //获取主题
  3. Localizations.of(context, type) //获取Local
  4. context.size //获取上下文大小
  5. context.findRenderObject() //查找当前或最近的一个祖先RenderObject

那么 BuildContext 到底是什么呢,查看其定义,发现其是一个抽象接口类:

  1. abstract class BuildContext {
  2. Widget get widget;
  3. BuildOwner get owner;
  4. RenderObject findRenderObject();
  5. Size get size;
  6. @Deprecated(
  7. 'Use dependOnInheritedElement instead. '
  8. 'This feature was deprecated after v1.12.1.'
  9. )
  10. InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect });
  11. InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect });
  12. @Deprecated(
  13. 'Use dependOnInheritedWidgetOfExactType instead. '
  14. 'This feature was deprecated after v1.12.1.'
  15. )
  16. InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect });
  17. T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect });
  18. @Deprecated(
  19. 'Use getElementForInheritedWidgetOfExactType instead. '
  20. 'This feature was deprecated after v1.12.1.'
  21. )
  22. InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType);
  23. InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
  24. @Deprecated(
  25. 'Use findAncestorWidgetOfExactType instead. '
  26. 'This feature was deprecated after v1.12.1.'
  27. )
  28. Widget ancestorWidgetOfExactType(Type targetType);
  29. T findAncestorWidgetOfExactType<T extends Widget>();
  30. @Deprecated(
  31. 'Use findAncestorStateOfType instead. '
  32. 'This feature was deprecated after v1.12.1.'
  33. )
  34. State ancestorStateOfType(TypeMatcher matcher);
  35. T findAncestorStateOfType<T extends State>();
  36. @Deprecated(
  37. 'Use findRootAncestorStateOfType instead. '
  38. 'This feature was deprecated after v1.12.1.'
  39. )
  40. State rootAncestorStateOfType(TypeMatcher matcher);
  41. T findRootAncestorStateOfType<T extends State>();
  42. @Deprecated(
  43. 'Use findAncestorRenderObjectOfType instead. '
  44. 'This feature was deprecated after v1.12.1.'
  45. )
  46. RenderObject ancestorRenderObjectOfType(TypeMatcher matcher);
  47. T findAncestorRenderObjectOfType<T extends RenderObject>();
  48. void visitAncestorElements(bool visitor(Element element));
  49. void visitChildElements(ElementVisitor visitor);
  50. DiagnosticsNode describeElement(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});
  51. DiagnosticsNode describeWidget(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});
  52. List<DiagnosticsNode> describeMissingAncestor({ @required Type expectedAncestorType });
  53. DiagnosticsNode describeOwnershipChain(String name);
  54. }

那这个 context 对象对应的实现类到底是谁呢?我们顺藤摸瓜,发现 build 调用是发生在

  • StatelessWidget 对应的 StatelessElement
  • StatefulWidget 对应的 StatefulElement

StatelessElement 中

  1. /// An [Element] that uses a [StatelessWidget] as its configuration.
  2. class StatelessElement extends ComponentElement {
  3. /// Creates an element that uses the given widget as its configuration.
  4. StatelessElement(StatelessWidget widget) : super(widget);
  5. @override
  6. StatelessWidget get widget => super.widget as StatelessWidget;
  7. @override
  8. Widget build() => widget.build(this);
  9. @override
  10. void update(StatelessWidget newWidget) {
  11. super.update(newWidget);
  12. assert(widget == newWidget);
  13. _dirty = true;
  14. rebuild();
  15. }
  16. }

StatelessElement 中:

  1. /// An [Element] that uses a [StatefulWidget] as its configuration.
  2. class StatefulElement extends ComponentElement {
  3. /// Creates an element that uses the given widget as its configuration.
  4. StatefulElement(StatefulWidget widget)
  5. : _state = widget.createState(),
  6. super(widget) {
  7. assert(() {
  8. if (!state._debugTypesAreRight(widget)) {
  9. throw FlutterError.fromParts(<DiagnosticsNode>[
  10. ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
  11. ErrorDescription(
  12. 'The createState function for ${widget.runtimeType} returned a state '
  13. 'of type ${state.runtimeType}, which is not a subtype of '
  14. 'State<${widget.runtimeType}>, violating the contract for createState.',
  15. ),
  16. ]);
  17. }
  18. return true;
  19. }());
  20. assert(state._element == null);
  21. state._element = this;
  22. assert(
  23. state._widget == null,
  24. 'The createState function for $widget returned an old or invalid state '
  25. 'instance: ${state._widget}, which is not null, violating the contract '
  26. 'for createState.',
  27. );
  28. state._widget = widget;
  29. assert(state._debugLifecycleState == _StateLifecycle.created);
  30. }
  31. @override
  32. Widget build() => state.build(this);
  33. /// The [State] instance associated with this location in the tree.
  34. ///
  35. /// There is a one-to-one relationship between [State] objects and the
  36. /// [StatefulElement] objects that hold them. The [State] objects are created
  37. /// by [StatefulElement] in [mount].
  38. State<StatefulWidget> get state => _state!;
  39. State<StatefulWidget>? _state;
  40. @override
  41. void reassemble() {
  42. if (_debugShouldReassemble(_debugReassembleConfig, _widget)) {
  43. state.reassemble();
  44. }
  45. super.reassemble();
  46. }
  47. @override
  48. void _firstBuild() {
  49. assert(state._debugLifecycleState == _StateLifecycle.created);
  50. try {
  51. _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
  52. final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
  53. assert(() {
  54. if (debugCheckForReturnedFuture is Future) {
  55. throw FlutterError.fromParts(<DiagnosticsNode>[
  56. ErrorSummary('${state.runtimeType}.initState() returned a Future.'),
  57. ErrorDescription('State.initState() must be a void method without an `async` keyword.'),
  58. ErrorHint(
  59. 'Rather than awaiting on asynchronous work directly inside of initState, '
  60. 'call a separate method to do this work without awaiting it.',
  61. ),
  62. ]);
  63. }
  64. return true;
  65. }());
  66. } finally {
  67. _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  68. }
  69. assert(() {
  70. state._debugLifecycleState = _StateLifecycle.initialized;
  71. return true;
  72. }());
  73. state.didChangeDependencies();
  74. assert(() {
  75. state._debugLifecycleState = _StateLifecycle.ready;
  76. return true;
  77. }());
  78. super._firstBuild();
  79. }
  80. @override
  81. void performRebuild() {
  82. if (_didChangeDependencies) {
  83. state.didChangeDependencies();
  84. _didChangeDependencies = false;
  85. }
  86. super.performRebuild();
  87. }
  88. @override
  89. void update(StatefulWidget newWidget) {
  90. super.update(newWidget);
  91. assert(widget == newWidget);
  92. final StatefulWidget oldWidget = state._widget!;
  93. // We mark ourselves as dirty before calling didUpdateWidget to
  94. // let authors call setState from within didUpdateWidget without triggering
  95. // asserts.
  96. _dirty = true;
  97. state._widget = widget as StatefulWidget;
  98. try {
  99. _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
  100. final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
  101. assert(() {
  102. if (debugCheckForReturnedFuture is Future) {
  103. throw FlutterError.fromParts(<DiagnosticsNode>[
  104. ErrorSummary('${state.runtimeType}.didUpdateWidget() returned a Future.'),
  105. ErrorDescription( 'State.didUpdateWidget() must be a void method without an `async` keyword.'),
  106. ErrorHint(
  107. 'Rather than awaiting on asynchronous work directly inside of didUpdateWidget, '
  108. 'call a separate method to do this work without awaiting it.',
  109. ),
  110. ]);
  111. }
  112. return true;
  113. }());
  114. } finally {
  115. _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  116. }
  117. rebuild();
  118. }
  119. @override
  120. void activate() {
  121. super.activate();
  122. state.activate();
  123. // Since the State could have observed the deactivate() and thus disposed of
  124. // resources allocated in the build method, we have to rebuild the widget
  125. // so that its State can reallocate its resources.
  126. assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
  127. markNeedsBuild();
  128. }
  129. @override
  130. void deactivate() {
  131. state.deactivate();
  132. super.deactivate();
  133. }
  134. @override
  135. void unmount() {
  136. super.unmount();
  137. state.dispose();
  138. assert(() {
  139. if (state._debugLifecycleState == _StateLifecycle.defunct)
  140. return true;
  141. throw FlutterError.fromParts(<DiagnosticsNode>[
  142. ErrorSummary('${state.runtimeType}.dispose failed to call super.dispose.'),
  143. ErrorDescription(
  144. 'dispose() implementations must always call their superclass dispose() method, to ensure '
  145. 'that all the resources used by the widget are fully released.',
  146. ),
  147. ]);
  148. }());
  149. state._element = null;
  150. // Release resources to reduce the severity of memory leaks caused by
  151. // defunct, but accidentally retained Elements.
  152. _state = null;
  153. }
  154. @override
  155. InheritedWidget dependOnInheritedElement(Element ancestor, { Object? aspect }) {
  156. assert(ancestor != null);
  157. assert(() {
  158. final Type targetType = ancestor.widget.runtimeType;
  159. if (state._debugLifecycleState == _StateLifecycle.created) {
  160. throw FlutterError.fromParts(<DiagnosticsNode>[
  161. ErrorSummary('dependOnInheritedWidgetOfExactType<$targetType>() or dependOnInheritedElement() was called before ${state.runtimeType}.initState() completed.'),
  162. ErrorDescription(
  163. 'When an inherited widget changes, for example if the value of Theme.of() changes, '
  164. "its dependent widgets are rebuilt. If the dependent widget's reference to "
  165. 'the inherited widget is in a constructor or an initState() method, '
  166. 'then the rebuilt dependent widget will not reflect the changes in the '
  167. 'inherited widget.',
  168. ),
  169. ErrorHint(
  170. 'Typically references to inherited widgets should occur in widget build() methods. Alternatively, '
  171. 'initialization based on inherited widgets can be placed in the didChangeDependencies method, which '
  172. 'is called after initState and whenever the dependencies change thereafter.',
  173. ),
  174. ]);
  175. }
  176. if (state._debugLifecycleState == _StateLifecycle.defunct) {
  177. throw FlutterError.fromParts(<DiagnosticsNode>[
  178. ErrorSummary('dependOnInheritedWidgetOfExactType<$targetType>() or dependOnInheritedElement() was called after dispose(): $this'),
  179. ErrorDescription(
  180. 'This error happens if you call dependOnInheritedWidgetOfExactType() on the '
  181. 'BuildContext for a widget that no longer appears in the widget tree '
  182. '(e.g., whose parent widget no longer includes the widget in its '
  183. 'build). This error can occur when code calls '
  184. 'dependOnInheritedWidgetOfExactType() from a timer or an animation callback.',
  185. ),
  186. ErrorHint(
  187. 'The preferred solution is to cancel the timer or stop listening to the '
  188. 'animation in the dispose() callback. Another solution is to check the '
  189. '"mounted" property of this object before calling '
  190. 'dependOnInheritedWidgetOfExactType() to ensure the object is still in the '
  191. 'tree.',
  192. ),
  193. ErrorHint(
  194. 'This error might indicate a memory leak if '
  195. 'dependOnInheritedWidgetOfExactType() is being called because another object '
  196. 'is retaining a reference to this State object after it has been '
  197. 'removed from the tree. To avoid memory leaks, consider breaking the '
  198. 'reference to this object during dispose().',
  199. ),
  200. ]);
  201. }
  202. return true;
  203. }());
  204. return super.dependOnInheritedElement(ancestor as InheritedElement, aspect: aspect);
  205. }
  206. /// This controls whether we should call [State.didChangeDependencies] from
  207. /// the start of [build], to avoid calls when the [State] will not get built.
  208. /// This can happen when the widget has dropped out of the tree, but depends
  209. /// on an [InheritedWidget] that is still in the tree.
  210. ///
  211. /// It is set initially to false, since [_firstBuild] makes the initial call
  212. /// on the [state]. When it is true, [build] will call
  213. /// `state.didChangeDependencies` and then sets it to false. Subsequent calls
  214. /// to [didChangeDependencies] set it to true.
  215. bool _didChangeDependencies = false;
  216. @override
  217. void didChangeDependencies() {
  218. super.didChangeDependencies();
  219. _didChangeDependencies = true;
  220. }
  221. @override
  222. DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
  223. return _ElementDiagnosticableTreeNode(
  224. name: name,
  225. value: this,
  226. style: style,
  227. stateful: true,
  228. );
  229. }
  230. @override
  231. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  232. super.debugFillProperties(properties);
  233. properties.add(DiagnosticsProperty<State<StatefulWidget>>('state', _state, defaultValue: null));
  234. }
  235. }

通过上面我们发现build传递的参数是 this,很明显!

  • BuildContext 就是 StatelessElement。
  • StatefulWidget 的 context 是 StatefulElement。

但 StatelessElement 和 StatefulElement 本身并没有实现 BuildContext 接口,继续跟踪代码,发现它们间接继承自Element 类,然后查看 Element 类定义,发现 Element 类果然实现了BuildContext接口:

  1. class Element extends DiagnosticableTree implements BuildContext {
  2. ...
  3. }

至此真相大白,BuildContext就是widget对应的Element
所以我们可以通过 context 在 StatelessWidget 和 StatefulWidget 的 build 方法中直接访问Element对象。我们获取主题数据的代码 Theme.of(context) 内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。

思考题:为什么build方法的参数不定义成Element对象,而要定义成BuildContext ?

进阶

我们可以看到 Element是Flutter UI框架内部连接widget和 RenderObject 的纽带
大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽 Element 细节,所以Framework在 StatelessWidget 和 StatefulWidget 中通过build方法参数又将 Element 对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作 Element 对象。那么现在笔者提两个问题,请读者先自己思考一下:

  1. 如果没有widget层,单靠 Element 层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?
  2. Flutter UI框架能不做成响应式吗?

    对于问题1,答案当然是肯定的,因为我们之前说过widget树只是 Element 树的映射,我们完全可以直接通过Element 来搭建一个UI框架。下面举一个例子:

我们通过纯粹的Element来模拟一个 StatefulWidget 的功能,假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序,代码如下:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. title: 'Flutter Demo',
  8. theme: ThemeData(
  9. primarySwatch: Colors.blue,
  10. ),
  11. home:
  12. Scaffold(appBar: AppBar(title: Text('自定义UI框架')), body: CustomHome()),
  13. );
  14. }
  15. }
  16. class CustomHome extends Widget {
  17. // @override
  18. // Element createElement() {
  19. // return CustomElement(this);
  20. // }
  21. @override
  22. Element createElement() => CustomElement(this);
  23. }
  24. class CustomElement extends ComponentElement {
  25. CustomElement(Widget widget) : super(widget);
  26. String text = "123456789";
  27. @override
  28. Widget build() {
  29. Color primary = Theme.of(this).primaryColor; //1
  30. return GestureDetector(
  31. child: Center(
  32. child: FlatButton(
  33. child: Text(
  34. text,
  35. style: TextStyle(color: primary),
  36. ),
  37. onPressed: () {
  38. var t = text.split("")..shuffle();
  39. text = t.join();
  40. markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
  41. },
  42. ),
  43. ),
  44. );
  45. }
  46. }
  • 上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用 this 代替即可,如代码注释1处Theme.of(this)参数直接传this即可,因为当前对象本身就是Element实例。
  • 当text发生改变时,我们调用 markNeedsBuild() 方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法
  • 上面代码中build方法返回的仍然是一个widget,这是由于Flutter框架中已经有了widget这一层,并且组件库都已经是以widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。
  • 如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:
    1. class CustomHome extends Widget {
    2. @override
    3. Element createElement() => HomeView(this)
    4. }

    现在就可以将 CustomHome 添加到widget树了,我们在一个新路由页创建它,最终效果如下如图所示:

Element与BuildContext - 图2 Element与BuildContext - 图3

对于问题2,答案当然也是肯定的,Flutter engine提供的dart API是原始且独立的,这个与操作系统提供的API类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android风格或iOS风格,但这些事Google不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为做与不做是一回事,但知道能不能做是另一回事,这能反映出我们对知识的理解程度。

总结

本节详细的介绍了 Element 的生命周期,以及它Widget、BuildContext的关系,也介绍了Element在Flutter UI系统中的角色和作用,我们将在下一节介绍Flutter UI系统中另一个重要的角色RenderObject。