避免简单增量器中的状态
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);
这种方法的优点几乎数不胜数,函数是纯粹的,它们的输出是确定的,因此测试非常容易等。
问答:
- "Let the consumer provide the state":函数 (inc/dec) 不使用它们自己的状态,它们将它作为参数并 return 它的新版本。试着想一个redux reducer,他们只嵌入了改变状态的逻辑......但最终,状态作为参数传递。
- "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 的例子。它由三件事组成。
- 机器的初始状态。在我们的例子中,初始状态是
0
.
- 给定当前状态和一些输入的转换函数会产生一个新状态。
- 给定当前状态的输出函数产生一些输出。
在我们的例子中,我们将转换函数和输出函数合并为一个 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>
我正在尝试使用 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);
这种方法的优点几乎数不胜数,函数是纯粹的,它们的输出是确定的,因此测试非常容易等。
问答:
- "Let the consumer provide the state":函数 (inc/dec) 不使用它们自己的状态,它们将它作为参数并 return 它的新版本。试着想一个redux reducer,他们只嵌入了改变状态的逻辑......但最终,状态作为参数传递。
- "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 的例子。它由三件事组成。
- 机器的初始状态。在我们的例子中,初始状态是
0
. - 给定当前状态和一些输入的转换函数会产生一个新状态。
- 给定当前状态的输出函数产生一些输出。
在我们的例子中,我们将转换函数和输出函数合并为一个 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>