React技术栈进阶之路之设计模式篇

文章的代码和最新内容请在github查看,也欢迎您star,issue

1.Redux导致的组件多余的渲染问题

请仔细阅读React 组件间通讯的文章的最后一个例子,最后的输出结果为:

这是因为在最后一个定时器中是如下的代码:

setTimeout(() => {
      store.dispatch({
        type: 'child_2_1',data: 'bye'
      })
    },2000);

此时你必须了解redux的观察者模式,当你dispatch一个事件的时候,我们的Child_2与Child_2_1中subscribe的事件都会被执行,所以首先是Child_2_1直接进行了一次更新,然后接着由于Child_2的更新有发生了一次更新,所以Child_2_1总共在后面的dispatch中输出了两次,即渲染了两次。对于redux的观察者模式不了解的可以查看redux原理分析。同时,该文章给出了react兄弟组件通信的方法:即采用观察者模式或者redux(实际上也是观察者模式)来解决

2.React开发中那些设计模式

(1)使用this.props.children来处理组件的低耦合。此时,父组件可以访问和读取子组件。而我们以前直接将Navigator组件写入到Header组件的方式一方面使得测试不容易(当然,在测试中可以使用shallow-rendering的方式来解决),另一方面我们的Header和Navigator将会强耦合,对于一些不需要Navigator的Header组件来说就无法实现复用

(2)高阶组件可以很容易的实现逻辑的复用,对于一些重复的逻辑可以考虑使用高阶组件来完成

(3)我们写的大部分的模块和组件都会存在依赖关系,特别是当树形的组件树存在的时候,父子关系的依赖就会出现了。因此,项目成功的一个关键就是如何去管理这些依赖关系。此时你应该要考虑到一个广为人知的设计模式,也就是
我们所说的:依赖注入。我们给出下面的例子:

// Title.jsx
export default function Title(props) {
  return <h1>{ props.title }</h1>;
}
// Header.jsx
import Title from './Title.jsx';
export default function Header() {
  return (
    <header> <Title /> </header> ); } // App.jsx import Header from './Header.jsx'; class App extends React.Component { constructor(props) { super(props); this.state = { title: 'React in patterns' }; } render() { return <Header />;
  }
};

此时,假如你有一个需求,即你需要将“React in patterns”这个字符串传递给我们的Title组件。最直接的方法就是将属性从最顶层的App组件逐步传递到我们的Title组件。然而,当我们的属性很多或者组件嵌套的非常深的时候,那么很多中间的组件就需要处理那些他们根本不感兴趣的属性。此时,你可以考虑我们这里说的高阶组件的方法:

// enhance.jsx
var title = 'React in patterns';
var enhanceComponent = (Component) =>
  class Enhance extends React.Component {
    render() {
      return (
        <Component  {...this.state} {...this.props} title={ title } /> ) } }; // Header.jsx import enhance from './enhance.jsx'; import Title from './Title.jsx'; var EnhancedTitle = enhance(Title); //此时经过我们的enchance方法的处理,我们的Title组件会接受到一个title属性 export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }

注意:此时我们的title属性并没有从Header组件传递过去,而是直接在我们的Title组件中通过高阶组件的形式进行了注入了。我们的title属性对于高阶组件本身来说是透明的,他根本不知道我们要给最终的Title组件传递title属性。这很好,但是这只是解决了一半的问题,我们虽然不需要将title属性通过组件树的形式传递下去了,但是我们的数据该如何到达enchance.jsx呢?
React引入了context的概念,每一个组件都可以访问我们的context属性。他就像是一个事件系统,但是他是为了数据而设计的。

// a place where we'll define the context
var context = { title: 'React in patterns' };
class App extends React.Component {
  getChildContext() {
    return context;
  }
  ...
};
App.childContextTypes = {
  title: React.PropTypes.string
};

// a place where we need data
class Inject extends React.Component {
  render() {
    var title = this.context.title;
    ...
  }
}
Inject.contextTypes = {
  title: React.PropTypes.string
};

注意:我们需要完整的指定context对象的签名,即使用childContextType和getChildContext。如果任意一项没有指定,那么我们的context对象就是空对象。但是这似乎不太合理,我们可能需要在context中指定任意类型的数据。最佳实践告诉我们,context不应该仅仅是一个纯对象,而同时应该提供一个接口用于保存和获取数据,例如:

// dependencies.js
export default {
  data: {},get(key) {
    return this.data[key];
  },register(key,value) {
    this.data[key] = value;
  }
}

因此,回到上面的例子,我们的App组件最终会是下面的形式:

import dependencies from './dependencies';
dependencies.register('title','React in patterns');
class App extends React.Component {
  getChildContext() {
    return dependencies;
  }
  render() {
    return <Header />;
  }
};
App.childContextTypes = {
  data: React.PropTypes.object,get: React.PropTypes.func,register: React.PropTypes.func
};

而我们的Title组件通过context来获取到数据:

// Title.jsx
export default class Title extends React.Component {
  render() {
    return <h1>{ this.context.get('title') }</h1> } } Title.contextTypes = { data: React.PropTypes.object,register: React.PropTypes.func };

