FP 替代 JavaScript/ReactJS 中的多态性



我目前正在做一个 ReactJS 项目,我需要在其中创建"可重用"的组件,其中某些方法需要被"覆盖"。在OOP中,我会使用多态性。我已经做了一些阅读,似乎共识是使用 HoC/组合,但我不太清楚如何实现这一目标。我想如果我能用合成得到一个 ES6 样本,那么之后将这个想法改编到 ReactJS 可能会更容易。

下面是一个 ES6 OOP 示例(忽略事件处理,它仅用于测试),几乎是我想在 ReactJS 中实现的目标。有没有人对如何将 ReactJS 组件分解为 HoC 有一些指导,甚至只是演示我如何根据示例在 ES6 中使用组合?

class TransferComponent {
constructor(){
let timeout = null;
this.render();
this.events();
}
events(){
let scope = this;
document.getElementById('button').addEventListener('click', function(){
scope.validate.apply(scope);
});
}
validate(){
if(this.isValid()){
this.ajax();
}
}
isValid(){
if(document.getElementById('username').value !== ''){
return true;
}
return false;
}
ajax(){
clearTimeout(this.timeout);
document.getElementById('message').textContent = 'Loading...';
this.timeout = setTimeout(function(){
document.getElementById('message').textContent = 'Success';
}, 500);
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>n
<button id="button" type="button">Validate</button>';
}
}
class OverrideTransferComponent extends TransferComponent{
isValid(){
if(document.getElementById('username').value !== '' && document.getElementById('password').value !== ''){
return true;
}
return false;
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>n
<input type="text" id="password" value="password"/>n
<button id="button" type="button">Validate</button>';
}
}
const overrideTransferComponent = new OverrideTransferComponent();
<div id="content"></div>
<div id="message"></div>

更新:尽管我最初的问题是关于FP的,但我认为渲染道具是解决我的问题的一个非常好的解决方案,并且避免了HoC问题。

编辑:

这是帖子已过时。钩子更适合大多数用例。

原答案:

关于您的示例代码的答案在这篇文章的中间/底部。

进行 React 组合的一个好方法是渲染回调模式,也就是函数即子模式。与 HOC 相比,它的主要优势在于它允许您在运行时(例如在渲染中)动态组合组件,而不是在创作时静态编写组件。

无论您使用渲染回调还是 HOC,组件组合中的目标都是将可重用行为委托给其他组件,然后将这些组件作为 props 传递给需要它们的组件。

抽象示例:

以下Delegator组件使用渲染回调模式将实现逻辑委托给作为 prop 传入的ImplementationComponent

const App = () => <Delegator ImplementationComponent={ImplementationB} />;
class Delegator extends React.Component {
render() {
const { ImplementationComponent } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => {
/* ... do/render things based on doLogic ... */
} }
</ImplementationComponent>
</div>
);
}
}

各种实现组件如下所示:

class ImplementationA extends React.Component {
doSomeLogic() { /* ... variation A ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
}
class ImplementationB extends React.Component {
doSomeLogic() { /* ... variation B ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
} 

稍后,您可以按照相同的组合模式在Delegator组件中嵌套更多子组件:

class Delegator extends React.Component {
render() {
const { ImplementationComponent, AnotherImplementation, SomethingElse } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => { /* ... */} }
</ImplementationComponent>

<AnotherImplementation>
{ ({ doThings, moreThings }) => { /* ... */} }
</AnotherImplementation>

<SomethingElse>
{ ({ foo, bar }) => { /* ... */} }
</SomethingElse>
</div>
);
}
}

现在,嵌套子组件允许多个具体实现:

const App = () => (
<div>
<Delegator 
ImplementationComponent={ImplementationB}
AnotherImplementation={AnotherImplementation1}
SomethingElse={SomethingVariationY}
/>
<Delegator 
ImplementationComponent={ImplementationC}
AnotherImplementation={AnotherImplementation2}
SomethingElse={SomethingVariationZ}
/>
</div>
); 

答案(您的示例):

将上述组合模式应用于您的示例,该解决方案将重构您的代码,但假定它需要执行以下操作:

  • 允许输入及其验证逻辑的变化
  • 当用户提交有效输入时,执行一些 ajax

首先,为了使事情变得更容易,我将 DOM 更改为:

<div id="content-inputs"></div>
<div id="content-button"></div> 

现在,TransferComponent只知道如何显示按钮并在按下按钮且数据有效时执行某些操作。它不知道要显示哪些输入或如何验证数据。它将该逻辑委托给嵌套VaryingComponent

export default class TransferComponent extends React.Component {
constructor() {
super();
this.displayDOMButton = this.displayDOMButton.bind(this);
this.onButtonPress = this.onButtonPress.bind(this);
}
ajax(){
console.log('doing some ajax')
}
onButtonPress({ isValid }) {
if (isValid()) {
this.ajax();
}
}
displayDOMButton({ isValid }) {
document.getElementById('content-button').innerHTML = (
'<button id="button" type="button">Validate</button>'
);
document.getElementById('button')
.addEventListener('click', () => this.onButtonPress({ isValid }));
}
render() {
const { VaryingComponent } = this.props;
const { displayDOMButton } = this;
return (
<div>
<VaryingComponent>
{({ isValid, displayDOMInputs }) => {
displayDOMInputs();
displayDOMButton({ isValid });
return null;
}}
</VaryingComponent>
</div>
)
}
};

现在,我们创建VaryingComponent的具体实现,以充实各种输入显示和验证逻辑。

仅用户名实现:

export default class UsernameComponent extends React.Component {
isValid(){
return document.getElementById('username').value !== '';
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}

用户名和密码实现:

export default class UsernamePasswordComponent extends React.Component {
isValid(){
return (
document.getElementById('username').value !== '' &&
document.getElementById('password').value !== ''
);
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>n
<input type="text" id="password" value="password"/>n'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}

最后,组合TansferComponent实例如下所示:

<TransferComponent VaryingComponent={UsernameComponent} />
<TransferComponent VaryingComponent={UsernamePasswordComponent} />

非 React FP 示例

首先,在函数式编程中,函数是一等公民。这意味着您可以像对待 OOP 中的数据一样对待函数(即作为参数传递、分配给变量等)。

您的示例将数据与对象的行为混合在一起。为了编写纯函数式解决方案,我们需要将它们分开。

函数式编程从根本上讲是将数据与行为分开。

所以,让我们从isValid开始

功能有效

有几种方法可以在这里对逻辑进行排序,但我们将采用这个:

  1. 给定一个 id 列表
  2. 如果不存在无效的 id,则所有 id 都有效

在JS中,翻译为:

const areAllElementsValid = (...ids) => !ids.some(isElementInvalid)

我们需要几个辅助函数来完成这项工作:

const isElementInvalid = (id) => getValueByElementId(id) === ''
const getValueByElementId = (id) => document.getElementById(id).value

我们可以把所有这些写在一行上,但把它分解起来会让它更具可读性。有了这个,我们现在有一个通用函数,可以用来确定组件的isValid

areAllElementsValid('username') // TransferComponent.isValid
areAllElementsValid('username', 'password') // TransferOverrideComponent.isValid

功能渲染

我用documentisValid上作弊了一点。在真正的函数式编程中,函数应该是纯粹的。或者,换句话说,函数调用的结果必须仅从其输入(也称为幂等)确定,并且不能产生副作用

那么,我们如何渲染到 DOM 而没有副作用呢?好吧,React 使用了一个虚拟 DOM(一种存在于内存中的花哨的数据结构,并被传入和返回函数以保持函数纯度)作为核心库。React 的副作用存在于react-dom库中。

对于我们的案例,我们将使用一个超级简单的虚拟 DOM(类型string)。

const USERNAME_INPUT = '<input type="text" id="username" value="username"/>'
const PASSWORD_INPUT = '<input type="text" id="password" value="password"/>'
const VALIDATE_BUTTON = '<button id="button" type="button">Validate</button>'

这些是我们的组件- 使用 React 术语 - 我们可以将其组合到 UI 中:

USERNAME_INPUT + VALIDATE_BUTTON // TransferComponent.render
USERNAME_INPUT + PASSWORD_INPUT + VALIDATE_BUTTON // TransferOverrideComponent.render

这可能看起来过于简单化,根本不起作用。但+运算符实际上是功能性的!想想吧:

  • 它需要两个输入(左操作数和右操作数)
  • 它返回一个结果(对于字符串,操作数的串联)
  • 它没有副作用
  • 它不会改变其输入(结果是一个新的字符串 - 操作数保持不变)

因此,有了这个,render现在可以正常工作了!

阿贾克斯呢?

不幸的是,我们无法执行 ajax 调用、改变 DOM、设置事件侦听器或设置超时而不会产生副作用。我们可以走复杂的路线,为这些操作创建monads,但为了我们的目的,只要说我们将继续使用非函数式方法就足够了。

在 React 中应用它

下面是使用常见 React 模式重写的示例。我正在使用受控组件进行表单输入。我们讨论过的大多数函数概念实际上都存在于 React 的引擎盖下,所以这是一个非常简单的实现,没有使用任何花哨的东西。

class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
success: false
};
}
handleSubmit() {
if (this.props.isValid()) {
this.setState({
loading: true
});
setTimeout(
() => this.setState({
loading: false,
success: true
}),
500
);
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{this.props.children}
<input type="submit" value="Submit" />
</form>
{ this.state.loading && 'Loading...' }
{ this.state.success && 'Success' }
</div>
);
}
}

