发布于 2026-01-06 1 阅读
0

在 JavaScript 中结合命令模式和状态模式

在 JavaScript 中结合命令模式和状态模式

JavaScript 是一种以其灵活性而闻名的流行语言。正因如此,像命令模式这样的模式才更容易在我们的应用程序中实现。

如果说有一种设计模式能够与状态模式完美契合,那无疑就是命令模式了。

如果你读过我之前关于状态模式的博客文章,你可能会记得这句话:“状态模式确保对象根据应用程序的当前‘状态’以可预测的、协调的方式行事。”

命令模式的主要目标是将两个重要参与者之间的沟通分开:

  1. 发起者(也称为调用者)
  2. 处理程序

本文将结合命令模式和状态模式进行讲解。如果您正在学习其中任何一种模式,为了更好地理解本文内容,我建议您在学习命令模式之前,先确保理解状态模式的流程,这样才能更好地体会代码行为如何在功能保持不变的情况下发生显著变化。

让我们先来看一个使用状态模式的例子,以便更清楚地理解这一点:

let state = { backgroundColor: 'white', profiles: [] }
let subscribers = []

function notifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
}

function setBackgroundColor(color) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: color,
  }))
}

function setState(newState) {
  let prevState = state
  state =
    typeof newState === 'function'
      ? newState(prevState)
      : { ...prevState, ...newState }

  notifySubscribers(prevState, state)
}

function subscribe(callback) {
  subscribers.push(callback)
}

function addProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
}

subscribe(
  (function () {
    function getColor(length) {
      if (length >= 5 && length <= 8) return 'blue' // Average
      if (length > 8 && length < 10) return 'orange' // Reaching limit
      if (length > 10) return 'red' // Limit reached
      return 'white' // Default
    }

    return (prevState, newState) => {
      const prevProfiles = prevState?.profiles || []
      const newProfiles = newState?.profiles || []
      if (prevProfiles.length !== newProfiles.length) {
        setBackgroundColor(getColor(newProfiles.length))
      }
    }
  })(),
)

console.log(state.backgroundColor) // 'white'

addProfile({ id: 0, name: 'george', gender: 'male' })
addProfile({ id: 1, name: 'sally', gender: 'female' })
addProfile({ id: 2, name: 'kelly', gender: 'female' })

console.log(state.backgroundColor) // 'white'

addProfile({ id: 3, name: 'mike', gender: 'male' })
addProfile({ id: 4, name: 'bob', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

addProfile({ id: 5, name: 'kevin', gender: 'male' })
addProfile({ id: 6, name: 'henry', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

addProfile({ name: 'ronald', gender: 'male' })
addProfile({ name: 'chris', gender: 'male' })
addProfile({ name: 'andy', gender: 'male' })
addProfile({ name: 'luke', gender: 'male' })

console.log(state.backgroundColor) // 'red'
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,我们有一个 ` stateand`subscribers对象。该subscribers对象包含一个回调函数集合。每当setState调用 `function` 时,这些回调函数都会被调用:

状态模式通知订阅者回调(JavaScript)

每次状态更新时,所有已注册的回调函数都会被调用,并将先前的状态(prevState)和新状态(newState)作为参数。

我们注册了一个回调监听器,以便监控状态更新,并在个人资料数量达到特定值时更新背景颜色length。下表更清晰地展示了个人资料数量与其对应颜色的对应关系:

最低阈值 背景颜色
0 白色的
5 蓝色的
9 橙子
10 红色的

状态模式订阅回调处理程序变更

那么,命令模式如何融入其中呢?如果我们回顾一下代码,就会发现我们定义了一些函数,分别负责调用和处理这个逻辑:

function notifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
}

function setBackgroundColor(color) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: color,
  }))
}

function addProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
}
Enter fullscreen mode Exit fullscreen mode

我们可以将这些抽象成命令。在接下来的代码示例中,我们将展示相同的代码,但命令模式与状态模式协调一致。这样一来,就只剩下两个函数没有改动:setStatesubscribe

接下来,我们将引入命令模式,并将我们的抽象函数命令化:

let state = { backgroundColor: 'white', profiles: [] }
let subscribers = []
let commands = {}

function setState(newState) {
  let prevState = state
  state =
    typeof newState === 'function'
      ? newState(prevState)
      : { ...prevState, ...newState }

  dispatch('NOTIFY_SUBSCRIBERS', { prevState, newState: state })
}

function subscribe(callback) {
  subscribers.push(callback)
}

function registerCommand(name, callback) {
  if (commands[name]) commands[name].push(callback)
  else commands[name] = [callback]
}

function dispatch(name, action) {
  commands[name]?.forEach?.((fn) => fn?.(action))
}

registerCommand(
  'SET_BACKGROUND_COLOR',
  function onSetState({ backgroundColor }) {
    setState((prevState) => ({ ...prevState, backgroundColor }))
  },
)

registerCommand('NOTIFY_SUBSCRIBERS', function onNotifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
})

registerCommand('ADD_PROFILE', function onAddProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
})

