合理选择装饰器
最小化状态共享范围
在没有强烈的业务需求下,尽可能按照状态需要共享的最小范围选择合适的装饰器。
应用开发过程中,按照组件颗粒度,状态一般分为组件内独享的状态和组件间需要共享的状态
组件间需要共享的状态
组件间需要共享的状态,按照共享范围从小到大依次有三种场景:
父子组件间共享状态,不同子树上组件间共享状态和不同组件树间共享状态
-
父子组件间共享状态:如下图,
-
"父组件"和其子组件"子组件A"、"子组件B"共享状态loading。
-
图3父子组件间共享状态场景
-
不同子树上组件间共享状态:
-
如下图,祖先组件的左子树上"孙子组件AAA"和右子树上"孙子组件BAA"共享状态loading。
-
图4不同子树上组件间状态共享场景
-
不同组件树间共享状态:
-
如下图,组件树A内"子组件AA"和组件树B内"孙子组件AAA"共享状态loading。
-
图5不同组件树间共享状态的场景
对于上述三种场景,ArkUI提供了@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink、@Provide+@Consume、AppStorage、LocalStorage六种装饰器组合以解决不同范围内的组件间状态共享。按照共享范围能力从小到大,各装饰器组合的共享范围能力和生命周期如下:
- @State+@Prop、@State+@Link、@State+@Observed+@ObjectLink:三者的共享范围为从@State所在的组件开始,到@Prop/@Link/ObjectLink所在组件的整条路径,路径上所有的中间组件通过@Prop/@Link/@ObjectLink都可以共享同一个状态。@State修饰的状态和其所属的自定义组件共享生命周期,在组件内定义时创建,组件销毁时被回收。@Link装饰的变量和其所属的自定义组件共享生命周期。@ObjectLink装饰的变量和其所属的自定义组件共享生命周期。(父子)
- @Provide+@Consume:状态共享范围是以@Provide所在组件为祖先节点的整棵子树,子树上的任意后代组件通过@Consume都可以共享同一个状态。@Provide修饰的变量与其所属的组件绑定,在组件内定义时被创建,在组件销毁时被回收。(根节点为祖先节点以下的子节点都可获取到)
- LocalStorage:共享范围为UIAbility内以页面为单位的不同组件树间的共享。存储在LocalStorage中的状态的生命周期与LocalStorage绑定。LocalStorage的声明周期由应用程序决定,当应用释放最后一个指向LocalStorage的引用时,LocalStorage被垃圾回收。(同页面的不同组件树间的共享)
- AppStorage:共享范围是应用全局。AppStorage与应用的进程绑定,由UI框架在应用程序启动时创建,当应用进程终止,AppStorage被回收。存储在AppStorage中的状态的生命周期与LocalStorage绑定。(全局变量)
按照软件开发原则,应优先选择共享范围能力小的装饰器方案,减少不同模块间的数据耦合,便于状态及时回收。
建议选择装饰器的优先级为:@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。
减少不必要的参数层层传递
看个示例图
项目中使用Navigation组件管理路由,定义"appNavigationStack"变量表示应用当前的路由信息。现"DiscoverView"组件和"ResourceListView"组件需要共享路由信息状态。按照@State+@Prop层层传递的方案,当前各组件的设计如下:
图7@State+@Prop当前组件设计图
层层传递,直到两个需要共享状态的组件
若此时产品需要新增功能,该功能要求在"DiscoverView"组件的后代"ActionButtonView"组件上新增对路由信息的判断逻辑。此时开发者需修改上述各个组件设计如下图所示:
图8@State+@Prop新增功能后组件设计图
可以看到,新功能的逻辑原本只是在"ActionButtonView"这一个组件中使用,开发者却需要修改从"DiscoverView"组件到"ActionButtonView"组件路径上3个组件的结构。若当业务后续再次变更为无需使用该状态时,也同样需要修改多个组件。这显然不是很好的实现方案。
此时使用@Provide+@Consume方案更为合理。同样是"ResourceListView"组件和"DiscoverView"组件共享状态,此方案各组件设计如下:
图9使用@Provide+@Consume方式当前各组件设计
通过在最顶部组件"MainPage"中注入key值为"appNavigationStack"的路由信息状态,其后代组件均可以通过@Consume装饰器获取该状态值。当业务变动需要"DiscoverView"的后代"ActionButtonView"组件也共享路由信息时,此方案只需在组件"ActionButtonView"上使用@Consume装饰器直接获取路由信息状态,而无需修改其他组件。此时各组件设计如下:
图10使用@Provide+@Consume方式业务变动后各组件设计
因此当共享状态的组件间跨层级较深时,或共享的信息对于整个组件树是"全局"的存在时,选择@Provide+@Consume的装饰器组合代替层层传递的方式,能够提升代码的可维护性和可拓展性。
按照状态复杂度选择装饰器
- @State+@Prop组合方案:
- @Prop装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
- @Prop装饰的变量是对父组件传入状态值的深拷贝,当@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。
- @Prop装饰的变量和父组件是单向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Prop所在组件的实例会重新渲染。 当该组件内被@Prop装饰的变量被修改时,父组件数据源不会变化,父组件实例也不会重新渲染。
- @State+@Link组合方案:
- @Link装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
- @Link装饰器修饰的变量是对父组件传入状态的引用的拷贝,两者指向同一个地址。当状态是简单数据类型或简单Object类型时,@Link和@Prop在状态创建时间和内存的占用方面区别不大。当状态为复杂的Object、class或其类型数组时,@Link相较@Prop能明显减少状态创建时间和内存的占用。
- @Link装饰器的变量和父组件是双向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Link所在组件的实例会重新渲染。 当该组件内被@Link装饰的变量被修改时,父组件数据源会同步修改,父组件实例也会重新渲染。
- @State+@Observed+@ObjectLink组合方案:
- @ObjectLink只支持接收被@Observed装饰的class实例及继承Date或者Array的class实例。
- @ObjectLink装饰的变量是只读的,不支持对状态重新赋值。
- @ObjectLink必须配合@Observed使用,它的设计是为了解决对嵌套类对象属性变化的监听,如需要观察对象数组中单个数据项的属性值变化,或嵌套对象的对象类型属性的子属性变化。
结合三个方案的特性,在选择时有如下建议:
- 需要观察嵌套类对象的深层属性变化的场景,选择@State+@Observed+@ObjectLink。
- 状态是复杂对象、类或其类型数组的场景,选择@State+@Link。
- 状态是简单数据类型时,使用@State+@Link和@State+@Prop均可。在功能层面上,依据@Prop单向绑定的特性,@State+@Prop适合用于非实时修改的场景,如编辑电话薄联系人信息时,展示编辑界面的子组件信息的修改要求不实时同步回父组件,需要等到编辑完成后点击"确认"按钮时才会以事件驱动的方式修改父组件的状态。依据@Link双向绑定的特性,@State+@Link适合用于实时修改的场景,如组件嵌套时的滚动条同步。
总结
在实际开发中,合理选择装饰器主要包含以下三步:
1.首先根据状态需要共享的范围大小,尽量选择共享能力小的装饰器方案,优先级依次为@State+@Prop、@State+@Link或@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。
2.当共享的状态的组件间层级相差较大时,为避免较差的代码可扩展性和可维护性,@Provide+@Consume的方案要优于层层传递的共享方案。
3.对于具有相同优先级的@State+@Prop、@State+@Link或@State+@Observed+@ObjectLink 三个方案,应结合状态的复杂程度和装饰器各自的特性选择。
实际开发中,应根据业务需求衡量优先级选择合适的装饰器,整体可参考如下建议:
- @State+@Prop:适合状态结构简单,且共享状态的组件间层级相差不大的场景。或功能上要求子组件不实时同步修改给父组件的场景。
- @State+@Link:适合状态结构复杂,且共享状态的组件间层级相差不大的场景。或功能上要求子组件对状态的修改实时同步给父组件的场景。
- @State+@Observed+@ObjectLink:适合需要观察嵌套类对象的子属性变化的场景或对象数组的数据项属性变化的场景,如监听列表卡片上某个属性的变化。
- @Provide+@Consume:适合用于对于整个组件树而言"全局"的状态,且该状态改动不频繁的状态共享场景,如共享界面的路由信息。
- AppStorage:适合对于整个应用而言"全局"的变量或应用的主线程内多个UIAbility实例间的状态共享,如用户信息。
- LocalStorage:适合对于单个Ability而言"全局"的变量,主要用于不同页面间的状态共享场景。
精细化拆分复杂状态
第一种方式
使用AppStorage存储用户信息UserData,将收藏的id加入到UserData这个类中 那么探索页面的收藏发生改变 我的页面自然同步 但是有一点不好 我的页面一旦我的信息发生修改 那么AppStorage存储用户信息UserData一旦监听到修改就会同步更新探索和我的页面 其实探索页根本不需要存储用户信息只需要存储收藏的id数组就行 这样我们就分开存储
单独存储下id就好了 在此方案中,由于文章卡片组件没有绑定AppStorage中key值为"userData"的变量,当用户编辑修改了用户描述userData.description的值时, 文章卡片组件不会重新渲染。
因此,从性能的角度考虑,在使用LocalStorage或AppStorage装饰器存储状态变量时需要合理设计状态的数据结构,避免无意义的渲染刷新。