【问题标题】:Avoiding state in simple incrementer避免简单增量器中的状态
【发布时间】:2020-09-14 13:36:31
【问题描述】:

我正在尝试使用 JavaScript 函数式编程技术来避免状态。我扎实掌握了许多基本的 fp 技术,例如闭包、柯里化等。但我无法全神贯注地处理状态。

我想知道创建函数式程序的人将如何实现以下非常简单的应用程序:

用户单击浏览器中的按钮(jQuery 实现很好)。每次用户单击按钮时,屏幕上的值应增加 1。

我们如何在不改变状态的情况下做到这一点?如果需要突变状态,从功能的角度来看,最好的方法是什么?

【问题讨论】:

  • 函数式方法不是避免状态,而是明确状态。在某种抽象层面上,我们甚至无法做到这一点,因为 DOM 只是可变的,而用户交互是有状态的。
  • 我知道 DOM 是可变的,需要一个带有副作用的函数来更新 DOM。所以这里肯定有一些给予和接受。在这种情况下,您将如何明确表示状态?

标签: javascript functional-programming state immutability


【解决方案1】:

下面是您如何实现一个简单的计数器应用程序,而不需要改变除 DOM 之外的任何内容。

const h1 = document.querySelector("h1");

const [decrement, reset, increment] = document.querySelectorAll("button");

const render = count => {
    h1.innerHTML = count; // output
    decrement.onclick = event => render(count - 1); // -+
    reset.onclick     = event => render(0);         //  |-- transition functions
    increment.onclick = event => render(count + 1); // -+
};

render(0);
//     ^
//     |
//     +-- initial state
<h1></h1>
<button>-</button>
<button>Reset</button>
<button>+</button>

这是Moore machine 的示例。摩尔机器是finite-state machine。它由三部分组成。

  1. 机器的初始状态。在我们的例子中,初始状态是0
  2. 一个转换函数,它给定当前状态和一些输入,产生一个新状态。
  3. 给定当前状态的输出函数会产生一些输出。

在我们的例子中,我们将转换函数和输出函数组合成一个 render 函数。这是可能的,因为转换函数和输出函数都需要当前状态。

当为渲染函数提供当前状态时,它会产生一些输出以及一个转换函数,当提供一些输入时,它会产生一个新状态并更新状态机。

在我们的例子中,我们将转换函数划分为多个共享当前状态的转换函数。


我们还可以使用事件委托来提高性能。在下面的示例中,我们只在整个文档上注册了一个点击事件监听器。当用户单击文档中的任意位置时,我们检查目标元素是否具有onClick 方法。如果是,我们将其应用于事件。

接下来,在render 函数中,我们没有为每个按钮注册单独的onclick 侦听器,而是将它们存储为名为@9​​87654331@ 的常规方法(不同的情况)。这更高效,因为我们没有注册多个事件侦听器,这会降低应用程序的速度。

const h1 = document.querySelector("h1");

const [decrement, reset, increment] = document.querySelectorAll("button");

const render = count => {
    h1.innerHTML = count;
    decrement.onClick = event => render(count - 1);
    reset.onClick     = event => render(0);
    increment.onClick = event => render(count + 1);
};

render(0);

document.addEventListener("click", event => {
    if (typeof event.target.onClick === "function") {
        event.target.onClick(event);
    }
});
<h1></h1>
<button>-</button>
<button>Reset</button>
<button>+</button>

【讨论】:

  • 我喜欢这个解决方案和对摩尔机器的引用。 render() 函数也让人想起 React socool 解决方案。虽然这个解决方案在技术上并没有改变逻辑中的状态,但它仍然依赖于存储在 DOM 中的状态。不一定是可怕的事情,但这是理想的吗?
  • React 的外观并不肤浅。 React 本质上模拟了一个摩尔机器,尽管它被副作用扭曲了。我不是 React 的忠实粉丝。 Facebook 的想法是正确的,但他们搞砸了实施。 React 比 jQuery 更实用,但并不是真正的实用。甚至 React 钩子也不是真正的功能。如果你想要一个真正实用的框架来创建 Web 应用程序,那么看看 Elm。
  • @AaditMShah 好吧,从技术上讲,状态存储在一个变量范围中,该范围被存储在 DOM 中的函数所封闭,但是您无法真正避免这种情况,而且 imo 这提供的封装实际上比存储更好全局范围内的状态。然而,导入部分是它不会从可变 DOM 中读取任何状态。
  • @Hitmands 如果您使用事件委托,您甚至不必在每次渲染时重新分配事件侦听器。这是一个例子。 jsfiddle.net/aaditmshah/dj8c45zt。现在,您只有一个事件侦听器,并且在每次渲染时更新 DOM 对象的属性,这比添加事件侦听器要快得多。
  • @scriptum 将事件委托移动到答案的末尾,并添加了为什么它更好的解释。
【解决方案2】:

如您所见,非 fp 模式下的计数器只是 有状态。 它保存状态,以便可以根据他们的 api 增加或减少它。

const createCounter = () => {
  let value = 0;
  
  return {
    get value() {
      return value;
    },
    increment() {
      value = value + 1;
    },
    decrement() {
      value = value - 1;
    },
  };
};


const counter = createCounter();
console.log('initial value', counter.value);

counter.increment();
counter.increment();
console.log('value after two increments', counter.value);


counter.decrement();
console.log('value after one decrement', counter.value);

构建计数器的功能性方式是让消费者提供状态。函数只知道如何改变它:

const incrementCounter = counter => counter + 1;
const decrementCounter = counter => counter - 1;

const value = 0;
console.log('initial value', value);

const valueAfterTwoIncrements = incrementCounter(
  incrementCounter(value),
);

console.log('value after two increments', valueAfterTwoIncrements);


const valueAfterOneDecrement = decrementCounter(valueAfterTwoIncrements);

console.log('value after one decrement', valueAfterOneDecrement);

这种方法的优点几乎是无数的,函数是纯粹的,它们的输出是确定性的,所以测试非常容易等等。


问答:

  1. “让消费者提供状态”:函数 (inc/dec) 不适用于它们自己的状态,它们将其作为参数并返回它的新版本。试着想一个redux reducer,他们只嵌入了改变状态的逻辑......但最终,状态作为参数传递。
  2. “值继续递增/递减”:状态永远不会改变,这被称为immutability,纯函数将始终返回它的新副本,因此如果您愿意,您必须将其存储在某个地方别的。

使用 DOM

将视图层与实际的业务逻辑层分开

正如您在下面的示例中看到的,视图是数据不感知的,dom 仅用于呈现或触发 ui 事件,而实际的业务逻辑(当前状态值以及如何增加/减少)被保留在不同且分离良好的层上。编排层最终用于将这两层绑定在一起。

/***** View Layer *****/
const IncBtn = ({ dispatch }) => {
  dispatch({ type: 'INC' });
};

const DecBtn = ({ dispatch }) => {
  dispatch({ type: 'DEC' });
};

const Value = ({ getState }) => {
  document.querySelector('#value').value = getState();
};


/***** Business Logic Layer *****/
const counter = (state = 0, { type }) => {
  switch(type) {
    case 'INC':
      return state + 1;
      
    case 'DEC':
      return state - 1;
    
    default:
      return state;
  }
};


/***** Orchestration Layer *****/
const createStore = (reducer) => {
  let state = reducer(undefined, { type: 'INIT' });
  
  return {
    dispatch: (action) => {
      state = reducer(state, action);
    },
    getState: () => state,
  };
}


(() => {
  const store = createStore(counter);
  // first render
  Value(store);
  
  document
    .querySelector('#inc')
    .addEventListener('click', () => {
      IncBtn(store);
      
      Value(store);
    });
  
  document
    .querySelector('#dec')
    .addEventListener('click', () => {
      DecBtn(store);
      
      Value(store);
    });
})();
<button id="inc">Increment</button>
<button id="dec">Decrement</button>
<hr />

<input id="value" readonly disabled/>

【讨论】:

  • 嗯,虽然在调用堆栈中存储状态在技术上是正确的,但您没有解决 DOM 部分,即如何将这种方法与 DOM 连接起来。
  • 我可以接受这个答案,如果它清楚地显示了应该如何更新状态,则不用提及 DOM 方面。不过,我不确定您所说的“让消费者提供状态”是什么意思。我也看不到值实际上在哪里继续增加。它可以从 0 变为 2,但这不会将其从 2 变为 4。
  • 您的 DOM 示例不起作用,因为您的商店不纯。您能否在不改变商店内状态的情况下使您的示例正常工作?提示,这是可能的,并且生成的代码更简洁。
  • 不太清楚你的商店不纯是什么意思,真正需要纯的是你的减速器功能......商店根据定义是有状态的,你可以看看redux是怎么实现的。
  • Redux 是一个不好的例子。它不必要地使动作、reducer 和调度复杂化。考虑你的例子。当用户按下inc 按钮时,IncBtn 函数被调用,该函数向reducer 分派一个动作,reducer 将增加的值返回给store,从而更新其内部状态。之后,您必须手动调用Value 函数来呈现更新的状态。这会导致代码膨胀。你可以用 68 行代码完成的事情,我可以用递归的 19 行代码完成。东西到处都是,这使得维护变得困难。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-22
  • 2010-09-07
相关资源
最近更新 更多