资讯专栏INFORMATION COLUMN

React组件设计技巧

Luosunce / 1216人阅读

摘要:我们可以通过刚刚高阶函数的思想来创建一个中间组件,也就是我们说的高阶组件。仅传递组件所需要的属性。在受控组件中,表单数据由组件负责处理。作为顶层组件接收一个名为的,可以接收任意需要被放入中的字符串,数字,甚至是函数。

React组件设计 组件分类 展示组件和容器组件
展示组件 容器组件
关注事物的展示 关注事物如何工作
可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有DOM标签和css样式
常常允许通过this.props.children传递 提供数据和行为给容器组件或者展示组件
对第三方没有任何依赖,比如store 或者 flux action 调用flux action 并且提供他们的回调给展示组件
不要指定数据如何加载和变化 作为数据源,通常采用较高阶的组件,而不是自己写,比如React Reduxconnect(), Relay的createContainer(), Flux UtilsContainer.create()
仅通过属性获取数据和回调 null
很少有自己的状态,即使有,也是自己的UI状态 null
除非他们需要的自己的状态,生命周期,或性能优化才会被写为功能组件 null

下面是一个可能会经常写的组件,评论列表组件,数据交互和展示都放到了一个组件里面。

// CommentList.js
class CommentList extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: "json",
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return 
    {this.state.comments.map(renderComment)}
