最右 JS2Flutter 框架——渲染机制(二)
1、概述
在上一篇文章最右JS2Flutter框架——开篇[1]中,我们已经介绍了如何实现最简单的Hello World,示例中只涉及到Text一个Widget,实际开发过程中,会需要用到各种丰富的Widget,甚至还有自定义的Widget。我们需要解决两个问题,怎么维护好这些Widget并渲染出页面,以及如何处理页面的刷新、跳转、退出等操作。
2、主流程
JS2Flutter框架在编译期间会把开发者对Flutter的依赖切换到镜像Flutter之上,并借助dart2js将Flutter镜像及业务代码编译成js,这也是实现对Flutter开发者透明的关键所在。在运行时跟Flutter的流程类似,通过runApp从根节点开始构建Client的虚拟树,Binding的过程中会依赖镜像的Widget、Animation、Gestures等,虚拟树构建完成之后,会序列化成JSON,传递到Host侧,然后通过ClassInstantiation解析并构建出真实的Widget树,然后绑定到AppContainer之上。当Host侧接收到事件之后,回溯给Client侧的镜像Widget,并回传给业务。借助下图更容易理解整个过程。
3、虚拟树构建
我们要先理解Flutter从Widget树构建出Element树的流程,不清楚的同学可以查看Gityuan的博客——深入理解Flutter应用启动[2]。Flutter为了满足不同的需求场景,提供了一些比较基础等Widget,比如StatelessWidget、StatefulWidget、LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget等。在理解了这个过程之后,我们确信需要跟Flutter一样,提供一些基础的Widget,这些基础的Widget都有与之对应的Element,跟Flutter的工作方式一样,在WidgetBinding的过程中,从根节点的Widget对应的Element开始mount,层层构建子Widget的Element并mount,直到把所有的节点都记录下来,从而完成Client侧虚拟树的构建。
他们都是Widget的派生类,而Widget本身继承自DartObject。DartObject是为了记录类的信息并提供数据化能力,当然它不只针对于Widget,它同样适用于Color、Offset等数据类。
每个Widget都会重写toJson,将自己的属性记录到节点信息中去,以MaterialButton为例,除了记录UI相关的属性外,如果有监听事件响应,也需记录下来。
4、真实Widget树构建
虚拟树构建完成之后会数据化并传给Host侧,解析数据,还原出真实的Widget树。参考上一篇的Hello World例子,我们解析className,识别到是一个Text,从而构建出一个真实的Text并填充data。我们向树形结构拓展一下,从根节点开始,解析根节点的类型、属性、以及孩子节点,孩子节点也需要做一样的操作,直到它本身是一个叶子节点。理解这个过程之后,我们就要思考如何去实现,构造Widget的信息基本上都集中在构造函数中,如果Flutter能用反射,那就很简单了,只需要根据类名信息反射构造对应的Widget并填充属性,但是Flutter禁用了反射,所以我们只能提前把className和相应构造器的关系进行绑定,当需要构造某个Widget时,根据类名找到对应的构造器进行对应属性的解析,ClassInstantiation便是去完成这件事情。
事件应该如何处理呢?我们还是以MaterialButton为例,当它接收到onPressed事件之后,会传递给Client侧虚拟树中对应的镜像MaterialButton,再回调给业务。
5、状态的更新
页面的刷新、跳转、退出以及弹窗等都是状态的更新,不仅要更新虚拟树,而且要同步到Host侧,对真实的Widget树进行更新。
5.1 刷新
刷新主要是针对StatefulWidget,我们照样先看看Flutter是如何处理StatefulWidget的更新机制的,不清楚的同学可以查看Gityuan的博客——深入理解setState更新机制[3]。我们可以像Flutter一样,通过标记脏节点,重新构建子树的差量更新,当然也可以直接更新整个子树,对于Client侧来说,这两种方案差异不大,因为真实的渲染树是否更新并不由它们决定,而是通过差量数据构建出来的真实Widget子树与原来的真实Widget子树之间是否存在差异。
5.2 跳转和退出
跳转其实是分为两类的,一类是通过在WidgetsApp、MaterialApp、CupertinoApp提前注册的路由表去实现,这类属于静态注册,另一类是通过动态构建Route去实现,属于动态注册。弹窗就是一种动态注册。
最右是通过预占坑的方式去实现页面的跳转的,静态注册的路由会在WidgetsApp、MaterialApp和CupertinoApp的构造器中去还原路由表,每个路由都会构造一个预占坑的壳与之对应,动态注册的页面通过Navigator.of(context).push实时构建坑位,这些坑位都是StatefulWidget,当坑位initState的时候,向Client侧索取页面的虚拟树,拿到虚拟树之后构建出真实的Widget树,Client侧在构建出虚拟树之后会直接挂载到WidgetsApp、MaterialApp、CupertinoApp下面,当页面退出时,坑位触发dispose,此时请求Client侧的虚拟树移除对应子树。
5.3 AppLifecycleState
其实我们还涉及到App状态的监听,很多时候我们需要感知App的生命周期,来完成一些事情。Flutter通过WidgetsBinding提供了此能力,最右是如何解决这个问题的呢?借助AppContainer。细心的同学可能会发现深入理解Flutter应用启动[2]文章中提到了起点runApp(Widget app),我们是从数据流向的过程逐步剖析JS2Flutter的渲染过程,但实际上在Host端,Engine启动之后,会runApp一个AppContainer,它也是一个预占坑的StatefulWidget,当真实Widget树构建出来之后会刷新AppContainer,这是一个从始至终都存在的Widget,它的生命周期等于整个Flutter App的生命周期。我们可以借助它监听AppLifecycleState的变化,然后传递给Client侧的WidgetsBinding,Client侧的WidgetsBinding会去管理注册、分发等。
6、Canvas绘制
除了通过上述的Widget去描述UI之外,还有一类特殊的渲染方式,比如CustomPainter,需要通过Canvas绘制。这部分会在【最右JS2Flutter框架——动画、小游戏的实现】一文中进行详解。
7、结束语
本文阐述了最右JS2Flutter框架的渲染机制,实际上还有很多细节问题并未继续展开,感兴趣的同学可以发散思考,欢迎留言探讨。
8、参考文献
[1]:最右JS2Flutter框架——开篇 https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1
[2]:深入理解Flutter应用启动 http://gityuan.com/2019/06/29/flutter_run_app/
[3]:深入理解setState更新机制 http://gityuan.com/2019/07/06/flutter_set_state/
评论 (1 条评论)