Context & Actions

Context & Actions

一个Machine的状态(state)是有限的,例如水的状态(固、液、气、等离子),但我们仍然会需要储存非定性的可变资料(data),这些资料我们会储存在context中,如下:

const machine = Machine({
  context: {
    // 资料 (data) 存在 context 裡,key 可以自己订
    count: 0,
    user: null,
  },
  states: {
    //...
  },
});

我们可以透过withContext()动态的给定初始资料,如下:

const myMachine = machine.withContext({
  count: 10,
  user: {
    name: "Jerry",
  },
});

在任何状态下,我们都可以拿到context的值:

machine.initialState.context;
// { user: null, count: 0 }

const service = interpret(machine.withContext({
  count: 10,
  user: {
    name: 'Jerry'
  },
});
service.start();
service.state.context;
// { user: { name: 'Jerry' }, count: 10 }

至于要如何在特定的状态中改变machine内的context呢?我们会需要用到Assign ActionsActions是一种 理射后不理(Fire-and-forget)Effect,专门用来处理单一次的作用,另外在XState中还有许多不同种类的Effects

Effects

Statecharts的世界裡,Side Effect可以依行为区分为两类:

  • Fire-and-forget effects -指执行Side Effect后不会另外送任何eventstatecharteffect
  • Invoked effects -指除了可执行Side Effect之外还能发送和接收eventseffect

这两类EffectXState中依据不同的使用方式,又可以分为:

  • Fire-and-forget effects
    • Actions -用于单次、离散的Effect
    • Activities -用于连续的Effect
  • Invoked effects
    • Invoked Promises
    • Invoked Callbacks
    • Invoked Observables
    • invoked Machines

Actions

Action本身就是一个function,接收三个参数分别是context, event以及actionMetacontext就是当前machinecontextevent则是触发当前状态切换的事件,actionMeta则会存放当前的state以及action物件。

const action = (context, event, actionMeta) => {
  // do something...
};

我们可以把actions写在任何State的任何事件裡,如下:

const lightMachine = Machine({
  initial: "red",
  states: {
    red: {
      on: {
        CLICK: {
          // 转换到 green 的状态
          target: "green",
          // transition actions
          actions: (context, event) => console.log("hello green"),
        },
      },
    },
    green: {
      on: {
        CLICK: {
          target: "red",
          // transition actions
          actions: (context, event) => console.log("hello red"),
        },
      },
    },
  },
});

另外还有两种actions,分别是在进入state以及离开state时触发,如下:

const lightMachine = Machine({
  initial: "red",
  states: {
    red: {
      // entry actions
      entry: (context, event) => console.log("entry red"),
      // exit actions
      exit: (context, event) => console.log("exit red"),
      on: {
        CLICK: {
          target: "green",
        },
      },
    },
    //...
  },
});

在进入red状态时会触发red内部的entry,在离开red状态时会触发red内部的exit。这两种actions我们称为entry actions以及exit actions。另外actions可以定义在machine options内,并透过string来指定执行的action,如下:

const lightMachine = Machine({
  initial: 'red',
  states: {
    red: {
      // entry actions
      entry: 'entryRed'
      // exit actions
      exit: 'exitRed',
      on: {
        CLICK: {
          target: 'green',
          // transition actions
          actions: 'redClick',
        },
      }
    },
    //...
  }
}, {
  actions: {
    entryRed: (context, event) => console.log('entry red'),
    exitRed: (context, event) => console.log('exit red'),
    redClick: (context, event) => console.log('hello green'),
  },
});

所有设定actions的地方都可以是一个array,依序执行多个actions,如下:

const lightMachine = Machine(
  {
    initial: "red",
    states: {
      red: {
        // entry actions
        entry: ["entryRed", "temp"],
        // exit actions
        exit: ["exitRed", "temp"],
        on: {
          CLICK: {
            target: "green",
            // transition actions
            actions: ["redClick", "temp"],
          },
        },
      },
      //...
    },
  },
  {
    actions: {
      entryRed: (context, event) => console.log("entry red"),
      exitRed: (context, event) => console.log("exit red"),
      redClick: (context, event) => console.log("hello green"),
      temp: (context, event) => console.log("temp"),
    },
  }
);

在实务开发上,不建议直接把action function inlinemachine config裡,如下,这会造成之后难以除错、测试以及图像化。

  CLICK: {
    target: 'gerrn',
    actions: (context, event) => console.log('hello green')
  }

建议统一把actions放在machine options内,如下:

const lightMachine = Machine(
  {
    initial: "red",
    states: {
      red: {
        // entry actions
        entry: ["entryRed", "temp"],
        //...
      },
      //...
    },
  },
  {
    actions: {
      entryRed: (context, event) => console.log("entry red"),
      temp: (context, event) => console.log("temp"),
    },
  }
);

Assign Action

assign是一个function专门用来更新machine context,它吃一个assigner参数,这个参数会表示context要更新成什麽值。assigner可以是一个object (推荐用法),用法如下:

import { Machine, assign } from "xstate";

// ...
actions: assign({
  // 透过外部传进来的 event 来改变 count
  count: (context, event) => context.count + event.value,
  message: "value 也可以直接是 static value",
});
// ...

assigner也可以是一个function,用法如下:

// ...
  // 他会 partial update context
	actions: assign((context, event) => {
    return {
      count: context.count + event.value,
      message: 'value 也可以直接是 static value'
    }
  }),
// ...

让我们直接来看一个简单的例子吧:

const counterMachine = Machine(
  {
    id: "counter",
    initial: "ENABLED",
    context: {
      count: 0,
    },
    states: {
      ENABLED: {
        on: {
          INC: {
            actions: ["increment"],
          },
          DYNAMIC_INC: {
            actions: ["dynamic_increment"],
          },
          RESET: {
            actions: ["reset"],
          },
          DISABLE: "DISABLED",
        },
      },
      DISABLED: {
        on: {
          ENABLE: "ENABLED",
        },
      },
    },
  },
  {
    actions: {
      increment: assign({
        count: (context) => context.count + 1,
      }),
      dynamic_increment: assign({
        count: (context, event) => context.count + (event.value || 0),
      }),
      reset: assign({
        count: 0,
      }),
    },
  }
);

从上面这个范例,可以看出使用XState能够很清楚的定义出什麽状态下可以接收哪些event,例如在DISABLED的状态下就只会对ENABLEevent会有反应,对于INC, RESET等事件就不会有反应。另外从DYNAMIC_INC事件可以看出如何根据外部传入的参数控制增长数值,详细可以参考以下这段,程序码:

//...
on: {
  [COUNTER_EVENTS.DYNAMIC_INC]: {
    actions: ['dynamic_increment'],
  },
}
//...
actions: {
  dynamic_increment: assign({
    count: (context, event) => context.count + (event.value || 0)
    // event 除了 type 这个属性之外有什麽 property 是外部决定的
  }),
},
//...
//...
<Button
  label="Increment"
  onClick={() =>
    // 这裡传入 DYNAMIC_INC event 同时要给 value
    send({ type: COUNTER_EVENTS.DYNAMIC_INC, value: Number(value) })
  }
/>
//...

注意事项

  • 永远不要从外部修改一个machine内的context,任何改变context的行为都应该来自event
  • 推荐使用 assign({ ... }) 的写法,这个写法利于未来的工具做分析。
  • 跟所有actions相同不建议inline写在machine裡面,建议定义在machine optionsactions内。
  • 理想上,context应该是一个JSplain object,并且应该可以被序列化。
  • 记得 assign 就只是pure function回传一个action物件,并直接对machine造成影响。
下一页