当然,我们并不想在任何需要使用context地方都声明一下contextTypes,而这个细节我们可以包裹到我们的高阶组件中。同样的,我们可以声明一个工具函数而来帮助我们操作context,而不是使用this.context.get(‘title’)这种方式来获取我们需要的值,此时我们直接告诉高阶组件我们需要哪些数据,然后让他通过prop的形式传递给我们最终的组件,如下:

// Title.jsx
import wire from './wire';

function Title(props) {
  return <h1>{ props.title }</h1>;
}

export default wire(Title,['title'],function resolve(title) {
  return { title };
});

这个wire方法第一个参数是我们的React组件,第二个参数是所有已经注册的依赖的数组,第三个参数可以叫做mapper,是一个函数。这个函数会接受到我们保存在context中的值,然后返回给我们的是一个对象,这个对象中保存的是我们组件,如Title组件真实的prop属性。在这个例子中,我们只是传递了一个title的字符串,但是在一个大型的应用中很可能就是一个数据仓库,配置信息或者其他类型的信息。因此,我们要做到,传递给组件的prop属性是这个组件真实需要的,而不要使用无用的数据来污染组件。

下面是wire函数的签名:

export default function wire(Component,dependencies,mapper) {
  class Inject extends React.Component {
    render() {
      var resolved = dependencies.map(this.context.get.bind(this.context));
      var props = mapper(...resolved);

      return React.createElement(Component,props);
    }
  }
  Inject.contextTypes = {
    data: React.PropTypes.object,register: React.PropTypes.func
  };
  return Inject;
};

Inject是一个高阶组件,他会访问context属性,然后获取哪些dependencies指定的属性。而我们的mapper方法会获取context数据,然后将它转化为组件的props.

(4)单向数据交流

单向数据流是React中一个很重要的概念。他的主流思想是:组件本身不修改他们接受到的props属性,他们仅仅是监听数据的变化,或者提供一个新的数据(用于更新数据),但是他们不会直接更新store中的数据。而store中数据的更新是来自于另外一个机制,另外一个地方,而组件本身只会通过这个更新的数据来重新渲染而已。

下面给出Switcher的例子:

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { flag: false };
    this._onButtonClick = e => this.setState({ flag: !this.state.flag });
  }
  render() {
    return (
      <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; // ... and we render it class App extends React.Component { render() { return <Switcher />;
  }
};

在这个例子中,我们的组件本身是有数据的。Switcher是唯一一个地方知道我们flag属性的值的。下面我们展示一个store的例子,此时不仅Switcher我们的store本身也是知道flag的:

var Store = {
  _flag: false,set: function(value) {
    this._flag = value;
  },get: function() {
    return this._flag;
  }
};

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { flag: false };
    this._onButtonClick = e => {
      this.setState({ flag: !this.state.flag },() => {
        this.props.onChange(this.state.flag);
      });
    }
  }
  render() {
    return (
      <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; class App extends React.Component { render() { return <Switcher onChange={ Store.set.bind(Store) } />;
  }
};

这个例子的的数据交流就是双向的,我们点击按钮的时候,React组件的状态发生变化导致重新渲染,这是可以理解的。但是点击按钮的时候,我们也会更新store的内容,因此我们就需要一种机制保证store和组件本身维护状态的一致(组件本身的状态通过state来维护,比如其他地方ajax请求导致store也变化了,那么我们就需要同步这种变化,如广播)。此时我们的数据流状态如下:

    User's input
     |
  Switcher  Store
                      ^ |
                      | |
                      | |
                      | v
    Service communicating
    with our backend

而我们的单向数据流就解决了这个问题。他去除了多状态,而只是维护store这一种状态。为了实现这种效果,我们需要修改我们的store对象,使得我们可以订阅store的改变:

var Store = {
  _handlers: [],_flag: '',onChange: function(handler) {
    this._handlers.push(handler);
  },set: function(value) {
    this._flag = value;
    this._handlers.forEach(handler => handler())
  },get: function() {
    return this._flag;
  }
};

此时,我们需要对我们的App组件进行处理,每次store发生变化的时候都重新渲染我们的组件:

class App extends React.Component {
  constructor(props) {
    super(props);
    Store.onChange(this.forceUpdate.bind(this));
  }
  render() {
    return (
      <div> <Switcher  value={ Store.get() } onChange={ Store.set.bind(Store) } /> </div> ); } };

注意:此处我们使用了forceUpdate,但是我们不推荐这样做,具体的使用你可以参考高阶组件重新渲染

此时我们的Switcher组件变得非常简单,我们不需要维持内部的状态了:

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this._onButtonClick = e => {
      this.props.onChange(!this.props.value);
    }
  }
  render() {
    return (
      <button onClick={ this._onButtonClick }> { this.props.value ? 'lights on' : 'lights off' } </button> ); } }

我们这种方式的好处在于:我们的组件本身只是store状态的一种反映。我们只是将React组件作为views层,而且我们只需要在一个地方来管理我们的状态。此时我们的数据流是如下的形式:

Service communicating
with our backend
    ^
    |
    v
  Store 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom