简体   繁体   English

FP替代JavaScript / ReactJS中的多态

[英]FP alternative to polymorphism in JavaScript/ReactJS

I'm currently working on a ReactJS project where I need to create "re-usable" components in which some of the methods would need to be "overridden". 我目前正在开发一个ReactJS项目,我需要创建“可重用”组件,其中一些方法需要被“覆盖”。 In OOP I would use polymorphism. 在OOP中我会使用多态。 I've done some reading and it seems the consensus is to use HoC/composition but I can't quite figure out how to achieve this. 我已经做了一些阅读,似乎共识是使用HoC /组合但我无法弄清楚如何实现这一目标。 I figure if I could just get a ES6 sample using composition it would maybe be easier to adapt the idea to ReactJS afterwards. 我想如果我可以使用合成得到一个ES6样本,那么之后可能更容易将这个想法改编为ReactJS。

Below is a ES6 OOP example (ignore the event handling it's just for testing) of pretty much what I would like to achieve in ReactJS. 下面是我想在ReactJS中实现的ES6 OOP示例(忽略处理它仅用于测试的事件)。 Does anyone have some guidance on how to break up a ReactJS component into a HoC or even just demonstrate how I would go about using composition in ES6 based on the example? 有没有人对如何将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> 

UPDATE: Even though my original question was around FP I think render props is a really good solution to my issue and avoids the HoC issues. 更新:尽管我的原始问题是关于FP我认为渲染道具是我的问题的一个非常好的解决方案,并避免了HoC问题。

The answer regarding your example code is in the middle/bottom of this post. 关于您的示例代码的答案位于本文的中间/底部。

A good way to go about React composition is the render-callback pattern , aka function-as-child. 关于React组合的一个好方法是渲染回调模式 ,即function-as-child。 The main advantage it has over HOC's is that it allows you to compose components dynamically at runtime (eg in render) instead of statically at author time. 它相对于HOC的主要优点是它允许您在运行时动态组合组件(例如在渲染中)而不是在作者时静态组合组件。

Regardless of whether you use render-callback or HOC's, the goal in component composition is to delegate reusable behavior to other components , and then pass those components in as props to the component that needs them. 无论您使用渲染回调还是HOC,组件组合的目标都是将可重用行为委托给其他组件 ,然后将这些组件作为props传递给需要它们的组件。

Abstract example: 抽象例子:

The following Delegator component uses the render-callback pattern to delegate implementation logic to an ImplementationComponent that is passed in as a prop: 以下Delegator组件使用render-callback模式将实现逻辑委托给作为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>
    );
  }
}

The various implementation components would look like: 各种实现组件如下所示:

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 })
  }
} 

Later down the line, you could nest more child components in the Delegator component following the same compositional pattern: 接下来,您可以按照相同的组合模式在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>
    );
  }
}

Now nested child component allows for multiple concrete implementations: 现在嵌套的子组件允许多个具体实现:

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

    <Delegator 
      ImplementationComponent={ImplementationC}
      AnotherImplementation={AnotherImplementation2}
      SomethingElse={SomethingVariationZ}
    />
  </div>
); 

Answer (your example): 答案(你的例子):

Applying the above composition pattern to your example, the solution restructures your code but assumes that it needs to do the following: 将上述组合模式应用于您的示例,该解决方案重新构建您的代码,但假定它需要执行以下操作:

  • allow variations of inputs and their validation logic 允许输入的变化及其验证逻辑
  • when user submits valid input, then do some ajax 当用户提交有效输入时,请执行一些ajax

Firstly, to make things easier I changed the DOM to: 首先,为了简化操作,我将DOM更改为:

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

Now, the TransferComponent knows only how to display a button and do something when the button is pressed and the data is valid. 现在, TransferComponent只知道如何显示按钮,并在按下按钮并且数据有效时执行某些操作。 It doesn't know what inputs to display or how to validate the data. 它不知道要显示什么输入或如何验证数据。 It delegates that logic to the nested VaryingComponent . 它将该逻辑委托给嵌套的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>
    )
  }
};