使用state可能看起来像是一种副作用,不是吗?在某些方面确实如此,但深入研究 React 内部可能会发现比从我们的单个组件中看到的更实用的实现。

下面是示例的Form。请注意,我们可以在这里以几种不同的方式处理提交。一种方法是将usernamepassword作为道具传递到Form(可能作为通用data道具)。另一种选择是传递特定于该表单的handleSubmit回调(就像我们为validate所做的那样)。

class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
isValid() {
return this.state.username !== '' && this.state.password !== '';
}
handleUsernameChange(event) {
this.setState({ username: event.target.value });
}
handlePasswordChange(event) {
this.setState({ password: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.username} onChange={this.handleUsernameChange} />
<input value={this.state.password} onChange={this.handlePasswordChange} />
</Form>
);
}
}

您也可以编写另一个Form但使用不同的输入

class CommentForm extends React.Component {
constructor(props) {
super(props);
this.state = {
comment: ''
};
}
isValid() {
return this.state.comment !== '';
}
handleCommentChange(event) {
this.setState({ comment: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.comment} onChange={this.handleCommentChange} />
</Form>
);
}
}

例如,你的应用可以呈现两种Form实现:

class App extends React.Component {
render() {
return (
<div>
<LoginForm />
<CommentForm />
</div>
);
}
}

最后,我们使用ReactDOM而不是innerHTML

ReactDOM.render(
<App />,
document.getElementById('content')
);

React 的功能性质通常通过使用 JSX 来隐藏。我鼓励你阅读一下我们正在做的事情实际上只是一堆组合在一起的函数。官方文档很好地涵盖了这一点。

为了进一步阅读,James K. Nelson在React上整理了一些优秀的资源,这些资源应该有助于你理解函数: https://reactarmory.com/guides/learn-react-by-itself/react-basics

阅读您的问题,不清楚您指的是组合还是继承,但它们是不同的 OOP 概念。如果您不知道它们之间的区别,我建议您查看本文。

关于您在 React 方面的具体问题。我建议您尝试用户组合,因为它为您提供了很大的灵活性来构建UI和传递道具。

例如,如果您正在使用 React,那么当您动态填充对话框时,您可能已经在使用合成。正如 React 文档所示:

function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}

Facebook 的人们一直在开发非常具有挑战性的 UI,并使用 React 构建数千个组件,但没有找到一个很好的继承而不是组合的用例。正如文档所说:

React 有一个强大的组合模型,我们建议使用组合而不是继承来重用组件之间的代码。

如果你真的想使用继承,他们的建议是将你想要在组件上重用的功能提取到一个单独的 JavaScript 模块中。组件可以导入它并使用该函数、对象或类,而无需扩展它。

在您提供的示例中,utils.js中的两个函数就可以了。看:

isUsernameValid = (username) => username !== '';
isPasswordValid = (password) => password !== '';

您可以导入它们并在组件中使用。

最新更新