; } renderComment({body, author}) { return
  • {body}—{author}
  • ; } }

    我们对上面的组件进行拆分,把他拆分成容器组件 CommentListContainer.js 和展示组件 CommentList

    // CommentListContainer.js
    class CommentListContainer extends React.Component {
      constructor() {
        super();
        this.state = { comments: [] }
      }
      componentDidMount() {
        $.ajax({
          url: "/my-comments.json",
          dataType: "json",
          success: function(comments) {
            this.setState({comments: comments});
          }.bind(this)
        });
      }
      render() {
        return ;
      }
    }
    
    
    // CommentList.js
    class CommentList extends React.Component {
      constructor(props) {
        super(props);
      }
      render() { 
        return 
      {this.props.comments.map(renderComment)}
    ; } renderComment({body, author}) { return
  • {body}—{author}
  • ; } }

    优势:

    展示和容器更好的分离,更好的理解应用程序和UI

    重用性高,展示组件可以用于多个不同的state数据源

    展示组件就是你的调色板,可以把他们放到多带带的页面,在不影响应用程序的情况下,让设计师调整UI

    迫使你分离标签,达到更高的可用性

    有状态组件和无状态组件

    下面是一个最简单的无状态组件的例子:

    function HelloComponent(props, /* context */) {
      return 
    Hello {props.name}
    } ReactDOM.render(, mountNode)

    可以看到,原本需要写“类”定义(React.createClass 或者 class YourComponent extends React.Component)来创建自己组件的定义(有状态组件),现在被精简成了只写一个 render 函数。更值得一提的是,由于仅仅是一个无状态函数,React 在渲染的时候也省掉了将“组件类” 实例化的过程。

    结合 ES6 的解构赋值,可以让代码更精简。例如下面这个 Input 组件:

    function Input({ label, name, value, ...props }, { defaultTheme }) {
      const { theme, autoFocus, ...rootProps } = props
      return (
        

    无状态组件不像上述两种方法在调用时会创建新实例,它创建时始终保持了一个实例,避免了不必要的检查和内存分配,做到了内部优化。

    无状态组件不支持 "ref"
    高阶组件

    高阶组件通过函数和闭包,改变已有组件的行为,本质上就是 Decorator 模式在 React 的一种实现。

    当写着写着无状态组件的时候,有一天忽然发现需要状态处理了,那么无需彻底返工:)
    往往我们需要状态的时候,这个需求是可以重用的。

    高阶组件加无状态组件,则大大增强了整个代码的可测试性和可维护性。同时不断“诱使”我们写出组合性更好的代码。

    高阶函数
    function welcome() {
        let username = localStorage.getItem("username");
        console.log("welcome " + username);
    }
    
    function goodbey() {
        let username = localStorage.getItem("username");
        console.log("goodbey " + username);
    }
    
    welcome();
    goodbey();

    我们发现两个函数有一句代码是一样的,这叫冗余唉。(平时可能会有一大段代码的冗余)。

    下面我们要写一个中间函数,读取username,他来负责把username传递给两个函数。

    function welcome(username) {
        console.log("welcome " + username);
    }
    
    function goodbey(username) {
        console.log("goodbey " + username);
    }
    
    function wrapWithUsername(wrappedFunc) {
        let newFunc = () => {
            let username = localStorage.getItem("username");
            wrappedFunc(username);
        };
        return newFunc;
    }
    
    welcome = wrapWithUsername(welcome);
    goodbey = wrapWithUsername(goodbey);
    
    welcome();
    goodbey();

    好了,我们里面的 wrapWithUsername 函数就是一个“高阶函数”。
    他做了什么?他帮我们处理了 username,传递给目标函数。我们调用最终的函数 welcome的时候,根本不用关心 username是怎么来的。

    举一反三的高阶组件

    下面是两个冗余的组件。

    import React, {Component} from "react"
    
    class Welcome extends Component {
        constructor(props) {
            super(props);
            this.state = {
                username: ""
            }
        }
    
        componentWillMount() {
            let username = localStorage.getItem("username");
            this.setState({
                username: username
            })
        }
    
        render() {
            return (
                
    welcome {this.state.username}
    ) } } export default Welcome;
    import React, {Component} from "react"
    
    class Goodbye extends Component {
        constructor(props) {
            super(props);
            this.state = {
                username: ""
            }
        }
    
        componentWillMount() {
            let username = localStorage.getItem("username");
            this.setState({
                username: username
            })
        }
    
        render() {
            return (
                
    goodbye {this.state.username}
    ) } } export default Goodbye;

    我们可以通过刚刚高阶函数的思想来创建一个中间组件,也就是我们说的高阶组件。

    import React, {Component} from "react"
    
    export default (WrappedComponent) => {
        class NewComponent extends Component {
            constructor() {
                super();
                this.state = {
                    username: ""
                }
            }
    
            componentWillMount() {
                let username = localStorage.getItem("username");
                this.setState({
                    username: username
                })
            }
    
            render() {
                return 
            }
        }
    
        return NewComponent
    }
    import React, {Component} from "react";
    import wrapWithUsername from "wrapWithUsername";
    
    class Welcome extends Component {
    
        render() {
            return (
                
    welcome {this.props.username}
    ) } } Welcome = wrapWithUsername(Welcome); export default Welcome;
    import React, {Component} from "react";
    import wrapWithUsername from "wrapWithUsername";
    
    class Goodbye extends Component {
    
        render() {
            return (
                
    goodbye {this.props.username}
    ) } } Goodbye = wrapWithUsername(Goodbye); export default Goodbye;

    看到没有,高阶组件就是把 username 通过 props 传递给目标组件了。目标组件只管从 props里面拿来用就好了。

    为了代码的复用性,我们应该尽量减少代码的冗余。

    提取共享的state,如果有两个组件都需要加载同样的数据,那么他们会有相同的 componentDidMount 函数。

    找出重复的代码,每个组件中constructor 和 componentDidMount都干着同样的事情,另外,在数据拉取时都会显示Loading... 文案,那么我们应该思考如何使用高阶组件来提取这些方法。

    迁移重复的代码到高阶组件

    包裹组件,并且使用props替换state

    尽可能地简化

    组件开发基本思想 单功能原则

    使用react时,组件或容器的代码在根本上必须只负责一块UI功能。

    让组件保持简单

    如果组件根本不需要状态,那么就使用函数定义的无状态组件。

    从性能上来说,函数定义的无状态组件 > ES6 class 定义的组件 > 通过 React.createClass() 定义的组件。

    仅传递组件所需要的属性。只有当属性列表太长时,才使用{...this.props}进行传递。

    如果组件里面有太多的判断逻辑(if-else语句)通常意味着这个组件需要被拆分成更细的组件或模块。

    使用明确的命名能够让开发者明白它的功能,有助于组件复用。

    基本准则

    shouldComponentUpdate中避免不必要的检查.

    尽量使用不可变数据类型(Immutable).

    编写针对产品环境的打包配置(Production Build).

    通过Chrome Timeline来记录组件所耗费的资源.

    componentWillMount或者componentDidMount里面通过setTimeOut或者requestAnimationFram来延迟执行那些需要大量计算的任务.

    组件开发技巧 form表单里的受控组件和不受控组件 受控组件

    在大多数情况下,我们推荐使用受控组件来实现表单。在受控组件中,表单数据由 React 组件负责处理。下面是一个典型的受控组建。

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {value: ""};
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert("A name was submitted: " + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          
    ); } }

    设置表单元素的value属性之后,其显示值将由this.state.value决定,以满足React状态的同一数据理念。每次键盘敲击之后会执行handleChange方法以更新React状态,显示值也将随着用户的输入改变。

    对于受控组件来说,每一次 state(状态)变化都会伴有相关联的处理函数。这使得可以直接修改或验证用户的输入和提交表单。

    不受控组件

    因为不受控组件的数据来源是 DOM 元素,当使用不受控组件时很容易实现 React 代码与非 React 代码的集成。如果你希望的是快速开发、不要求代码质量,不受控组件可以一定程度上减少代码量。否则。你应该使用受控组件。

    一般情况下不受控组件我们使用ref来获取DOM元素进行操作。

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleSubmit(event) {
        alert("A name was submitted: " + this.input.value);
        event.preventDefault();
      }
    
      render() {
        return (
          
    ); } }
    组件条件判断 三元函数组件判断渲染
    const sampleComponent = () => {
      return isTrue ? 

    True!

    :

    false!

    };
    使用&&表达式替换不必要的三元函数
    const sampleComponent = () => {
      return isTrue ? 

    True!

    : };
    const sampleComponent = () => {
      return isTrue && 

    True!

    };

    需要注意的是如果isTrue 为 0 ,其实会转换成 false,但是在页面中显示的时候,&&还是会返回0显示到页面中。

    多重嵌套判断
    // 问题代码
    const sampleComponent = () => {
      return (
        
    {flag && flag2 && !flag3 ? flag4 ?

    Blah

    : flag5 ?

    Meh

    :

    Herp

    :

    Derp

    }
    ) };

    解决方案:

    最佳方案: 将逻辑移到子组件内部

    使用IIFE(Immediately-Invoked Function Expression 立即执行函数)

    满足条件的时候使用return强制跳出函数

    const sampleComponent = () => {
      const basicCondition = flag && flag2 && !flag3;
      if (!basicCondition) return 

    Derp

    ; if (flag4) return

    Blah

    ; if (flag5) return

    Meh

    ; return

    Herp

    }
    setState异步性

    在某些情况下,React框架出于性能优化考虑,可能会将多次state更新合并成一次更新。正因为如此,setState实际上是一个异步的函数。 如果在调用setState()函数之后尝试去访问this.state,你得到的可能还是setState()函数执行之前的结果。

    但是,有一些行为也会阻止React框架本身对于多次state更新的合并,从而让state的更新变得同步化。 比如: eventListeners, Ajax, setTimeout 等等。

    React框架之所以在选择在调用setState函数之后立即更新state而不是采用框架默认的方式,即合并多次state更新为一次更新,是因为这些函数调用(fetch,setTimeout等浏览器层面的API调用)并不处于React框架的上下文中,React没有办法对其进行控制。React在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state)。

    解决setState函数异步的办法?

    根据React官方文档,setState函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState函数执行后的回调。通过传入回调函数的方式,React可以保证传入的回调函数一定是在setState成功更新this.state之后再执行。

    this.setState({count: 1}, () => {
        console.log(this.state.count); // 1
    })
    React源码中setState的实现
    ReactComponent.prototype.setState = function(partialState, callback) {
      invariant(
        typeof partialState === "object" ||
        typeof partialState === "function" ||
        partialState == null,
        "setState(...): takes an object of state variables to update or a " +
        "function which returns an object of state variables."
      );
      this.updater.enqueueSetState(this, partialState);
      if (callback) {
        this.updater.enqueueCallback(this, callback, "setState");
      }
    };

    updater的这两个方法,和React底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOMdiff算法。

    依赖注入

    React中,想做依赖注入(Dependency Injection)其实相当简单。可以通过props来进行传递。但是,如果组件数量很多,并且组件嵌套层次很深的话,这种方式就不太合适。

    高阶组件
    // inject.jsx
    var title = "React Dependency Injection";
    export default function inject(Component) {
      return class Injector extends React.Component {
        render() {
          return (
            
          )
        }
      };
    }
    // Title.jsx
    export default function Title(props) {
      return 

    { props.title }

    ; }
    // Header.jsx
    import inject from "./inject.jsx";
    import Title from "./Title.jsx";
    
    var EnhancedTitle = inject(Title);
    export default function Header() {
      return (
        
    ); }
    context

    React v16.3.0 之前的 Context:

    var context = { title: "React in patterns" };
    class App extends React.Component {
      getChildContext() {
        return context;
      }
      // ...
    }
    
    App.childContextTypes = {
      title: PropTypes.string
    };
    class Inject extends React.Component {
      render() {
        var title = this.context.title;
      // ...
      }
    }
    Inject.contextTypes = {
      title: PropTypes.string
    };

    之前的 Context 作为一个实验性质的 API,直到 React v16.3.0 版本前都一直不被官方所提倡去使用,其主要原因就是因为在子组件中使用 Context 会破坏 React 应用的分型架构。

    这里的分形架构指的是从理想的 React 应用的根组件树中抽取的任意一部分都仍是一个可以直接运行的子组件树。在这个子组件树之上再包一层,就可以将它无缝地移植到任意一个其他的根组件树中。

    但如果根组件树中有任意一个组件使用了支持透传的 Context API,那么如果把包含了这个组件的子组件树多带带拿出来,因为缺少了提供 Context 值的根组件树,这时的这个子组件树是无法直接运行的。

    并且他有一个致命缺陷:任何一个中间传递的组件shouldComponentUpdate 函数返回false,组件都不会得到更新。

    新的Context Api

    新的Context Api 采用声明式的写法,并且可以透过shouldComponentUpdate 函数返回false的组件继续向下传播,以保证目标组件一定可以接收到顶层组件 Context 值的更新,一举解决了现有 Context API 的两大弊端,也终于成为了 React 中的第一级(first-class) API

    新的 Context API 分为三个组成部分:

    React.createContext 用于初始化一个 Context

    XXXContext.Provider作为顶层组件接收一个名为 valueprop,可以接收任意需要被放入 Context 中的字符串,数字,甚至是函数。

    XXXContext.Consumer作为目标组件可以出现在组件树的任意位置(在 Provider 之后),接收 children prop,这里的 children 必须是一个函数(context => ())用来接收从顶层传来的 Context

    const ThemeContext = React.createContext("light");
    
    class App extends React.Component {
      render() {
        return (
          
            
          
        );
      }
    }
    
    function Toolbar(props) {
      return (
        
    ); } function ThemedButton(props) { return ( {theme =>
    事件处理中的this指向问题
    class Switcher extends React.Component {
      constructor(props) {
        super(props);
        this.state = { name: "React in patterns" };
      }
      render() {
        return (
          
        );
      }
    
      _handleButtonClick() {
        console.log(`Button is clicked inside ${ this.state.name }`);
        // 将导致
        // Uncaught TypeError: Cannot read property "state" of null
      }
    }

    我们可以通过下面三种方式简单实现this指向的绑定:

    constructor 中事先绑定 this._buttonClick = this._handleButtonClick.bind(this);

    调用时使用箭头函数

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/95244.html

    相关文章

    • 前端每周清单:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧

      摘要:前端每周清单第期微服务实践,与,组件技巧,攻防作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 前端每周清单第 26 期:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧,HeadlessChrome 攻防 作者:王下邀月熊 编辑:徐川...

      wall2flower 评论0 收藏0
    • 精读《源码学习》

      摘要:精读原文介绍了学习源码的两个技巧,并利用实例说明了源码学习过程中可以学到许多周边知识,都让我们受益匪浅。讨论地址是精读源码学习如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 1. 引言 javascript-knowledge-reading-source-code 这篇文章介绍了阅读源码的重要性,精读系列也已有八期源码系列文章,分别是: 精读《Immer.js》源...

      aboutU 评论0 收藏0
    • Vue.js 的注意事项与技巧

      摘要:需要注意的是,同样的行为也适用于。这意味着我们必须重新绑定每个事件。组件的由调用它的父组件提供,这意味着所有事件都应该与父组件相关联。 原文链接:Vue.js — Considerations and Tricks showImg(https://segmentfault.com/img/bVbqHOd?w=1600&h=1599); Vue.js 是一个很棒的框架。然而,当你开始构建...

      lsxiao 评论0 收藏0
    • 前端资源系列(4)-前端学习资源分享&前端面试资源汇总

      摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...

      princekin 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <