避免简单增量器中的状态

Avoiding state in simple incrementer

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

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

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

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

如您所见,非 fp 模式下的计数器只是 stateful。 它保持状态,以便可以根据它们的 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);

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

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. "Let the consumer provide the state":函数 (inc/dec) 不使用它们自己的状态,它们将它作为参数并 return 它的新版本。试着想一个redux reducer,他们只嵌入了改变状态的逻辑......但最终,状态作为参数传递。
  2. "where the values continue to get incremented/decremented":状态永远不会改变,这叫做 immutability,纯函数总是 return 它的一个新副本,所以如果你想要你必须存储它在别的地方。

使用 DOM

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

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

/***** 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。

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. A Moore machine is a finite-state machine 的例子。它由三件事组成。

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

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

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

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


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

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

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>