Now we create concrete implementations of the VaryingComponent to flesh out various input display and validation logics. 现在我们创建VaryingComponent具体实现,以VaryingComponent各种输入显示和验证逻辑。

The username-only implementation: 仅用户名的实现:

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 });
  }
}

The username-and-password implementation: 用户名和密码实现:

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 });
  }
}

Finally, composing instances of the TansferComponent would look like: 最后,组成TansferComponent实例看起来像:

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

Reading your question, it's not clear if you are referring to composition or inheritance, but they are different OOP concepts. 阅读你的问题,不清楚你是指组成还是继承,但它们是不同的OOP概念。 I recommend you to take a look at this article if you don't know the difference between them. 如果你不知道它们之间的区别,我建议你看看这篇文章

About your specific problem with React. 关于React的具体问题。 I'd recommend you to try to user composition since it gives you a lot of flexibility to build your UI and pass props around. 我建议你尝试用户组合,因为它为你提供了很大的灵活性来构建你的UI和传递道具。

If you are working with React you are probably already using composition when you populate Dialogs dynamically, for example. 例如,如果您正在使用React,则可能在动态填充对话框时已经在使用合成。 As the React docs shows: 正如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>
  );
}

Folks at Facebook have been developing very challenging UIs and building thousand of components with React and didn't find a good use case for inheritance over composition. Facebook上的人们一直在开发非常具有挑战性的用户界面,并使用React构建了数千个组件,并没有找到一个很好的继承优于组合的用例。 As the docs says: 正如文档所说:

React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components. React有一个强大的组合模型,我们建议使用组合而不是继承来重用组件之间的代码。

If you really want to use inheritance, their recommendation is that you extract the functionality that you want to reuse over components into a separate JavaScript module. 如果您确实想要使用继承,那么他们的建议是将要重用的组件功能提取到单独的JavaScript模块中。 The components may import it and use that function, object, or a class, without extending it. 组件可以导入它并使用该函数,对象或类,而无需扩展它。

In the example you provided, two functions in a utils.js would do just fine. 在您提供的示例中, utils.js两个函数可以正常运行。 See: 看到:

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

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

You can import them and use in your components just fine. 您可以导入它们并在组件中使用就好了。

The non-React FP example 非React FP示例

For starters, in Functional Programming, the function is a first class citizen. 对于初学者来说,在功能编程中,该功能是一流的公民。 This means you can treat functions as you would data in OOP (ie pass as parameters, assign to variables, etc.). 这意味着您可以像处理OOP中的数据一样处理函数(即作为参数传递,分配给变量等)。

Your example comingles data with behavior in the objects. 您的示例将数据与对象中的行为混合在一起。 In order to write a purely functional solution, we'll want to separate these . 为了编写纯粹的功能解决方案, 我们希望将它们分开

Functional Programming is fundamentally about separating data from behavior. 功能编程从根本上讲是将数据与行为分离。

So, let's start with isValid . 所以,让我们从isValid开始吧。

Functional isValid 功能是有效的

There are a few ways to order the logic here, but we'll go with this: 有几种方法可以在这里订购逻辑,但我们将继续这样做:

  1. Given a list of ids 给出一个id列表
  2. All ids are valid if there does not exist an invalid id 如果不存在无效的ID,则所有ID都有效

Which, in JS, translates to: 在JS中,转换为:

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

We need a couple helper functions to make this work: 我们需要一些辅助函数来完成这项工作:

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

We could write all of that on one line, but breaking it up makes it a bit more readable. 我们可以在一行上写下所有内容,但是将其分解使它更具可读性。 With that, we now have a generic function that we can use to determine isValid for our components! 有了它,我们现在有了一个通用函数,我们可以用它来确定我们组件的isValid

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

Functional render 功能渲染

I cheated a little on isValid by using document . 我使用documentisValidisValid一点欺骗。 In true functional programming, functions should be pure . 在真正的函数式编程中,函数应该是纯粹的 Or, in other words, the result of a function call must only be determined from its inputs (aka it is idempotent) and it cannot have side effects . 或者,换句话说,函数调用的结果必须仅从其输入(也称为幂等)确定,并且它不能具有副作用