subscribe(
  (function () {
    function getColor(length) {
      if (length >= 5 && length <= 8) return 'blue' // Average
      if (length > 8 && length < 10) return 'orange' // Reaching limit
      if (length > 10) return 'red' // Limit reached
      return 'white' // Default
    }

    return ({ prevState, newState }) => {
      const prevProfiles = prevState?.profiles || []
      const newProfiles = newState?.profiles || []
      if (prevProfiles.length !== newProfiles.length) {
        dispatch('SET_BACKGROUND_COLOR', {
          backgroundColor: getColor(newProfiles.length),
        })
      }
    }
  })(),
)

console.log(state.backgroundColor) // 'white'

dispatch('ADD_PROFILE', { id: 0, name: 'george', gender: 'male' })
dispatch('ADD_PROFILE', { id: 1, name: 'sally', gender: 'female' })
dispatch('ADD_PROFILE', { id: 2, name: 'kelly', gender: 'female' })

console.log(state.backgroundColor) // 'white'

dispatch('ADD_PROFILE', { id: 3, name: 'mike', gender: 'male' })
dispatch('ADD_PROFILE', { id: 4, name: 'bob', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

dispatch('ADD_PROFILE', { id: 5, name: 'kevin', gender: 'male' })
dispatch('ADD_PROFILE', { id: 6, name: 'henry', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

dispatch('ADD_PROFILE', { id: 7, name: 'ronald', gender: 'male' })
dispatch('ADD_PROFILE', { id: 8, name: 'chris', gender: 'male' })
dispatch('ADD_PROFILE', { id: 9, name: 'andy', gender: 'male' })
dispatch('ADD_PROFILE', { id: 10, name: 'luke', gender: 'male' })

console.log(state.backgroundColor) // 'red'
Enter fullscreen mode Exit fullscreen mode

现在更容易确定需要更新状态的函数。我们可以将其他所有内容分离到各自独立的命令处理程序中。这样,我们可以将它们隔离到单独的文件或位置,以便更轻松地进行操作。

以下是我们在更新后的示例中演示的达到该目标的步骤:

  1. 创建commands变量。该变量将存储已注册的命令及其回调处理程序。
  2. 定义registerCommand此操作会将新命令及其回调处理程序注册到commands对象中。
  3. 定义dispatch。它负责调用与其命令关联的回调处理程序。

通过这三个步骤,我们完美地设置了命令,使其能够被客户端代码注册,从而允许客户端实现自己的命令和逻辑。请注意,我们的 ` registerCommandand`dispatch函数不需要了解任何与状态对象相关的信息。

我们可以轻松地利用这一点,继续将它们隔离到一个单独的文件中:

commands.js

// Private object hidden within this scope
let commands = {}

export function registerCommand(name, callback) {
  if (commands[name]) commands[name].push(callback)
  else commands[name] = [callback]
}

export function dispatch(name, action) {
  commands[name]?.forEach?.((fn) => fn?.(action))
}
Enter fullscreen mode Exit fullscreen mode

至于这些文字中实际蕴含的逻辑:

registerCommand(
  'SET_BACKGROUND_COLOR',
  function onSetState({ backgroundColor }) {
    setState((prevState) => ({ ...prevState, backgroundColor }))
  },
)

registerCommand('NOTIFY_SUBSCRIBERS', function onNotifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
})

registerCommand('ADD_PROFILE', function onAddProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
})
Enter fullscreen mode Exit fullscreen mode

通常情况下,这些都由客户端代码来决定(严格来说,我们最后一段代码片段代表的客户端代码)。一些库的作者也会定义自己的命令处理程序供内部使用,但基本概念仍然适用。我经常看到的一种做法是将内部逻辑放在一个单独的文件中,并在文件名前加上前缀"internal"(例如internalCommands.ts:)。值得注意的是,这些文件中的函数99%的情况下都不会导出给用户。这就是为什么它们被标记为internal的原因。

下图是我们应用命令设计模式之前的代码示意图:

状态模式先于命令模式集成

紫色气泡代表函数。其中就包括setBackgroundColor两个函数。这两个函数会直接调用 `getState()` 来进行状态更改。换句话说,它们都会调用 `getState()` 并处理它们感兴趣的特定状态切片的状态更新逻辑。addProfilesetState

现在请看下图。这张图展示了我们实现该模式后的代码的样子:

命令设计模式与状态模式的结合

函数 `__function__` notifySubscribersaddProfile`__function__` 和setBackgroundColor`__function__` 已移除,但它们的所有逻辑仍然保留。现在它们被编写为命令处理程序:

命令设计模式与状态模式及指示器

命令处理程序单独定义其逻辑并进行注册。注册后,它们会被“保留”,直到被dispatch函数调用。

最终,代码功能保持不变,只有行为发生了改变。

谁会采用这种方法?

我脑海中立刻浮现出一个例子,那就是FacebookLexical包。Lexical 是一个“可扩展的 JavaScript Web 文本编辑器框架,注重可靠性、可访问性和性能”。

在词法分析中,编辑器命令可以注册并可供使用。处理逻辑在命令注册时定义,以便在调用时能够识别它们dispatch

结论

这篇文章就到此结束了!希望这篇文章对您有所帮助,敬请期待我未来的更多内容!

文章来源:https://dev.to/jsmanifest/combining-the-command-pattern-with-state-pattern-in-javascript-2bja