Skip to content

Snapshot

基本概念

当一个 actor 接收到事件时,其内部状态可能会发生变化。当状态发生转换时,actor 可能会发出一个快照。你可以通过 actor.getSnapshot() 同步读取 actor 的快照,或者通过 actor.subscribe(observer) 订阅快照。

获取快照

getSnapshot

ts
import { fromPromise, createActor } from 'xstate';

async function fetchCount() {
  return Promise.resolve(42);
}

const countLogic = fromPromise(async () => {
  const count = await fetchCount();

  return count;
});

const countActor = createActor(countLogic);

countActor.start();

countActor.getSnapshot(); // logs undefined

// After the promise resolves...
countActor.getSnapshot();
// => {
//   output: 42,
//   status: 'done',
//   ...
// }

subscribe

您可以通过 actor.subscribe(observer) 订阅一个 actor 的快照值。当快照值发出时,观察者将接收该值。观察者可以是:

  • 一个接收最新快照的普通函数,
  • 一个观察者对象,其 .next(snapshot) 方法接收最新的快照
  • 使用 error 回调来订阅 actor 抛出的错误。这使您能够处理由 actor 逻辑发出的错误。
ts
// Observer as a plain function
const subscription = actor.subscribe((snapshot) => {
  console.log(snapshot);
});
ts
// Observer as an object
const subscription = actor.subscribe({
  next(snapshot) {
    console.log(snapshot);
  },
  error(err) {
    // ...
  },
  complete() {
    // ...
  },
});

actor.subscribe(observer) 的返回值是一个订阅对象,它有一个 .unsubscribe() 方法。你可以调用 subscription.unsubscribe() 来取消订阅观察者:

ts
const subscription = actor.subscribe((snapshot) => {
  /* ... */
});

// Unsubscribe the observer
subscription.unsubscribe();

当 actor 停止时,其所有观察者将自动取消订阅。

Snapshot Object

Snapshot就是状态机在某一时刻(即某个State)的“完整快照”。它不是只有一个字符串,而是一个对象,包含了:

ts
{
  value,     // 当前处于哪些状态
  context,   // 扩展状态(业务数据)
  _meta,      // 状态元信息,需要通过getMeta获取
  children,  // 当前活跃的子 actor
  status,    // active | done | stopped
  output,     // 结束态输出(如果有)
  tags  //Set
}

snapshot是一个只读的,可信的UI状态源

value

  • 简单状态机:
ts
state.value === 'question'
  • 嵌套状态机
ts
state.value === { form: 'invalid' }

当前状态机中:

激活了一个父状态 form,并且在 form 里面,激活的子状态是 invalid

ts
createMachine({
  initial: 'form',
  states: {
    form: {
      initial: 'invalid',
      states: {
        invalid: {},
        valid: {},
      },
    },
  },
})

为什么不是 'invalid',而是 { form: 'invalid' }

因为 XState 不会丢失“层级信息”

  • 并行状态

每个 key = 一个并行区域

ts
state.value ===
  {
    monitor: 'on',
    mode: 'dark',
  };
ts
createMachine({
  type: 'parallel', // 👈 关键
  states: {
    playback: {
      initial: 'paused',
      states: {
        paused: {},
        playing: {},
      },
    },
    volume: {
      initial: 'unmuted',
      states: {
        muted: {},
        unmuted: {},
      },
    },
  },
})

事件如何作用在并行状态上:

ts
on: {
  PLAY: { target: '.playback.playing' },
  MUTE: { target: '.volume.muted' },
}
  • null

State machines may also have no state nodes other than the root state node. For these state machines, the state value is null.

status

status含义
active正在运行中
done已完成(进入 final)
error执行过程中发生错误
stopped被手动停止

context

state.context 表示的是当前状态下的上下文。

ts
const state = actor.getSnapshot()

state.context
// => { count: 0 }
ts
createMachine({
  context: {
    count: 0,
  },
  initial: 'a',
  states: {
    a: {
      on: {
        NEXT: {
          target: 'b',
          actions: assign({
            count: ({ context }) => context.count + 1,
          }),
        },
      },
    },
    b: {
      entry: ({ context }) => {
        console.log(context.count)
      },
    },
  },
})

你拿到的是“快照(snapshot)”,不是“状态本体”

ts
const state = actor.getSnapshot()

/*
{
  value: ...,
  context: { count: 1 },
  ...
}
*/

这个对象是:

  • ✅ 当前时刻的只读快照

  • ❌ 不是状态机内部正在用的那一份

ts
state.context.count++ // ❌

children

state.children 是“当前状态机正在运行的所有子 actor 的索引表”

key = actor id

value = ActorRef

output

状态机“走到最终完成态(final)时”,对外产出的结果。

必须 同时满足两个条件

  1. 状态机到达 顶层 final state
  2. final state 定义了 output
ts
const machine = createMachine({
  initial: 'loading',
  states: {
    loading: {
      on: {
        DONE: 'success'
      }
    },
    success: {
      type: 'final',
      output: { ok: true }
    }
  }
})
ts
const actor = createActor(machine).start()

actor.send({ type: 'DONE' })

const state = actor.getSnapshot()

state.status // 'done'
state.output // { ok: true }

methods

state.can(event)

ts
state.can(event:EventObject)
  • event:EventObject。事件对象

state.can(event) 用来判断:在“当前状态 + 当前上下文”下,发送这个事件会不会触发一次状态转换。state.can 会真实执行 guard

ts
const machine = createMachine({
  context: { valid: false },
  initial: 'form',
  states: {
    form: {
      on: {
        SUBMIT: {
          target: 'success',
          guard: ({ context }) => context.valid
        }
      }
    },
    success: {}
  }
})

const actor = createActor(machine).start()
const state = actor.getSnapshot()

state.can({ type: 'SUBMIT' }) // false(guard 不通过)

state.hasTag(tag)

state.hasTag(tag) 用来判断:当前激活的任意状态节点,是否打了某个 tag。

ts
state.hasTag(tag:string)

state.matches(value)

ts
state.matches(value)
  • value:StateValue。状态值

state.matches(...) 用来判断:当前 state.value 是否“包含”你给定的状态路径。只要当前状态“包含”你给的路径,就算匹配

ts
// state.value === 'question'
state.matches('question'); // true

// state.value === { form: 'invalid' }
state.matches('form'); // true
state.matches('question'); // false
state.matches({ form: 'invalid' }); // true
state.matches({ form: 'valid' }); // false

state.getMeta()

state.getMeta() 返回的是:当前“所有激活状态节点”的 meta 信息集合。不是某一个 state 的 meta,而是整棵激活状态树上的 meta

  • Return:Record<string, any>,key:状态节点的完整 ID(路径)
ts
const feedbackMachine = createMachine({
  id: 'feedback',
  // ...
  states: {
    form: {
      meta: {
        view: 'shortForm',
      },
    },
  },
});

const feedbackActor = createActor(feedbackMachine).start();

// Assume the current state is 'form'
const snapshot = feedbackActor.getSnapshot();
console.log(snapshot.getMeta());
// logs { 'feedback.form': { view:'shortForm' } }

辅助函数

waitFor

ts
waitFor(actor, predicate, options?)
  • actor:Actor
  • predicate:回调函数callback(snapshot)
  • options:
    • timeout:超时时间

你可以使用 waitFor 辅助函数等待一个 actor 的快照满足某个谓词条件。waitFor(...) 函数返回一个 promise,该 promise:

  • 当发出的快照满足 predicate 函数时解析
  • 如果当前快照已经满足 predicate 函数,则立即解决
  • 如果抛出错误或 options.timeout 值超时,则会被拒绝。
ts
import { waitFor } from 'xstate';
import { countActor } from './countActor.ts';

const snapshot = await waitFor(
  countActor,
  (snapshot) => {
    return snapshot.context.count >= 100;
  },
  {
    timeout: 10_000, // 10 seconds (10,000 milliseconds)
  },
);

console.log(snapshot.output);
// => 100