So, how do we render to the DOM without side effects? 那么,我们如何在没有副作用的情况下渲染DOM呢? Well, React uses a virtual DOM (a fancy data structure that lives in memory and is passed into and returned from functions to maintain functional purity) for the core library. 好吧,React使用一个虚拟DOM(一个花哨的数据结构,它存在于内存中并传递给函数并从函数返回以保持功能纯度),用于核心库。 React's side effects live in the react-dom library. React的副作用存在于react-dom库中。

For our case, we'll use a super simple virtual DOM (of type string ). 对于我们的情况,我们将使用一个超级简单的虚拟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>'

These are our components --to use the React terminology--which we can compose into UIs: 这些是我们的组件 - 使用React术语 - 我们可以将其组合到UI中:

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

This probably seems like an oversimplification and not functional at all. 这似乎过于简单化,根本不起作用。 But the + operator is in fact functional! +运算符实际上是功能性的! Think about it: 想一想:

  • it takes two inputs (the left operand and the right operand) 需要两个输入(左操作数和右操作数)
  • it returns a result (for strings, the concatenation of the operands) 它返回一个结果(对于字符串,操作数的串联)
  • it has no side effects 它没有副作用
  • it doesn't mutate its inputs (the result is a new string--the operands are unchanged) 它不会改变它的输入(结果是一个新的字符串 - 操作数不变)

So, with that, render is now functional! 那么, render现在正在发挥作用!

What about ajax? 怎么样的ajax?

Unfortunately, we can't perform an ajax call, mutate the DOM, set up event listeners, or set timeouts without side effects . 不幸的是,我们无法执行ajax调用,改变DOM,设置事件侦听器或设置没有副作用的超时。 We could go the complex route of creating monads for these actions, but for our purposes, suffice it to say that we'll just keep using the non-functional methods. 我们可以为这些行为创建复杂的monad路径,但就我们的目的而言,只需说我们将继续使用非功能性方法就足够了。

Applying it in React 在React中应用它

Here's a rewrite of your example using common React patterns. 这是使用常见的React模式重写您的示例。 I'm using controlled components for the form inputs. 我正在使用受控组件进行表单输入。 The majority of the functional concepts we've talked about really live under the hood in React, so this is a pretty simple implementation that doesn't use anything fancy. 我们所讨论的大多数功能概念实际上都存在于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>
        );
    }
}

The use of state probably seems like a side effect, doesn't it? 使用state可能看起来像副作用,不是吗? In some ways it is, but digging into the React internals may reveal a more functional implementation than can be seen from our single component. 在某种程度上它是,但是挖掘React内部结构可能会揭示出比我们的单个组件更多的功能实现。

Here's the Form for your example. 这是您的示例的Form Note that we could handle submission in a couple different ways here. 请注意,我们可以通过几种不同的方式处理提交。 One way is to pass the username and password as props into Form (probably as a generic data prop). 一种方法是将usernamepassword作为道具传递给Form (可能作为通用data道具)。 Another option is to pass a handleSubmit callback specific to that form (just like we're doing for validate ). 另一种选择是传递特定于该表单的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>
        );
    }
}

You could also write another Form but with different inputs 你也可以写另一个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>
        );
    }
}

For sake of example, your app can render both Form implementations: 为了便于示例,您的应用可以呈现两种Form实现:

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

Finally, we use ReactDOM instead of innerHTML 最后,我们使用ReactDOM而不是innerHTML

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

The functional nature of React is often hidden by using JSX. 通常使用JSX隐藏React的功能特性。 I encourage you to read up on how what we're doing is really just a bunch of functions composed together. 我鼓励你阅读我们正在做的事情,实际上只是一堆功能组合在一起。 The official docs cover this pretty well. 官方文档非常清楚。

For further reading, James K. Nelson has put together some stellar resources on React that should help with your functional understanding: https://reactarmory.com/guides/learn-react-by-itself/react-basics 为了进一步阅读,James K. Nelson在React上整合了一些有助于您的功能理解的一流资源: https//reactarmory.com/guides/learn-react-by-itself/react-basics

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM