【问题标题】:FP alternative to polymorphism in JavaScript/ReactJSFP 替代 JavaScript/ReactJS 中的多态性
【发布时间】:2018-04-24 06:48:29
【问题描述】:

我目前正在开发一个 ReactJS 项目,我需要创建“可重用”组件,其中一些方法需要“覆盖”。在 OOP 中,我会使用多态性。我已经阅读了一些内容,似乎共识是使用 HoC/composition,但我不太清楚如何实现这一点。我想如果我可以使用组合获得一个 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 问题。

【问题讨论】:

  • 我昨天刚回答了一个类似的问题:stackoverflow.com/a/47209919/633183
  • 感谢您的链接。是否可以调整我的命名和示例以便更有意义?我对 FP 很陌生,我正在努力理解你的例子。
  • 在 OOP 中,您也有对象组合和 AFAIK,它通常优于继承(也称为子类型多态性)。 HOC 只是意味着一个组件包含(或者更确切地说,因为它是一个函数/构造函数)另一个组件,它实现了特定的行为(has-a 关系)。这两个组件可以而且通常是不同的类型。
  • 如果您有机会,请查看 9 月的这次谈话。 MJ 提出了一个很好的观点,我现在的建议是“始终使用渲染道具来参数化 React 组件”。 youtu.be/BcVAq3YFiuc

标签: javascript reactjs ecmascript-6 functional-programming


【解决方案1】:

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

进行 React 组合的一个好方法是 render-callback pattern,也就是 function-as-child。它与 HOC 相比的主要优势在于它允许您在运行时(例如在渲染中)动态组合组件,而不是在创作时静态组合。

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

抽象例子:

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

各种实现组件如下所示:

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

【讨论】:

  • 虽然我不相信它比 HOC 更好,但我很欣赏这个链接,所以 +1 是我以前从未见过的独特技术。
  • 我给这个 -1 基本上与功能风格无关
  • 在花费大量研究时间 + 观看了 MJ 视频 bsapaka 链接后,我对渲染道具方法感到满意,尽管 FP 顽固分子会抱怨(尽管我的问题确实询问了 FP)。考虑到围绕 HoC 的问题,这是一个更好的 ReactJS 解决方案。
【解决方案2】:

非 React FP 示例

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

您的示例将 data 与对象中的 behavior 结合在一起。为了编写一个纯粹的功能解决方案,we'll want to separate these

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

那么,让我们从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 上作弊。在真正的函数式编程中,函数应该是纯的。或者,换句话说,函数调用的结果只能由它的输入来确定(也就是它是幂等的)并且它不能有side effects

那么,我们如何在没有副作用的情况下渲染到 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 呢?

不幸的是,我们无法在没有副作用的情况下执行 ajax 调用、改变 DOM、设置事件侦听器或设置超时。我们可以走复杂的路线为这些操作创建 monads,但就我们的目的而言,只要说我们将继续使用非功能性方法就足够了。

在 React 中应用它

这是使用常见 React 模式重写的示例。我将controlled components 用于表单输入。我们讨论过的大多数函数式概念实际上都存在于 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')
);

使用 JSX 通常会隐藏 React 的功能特性。我鼓励您仔细阅读我们所做的实际上只是一堆组合在一起的函数。 The official docs 很好地涵盖了这个。

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

【讨论】:

    【解决方案3】:

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

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

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

    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 !== '';
    

    你可以导入它们并在你的组件中使用就好了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-09
      • 2023-01-11
      • 1970-01-01
      相关资源
      最近更新 更多