状态管理

在数据不需要在多个组件之间流动的简单应用程序中,状态管理是非常简单的。可将部件需要的数据封装在部件内,这是 Dojo 应用程序中状态管理的最基本形式

随着应用程序变得越来越复杂,并且开始要求在多个部件之间共享和传输数据,就需要一种更健壮的状态管理形式。在这里,Dojo 开始展现出其响应式框架的价值,允许应用程序定义数据如何在组件之间流动,然后由框架管理变更检测和重新渲染。这是通过在部件的渲染函数中声明 VDOM 输出时将部件和属性连接在一起而做到的。

对于大型应用程序,状态管理可能是最具挑战性的工作之一,需要开发人员在数据一致性、可用性和容错性之间进行平衡。虽然这种复杂性大多超出了 web 应用程序层的范围,但 Dojo 提供了更进一步的解决方案,以确保数据的一致性。Dojo Store 组件提供了一个集中式的状态存储,它提供一致的 API,用于访问和管理应用程序中多个位置的数据。

基础:自封装的部件状态

部件可以通过多种方式维护其内部状态。基于函数的部件可以使用 icache 中间件来存储部件的本地状态,而基于类的部件可以使用内部的类字段。

内部状态数据可能直接影响部件的渲染输出,也可能作为属性传递给子部件,而它们继而又直接影响了子部件的渲染输出。部件还可能允许更改其内部状态,例如响应用户交互事件。

以下示例解释了这些模式:

src/widgets/MyEncapsulatedStateWidget.tsx

基于函数的部件:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. const factory = create({ icache });
  4. export default factory(function MyEncapsulatedStateWidget({ middleware: { icache } }) {
  5. return (
  6. <div>
  7. Current widget state: {icache.get<string>('myState') || 'Hello from a stateful widget!'}
  8. <br />
  9. <button
  10. onclick={() => {
  11. let counter = icache.get<number>('counter') || 0;
  12. let myState = 'State change iteration #' + ++counter;
  13. icache.set('myState', myState);
  14. icache.set('counter', counter);
  15. }}
  16. >
  17. Change State
  18. </button>
  19. </div>
  20. );
  21. });

基于类的部件:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. export default class MyEncapsulatedStateWidget extends WidgetBase {
  4. private myState = 'Hello from a stateful widget!';
  5. private counter = 0;
  6. protected render() {
  7. return (
  8. <div>
  9. Current widget state: {this.myState}
  10. <br />
  11. <button
  12. onclick={() => {
  13. this.myState = 'State change iteration #' + ++this.counter;
  14. }}
  15. >
  16. Change State
  17. </button>
  18. </div>
  19. );
  20. }
  21. }

注意,这个示例是不完整的,在正在运行的应用程序中,单击“Change State”按钮不会对部件的渲染输出产生任何影响。这是因为状态完全封装在 MyEncapsulatedStateWidget 部件中,而 Dojo 无从得知对部件的任何更改。框架只处理了部件的初始渲染。

要通知 Dojo 重新渲染,则需要封装渲染状态的部件自行失效。

让部件失效

基于函数的部件可以使用 icache 中间件处理本地的状态管理,当状态更新时会自动失效部件。icache 组合了 cacheinvalidator 中间件,拥有 cache 的处理部件状态管理的功能,和 invalidator 的当状态变化时让部件失效的功能。如果需要,基于函数的部件也可以直接使用 invalidator

基于类的部件,则有两种失效的方法:

  1. 在状态被更改后的适当位置显式调用 this.invalidate()
    • MyEncapsulatedStateWidget 示例中,可在“Change State”按钮的 onclick 处理函数中完成。
  2. 使用 @watch() 装饰器(来自 @dojo/framework/core/vdomercorators/watch 模块)注释任何相关字段。当修改了 @watch 注释的字段后,将隐式调用 this.invalidate(),这对于状态字段很有用,这些字段在更新时总是需要重新渲染。

注意: 将一个部件标记为无效,并不会立刻重新渲染该部件,而是通知 Dojo,部件已处于 dirty 状态,应在下一个渲染周期中进行更新和重新渲染。这意味着在同一个渲染帧内多次失效同一个部件并不会对应用程序的性能产生负面影响,但应避免过多重复的失效以确保最佳性能。

以下是修改过的 MyEncapsulatedStateWidget 示例,当状态变化时会正确地更新输出。

基于函数的部件:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. const factory = create({ icache });
  4. export default factory(function MyEncapsulatedStateWidget({ middleware: { icache } }) {
  5. return (
  6. <div>
  7. Current widget state: {icache.getOrSet<string>('myState', 'Hello from a stateful widget!')}
  8. <br />
  9. <button
  10. onclick={() => {
  11. let counter = icache.get<number>('counter') || 0;
  12. let myState = 'State change iteration #' + ++counter;
  13. icache.set('myState', myState);
  14. icache.set('counter', counter);
  15. }}
  16. >
  17. Change State
  18. </button>
  19. </div>
  20. );
  21. });

基于类的部件:

此处,myStatecounter 都在应用程序逻辑操作的同一个地方进行了更新,因此可将 @watch() 添加到任一字段上或者同时添加到两个字段上,这些配置的实际结果和性能状况完全相同:

src/widgets/MyEncapsulatedStateWidget.tsx

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import watch from '@dojo/framework/core/decorators/watch';
  3. import { tsx } from '@dojo/framework/core/vdom';
  4. export default class MyEncapsulatedStateWidget extends WidgetBase {
  5. private myState: string = 'Hello from a stateful widget!';
  6. @watch() private counter: number = 0;
  7. protected render() {
  8. return (
  9. <div>
  10. Current widget state: {this.myState}
  11. <br />
  12. <button
  13. onclick={() => {
  14. this.myState = 'State change iteration #' + ++this.counter;
  15. }}
  16. >
  17. Change State
  18. </button>
  19. </div>
  20. );
  21. }
  22. }

中级:传入部件属性

通过虚拟节点的 properties 将状态传入部件是 Dojo 应用程序中连接响应式数据流最有效的方法。

部件指定自己的属性接口,该接口包含部件希望向使用者公开的任何字段,包括配置选项、表示注入状态的字段以及任何事件处理函数。

基于函数的部件是将其属性接口以泛型参数的形式传给 create().properties<MyPropertiesInterface>() 的。然后,本调用链返回的工厂函数通过渲染函数定义中的 properties 函数参数,让属性值可用。

基于类的部件可将其属性接口定义为类定义中 WidgetBase 的泛型参数,然后通过 this.properties 对象访问其属性。

例如,一个支持状态和事件处理器属性的部件:

src/widgets/MyWidget.tsx

基于函数的部件:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. const factory = create().properties<{
  4. name: string;
  5. onNameChange?(newName: string): void;
  6. }>();
  7. export default factory(function MyWidget({ middleware: { icache }, properties }) {
  8. const { name, onNameChange } = properties();
  9. let newName = icache.get<string>('new-name') || '';
  10. return (
  11. <div>
  12. <span>Hello, {name}! Not you? Set your name:</span>
  13. <input
  14. type="text"
  15. value={newName}
  16. oninput={(e: Event) => {
  17. icache.set('new-name', (e.target as HTMLInputElement).value);
  18. }}
  19. />
  20. <button
  21. onclick={() => {
  22. icache.set('new-name', undefined);
  23. onNameChange && onNameChange(newName);
  24. }}
  25. >
  26. Set new name
  27. </button>
  28. </div>
  29. );
  30. });

基于类的部件:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. export interface MyWidgetProperties {
  4. name: string;
  5. onNameChange?(newName: string): void;
  6. }
  7. export default class MyWidget extends WidgetBase<MyWidgetProperties> {
  8. private newName = '';
  9. protected render() {
  10. const { name, onNameChange } = this.properties;
  11. return (
  12. <div>
  13. <span>Hello, {name}! Not you? Set your name:</span>
  14. <input
  15. type="text"
  16. value={this.newName}
  17. oninput={(e: Event) => {
  18. this.newName = (e.target as HTMLInputElement).value;
  19. this.invalidate();
  20. }}
  21. />
  22. <button
  23. onclick={() => {
  24. this.newName = '';
  25. onNameChange && onNameChange(newName);
  26. }}
  27. >
  28. Set new name
  29. </button>
  30. </div>
  31. );
  32. }
  33. }

此示例部件的使用者可以通过传入适当的属性与之交互:

src/widgets/NameHandler.tsx

基于函数的部件:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. import MyWidget from './MyWidget';
  4. const factory = create({ icache });
  5. export default factory(function NameHandler({ middleware: { icache } }) {
  6. let currentName = icache.get<string>('current-name') || 'Alice';
  7. return (
  8. <MyWidget
  9. name={currentName}
  10. onNameChange={(newName) => {
  11. icache.set('current-name', newName);
  12. }}
  13. />
  14. );
  15. });

基于类的部件:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import watch from '@dojo/framework/core/decorators/watch';
  4. import MyWidget from './MyWidget';
  5. export default class NameHandler extends WidgetBase {
  6. @watch() private currentName: string = 'Alice';
  7. protected render() {
  8. return (
  9. <MyWidget
  10. name={this.currentName}
  11. onNameChange={(newName) => {
  12. this.currentName = newName;
  13. }}
  14. />
  15. );
  16. }
  17. }

高级:提取和注入状态

实现复杂功能时,在部件内遵循状态封装模式可能会导致组件膨胀、难以管理。在大型应用程序中也可能出现另一个问题,数百个部件跨数十个层级组合在一起。通常是叶部件使用状态数据,并不是 VDOM 层次结构中的中间容器。让数据状态穿透这样一个层次结构复杂的部件需要增加脆弱、不必要的代码。

Dojo 提供的 Store 组件 解决了这些问题,它将状态管理提取到专用上下文中,然后将应用程序中的相关状态注入到特定的部件中。