React学习—React漫谈

前端阅读室

事件系统

React基于Virtual DOM实现了一个SyntheticEvent(合成事件)层,咱们定义的处理器会接收一个SyntheticEvent对象的实例,它彻底符合W3C标准,不会存在任何IE的兼容性问题。而且与原生的浏览器事件同样拥有一样的接口,一样支持事件的冒泡机制,我门可使用stopPropagation()和preventDefault()来中断它。若是须要访问原生事件对象,可使用nativeEvent属性。css

合成事件的绑定方式

React事件的绑定方式与原生的HTML事件监听器属性很类似。html

<button onClick={this.handleClick}>Test</button>

合成事件的实现机制

在React底层,主要对合成事件作了两件事:事件委派和自动绑定。前端

1.事件委派react

React不会把事件处理函数直接绑定到真实的节点上,而是把全部事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存全部组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,而后在映射里找到真正的事件处理函数并调用。(实现原理:对最外层的容器进行绑定,依赖事件的冒泡机制完成委派。)这样简化了事件处理和回收机制,效率也有很大提高。webpack

2.自动绑定web

在React组件中,每一个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。并且React还会对这种引用进行缓存。在使用ES6 classes或者纯函数时,这种自动绑定就不复存在了,咱们须要手动实现this的绑定。
咱们来看几种绑定方法
bind方法算法

class App extends Component {
  constuctor() {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
}

箭头函数能够自动绑定此函数的做用域的this数据库

class App extends Component {
  handleClick= () => {}
}

在React中使用原生事件

class NativeEventDemo extends Component {
  componentDidMount() {
    this.refs.button.addEventListener('click', this.handleClick)
  }
  componentWillUnmout() {
    this.refs.button.removeEventListener('click', this.handleClick)
  }
}

对比React合成事件与JavaScript原生事件

1.事件传播与阻止事件传播编程

浏览器原生DOM事件的传播能够分为3个阶段:事件捕获阶段、目标对象自己的事件处理程序调用、事件冒泡。能够将e.addEventListener的第三个参数设置为true时,为元素e注册捕获事件处理程序。事件捕获在IE9如下没法使用。事件捕获在应用程序开发中意义不大,React在合成事件中并无实现事件捕获,仅仅支持了事件冒泡机制。后端

阻止原生事件传播须要使用e.stopPropagation,不过对于不支持该方法的浏览器(IE9如下)只能使用e.cancelBubble = true来阻止。而在React合成事件中,只须要使用stopPropagation()便可。阻止React事件冒泡的行为只能用于React合成事件系统中,且没有办法阻止原生事件的冒泡。反之,原生事件阻止冒泡,能够阻止React合成事件的传播。

2.事件类型

React合成事件的事件类型是JavaScript原生事件类型的一个子集。它仅仅实现了DOM Level3的事件接口,而且统一了浏览器的兼容问题。有些事件React没有实现,或者受某些限制没办法去实现,如window的resize事件。

3.事件绑定方式

受到DOM标准影响,浏览器绑定原生事件的方式有不少种。React合成事件的绑定方式则简单不少

<button onClick={this.handleClick}>Test</button>

4.事件对象

在React合成事件系统中,不存在兼容性问题,能够获得一个合成事件对象。

表单

在React中,一切数据都是状态,固然也包括表单数据。接下来咱们讲讲React是如何处理表单的。

应用表单组件

html表单中的全部组件在React的JSX都有实现,只是它们在用法上有些区别,有些是JSX语法上的,有些则是因为React对状态处理上致使的一些区别。

1.文本框

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      inputValue: '',
      textareaValue: ''
    }
  }

  handleInputChange = (e) => {
    this.setState({
      inputValue: e.target.value
    });
  }

  handleTextareaChange = (e) => {
    this.setState({
      textareaValue: e.target.value
    })
  }

  render() {
    const { inputValue, textareaValue } = this.state;
    return (
      <div>
        <p>
          单行输入框:
          <input type="text" value={inputValue} onChange={this.handleInputChange}/>
        </p>
        <p>
          多行输入框:
          <textarea type="text" value={textareaValue} onChange={this.handleTextareaChange}/>
        </p>
      </div>
    )
  }

}

在HTML中textarea的值是经过children来表示的,而在react中是用一个value prop来表示表单的值的。

2.单选按钮与复选框

在HTML中,用类型为radio的input标签表示单选按钮,用类型为checkbox的input标签表示复选框。这两种表单的value值通常是不会改变的,而是经过一个布尔类型的checked prop来表示是否为选中状态。在JSX中这些是相同的,不过用法上仍是有些区别。

单选按钮的示例

import React, { Component } from 'react';

class App extends Component {
  construtor(props) {
    super(props);
    this.state = {
      radioValue: '',
    }
  }

  handleChange = (e) => {
    this.setState(
      radioValue: e.target.value
    )
  }

  render() {
    const { radioValue } = this.state;

    return (
      <div>
        <p>gender:</p>
        <label>
          male:
          <input 
            type="radio"
            value="male"
            checked={radioValue === 'male'}
            onChange={this.handleChange}
          />
        </label>
        <label>
          female:
          <input 
            type="radio"
            value="female"
            checked={radioValue === 'female'}
            onChange={this.handleChange}
          />
        </label>
      </div>
    )
  }
}

复选按钮的示例

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props)
    
    this.state = {
      coffee: []
    }
  }

  handleChange = (e) => {
    const { checked, value } = e.target;
    let { coffee } = this.state;

    if (checked && coffee.indexOf(value) === -1) {
      coffee.push(value)
    } else {
      coffee = coffee.filter(i => i !== value)
    }

    this.setState({
      coffee,
    })
  }

  render() {
    const { coffee } = this.state;
    return (
      <div>
        <p>请选择你最喜欢的咖啡</p>
        <label>
          <input 
            type="checkbox"
            value="Cappuccino"
            checked={coffee.indexOf('Cappuccino') !== -1}
            onChange={this.handleChange}
          />
          Cappuccino
        </label>
        <br />
        <label>
          <input 
            type="checkbox"
            value="CafeMocha"
            checked={coffee.indexOf('CafeMocha') !== -1}
            onChange={this.handleChange}
          />
          CafeMocha
        </label>
      </div>
    )
  }
}

3.Select组件

在HTML的select元素中,存在单选和多选两种。在JSX语法中,一样能够经过设置select标签的multiple={true}来实现一个多选下拉列表。

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      area: ''
    }
  }

  handleChange = (e) => {
    this.setState({
      area: e.target.value
    })
  }

  render() {
    const { area } = this.state;

    return (
      <select value={area} onChange={this.handleChange}>
        <option value='beijing'>北京</option>
        <option value='shangehai'>上海</option>
      </select>
    )
  }
}

select元素设置multiple={true}的示例

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      area: ['beijing', 'shanghai']
    }
  }

  handleChange = (e) => {
    const { options } = e.target;
    const area = Object.keys(options)
      .filter(i => options[i].selected === true)
      .map(i => options[i].value);

    this.setState({
      area,
    })
  }

  render () {
    const { area } = this.state;

    return (
      <select multiple={true} value={area} onChange={this.handleChange}>
        <option value="北京">北京</option>
        <option value="上海">上海</option>
      </select>
    )
  }
}

在HTMl的option组件须要一个selected属性来表示默认选中的列表项,而React的处理方式是经过为select组件添加value prop来表示选中的option,在必定程度上统一了接口。

实际上,也能够写成这种形式,不过开发体验就会差不少,React也会抛警告。

<select multiple={true} onChange={this.handleChange}>
  <option value="beijing" selected={area.indexOf('beijing') !== -1}>北京</option>
  <option value="shanghai" selected={area.indexOf('shanghai') !== -1}>上海</option>
</select>

受控组件

每当表单的状态发生变化,都会被写入到组件的state中,这种组件在React中被称为受控组件。在受控组件中,组件渲染出的状态与它的value或checked prop相对应。React经过这种方式消除了组件的局部状态,使得应用的整个状态更加可控。

非受控组件

若是一个表单组件没有value prop(或checked prop),就能够称之为非受控组件。相应的你可使用defaultValue和defaultChecked prop来表示组件的默认状态。

class App extends Compoent {
  constructor(props) {
    super(props)

  }

  handleSubmit = (e) => {
    e.preventDefault();

    const { value } = this.refs.name;
    console.log(value)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input ref="name" type="text" defaultValue="Hangzhou" />
        <button type="submit">submit</button>
      </form>
    )
  }
}

在React中,非受控组件是一种反模式,它的值不受组件自身的state或props控制。一般,须要为其添加ref prop来访问渲染后的底层DOM元素。

对比受控组件和非受控组件

受控组件和非受控组件的最大区别是:非受控组件的状态并不会受应用状态的控制,应用中也多了局部组件状态,而受控组件的值来自于组件的state。

1.性能上的问题

受控组件onChange后,调用setState会从新渲染,确实会有一些性能损耗。

2.是否须要事件绑定

受控组件须要为每一个组件绑定一个change事件,而且定义一个事件处理器来同步表单值和组件的状态。

尽管如此,在React仍然提倡使用受控组件,由于它可使得应用的整个状态更加可控。

表单组件的几个重要属性

1.状态属性

React的form组件提供了几个重要的属性,用于展现组件的状态。
value: 类型为text的input组件、textarea组件以及select组件都借助value prop来展现应用的状态。
checked: 类型为radio或checkbox的组件借助值为boolean类型的checked prop来展现应用的状态。
selected: 该属性可做用于select组件下面的option上,React并不建议使用功这种方式,推荐使用value.

2.事件属性

在状态属性发生变化时,会触发onChange事件属性。实际上,受控组件中的change事件与HTML DOM中提供的input事件更为相似。React支持DOM Level3中定义的全部表单事件。

样式处理

基本样式设置

React能够经过设置className prop给html设置class,也能够经过style prop给组件设置行内样式。

使用classnames库

咱们能够经过classnames库来设置html的类名

CSS Modules

CSS模块化的解决方案有不少,但主要有两类。

  1. Inline Style。这种方案完全抛弃CSS,使用JavaScript或JSON来写样式,能给CSS提供JavaScript一样强大的模块化能力。但缺点一样明显,它几乎不能利用CSS自己的特性,好比级联、媒体查询等,:hover和:active等伪类处理起来比较复杂。另外,这种方案须要依赖框架实现,其中与React相关的有Radium、jsxstyle和react-style
  2. CSS Modules。依旧使用CSS,但使用JavaScript来管理样式依赖。CSS Modules能最大化地结合现有CSS生态和JavaScript模块化能力,其API很是简洁。发布时依旧编译出单独的JavaScript和CSS文件。如今,webpack css-loader内置CSS Modules功能。

1.CSS模块化遇到了哪些问题

CSS模块化重要的是解决好如下两个问题:CSS样式的导入与导出。灵活按需导入以便复用代码,导出时要隐藏内部做用域,以避免形成全局污染。Sass、Less、PostCSS等试图解决CSS编程能力弱的问题,但并无解决模块化这个问题。React实际开发须要的CSS相关问题有:

  1. 全局污染:CSS使用全局选择器机制来设置样式,优势是方便重写样式。缺点是全部的样式全局生效,样式可能被错误覆盖。所以产生了很是丑陋的!important,甚至inline !important和复杂的选择器权重计数表,提升犯错几率和使用成本。Web Component标准中的Shadow DOM能完全解决这个问题,但它把样式完全局部化,形成外部没法重写样式,损失了灵活性。
  2. 命名混乱:因为全局污染的问题,多人协同开发时为了不样式冲突,选择器愈来愈复杂,容易造成不一样的命名风格,样式变多后,命名将更加混乱。
  3. 依赖管理不完全:组件应该相互独立,引入一个组件时,应该只引入它所须要的CSS样式。如今的作法是除了引入JavaScript,还要再引入它的CSS,并且Sass/Less很难实现对每一个组件都编译出单独的CSS,引入全部模块的CSS又形成浪费。JavaScript的模块化已经很是成熟,若是能让JavaScript来管理CSS依赖是很好的解决办法,而webpack的css-loader提供了这种能力。
  4. 没法共享变量:复杂组件要使用JavaScript和CSS来共同处理样式,就会形成有些变量在JavaScript和CSS中冗余,而预编译语言不能提供跨JavaScript和CSS共享变量的这种能力。
  5. 代码压缩不完全:对与很是长的类名压缩无能为力。

2.CSS Modules模块化方案

CSS Modules内部经过ICSS来解决样式导入和导出这两个问题,分别对应:import和:export两个新增的伪类。

:import("path/to/dep.css") {
  localAlias: keyFromDep;
}

:export {
  exportedKey: exportedValue;
}

但直接使用这两个关键字编程太烦琐,项目中不多会直接使用它们,咱们须要的是用JavaScript来管理CSS的能力。结合webpack的css-loader,就能够在CSS中定义样式,在JavaScript文件中导出。

启用CSS Modules

css?modules&localIdentName=[name]__[local]-[hash:base64:5]

加上modules即为启用,其中localIdentName是设置生成样式命名规则

下面咱们看看js是怎么引入CSS的:

/* button相关的全部样式 */
.normal {}
import styles from './Button.css'

buttonElm.outerHTML = `<button class=${styles.normal}>Submit</button>`

最终生成的HTML是这样的

<button class="button--normal-abc5436">Processing...</button>

这样class的名称基本就是惟一的。
CSS Modules对CSS中的class名都作了处理,使用对象来保存原class和混淆后class的对应关系。经过这些简单的处理,CSS Modules实现了如下几点:

  1. 全部样式都是局部化的,解决了命名冲突和全局污染问题
  2. class名生成规则配置灵活,能够以此来压缩class名
  3. 只须要引用组件的JavaScript,就能搞定组件全部的JavaScript和CSS
  4. 依然是CSS,学习成本几乎为零

样式默认局部

使用CSS Modules至关于给每一个class名外加了:local,以此来实现样式的局部化。若是咱们想切换到全局模式,可使用:global包裹

.normal {
  color: green;
}
/* 与上面等价 */
:local(.normal) {
  color: green;
}
/* 定义全局样式 */
:global(.btn) {
  color: red;
}
/* 定义多个全局样式 */
:global {
  .link {
    color: green;
  }
  .box {
    color: yellow;
  }
}

使用composes来组合样式

对于样式复用,CSS Modules只提供了惟一的方式来处理——composes组合。

/* components/Button.css */
.base { /* 全部通用的样式 */ }

.normal {
  composes: base;
  /* normal其余样式 */
}

此外,使用composes还能够组合外部文件中的样式

/* settings.css */
.primary-color {
  color: #f40;
}

/* component/Button.css */
.base { /* 全部通用样式 */ }

.primary {
  composes: base;
  composes: $primary-color from './settings.css'
}

对于大多数项目,有了composes后,已经再也不须要预编译处理器了。可是若是想用的话,因为composes不是标准的CSS语法,编译会报错,此时只能使用预处理本身的语法作样式复用了。

class命名技巧

CSS Modules的命名规范是从BEM扩展而来的。BEM把样式名分为3个级别

  1. Block: 对应模块名,如Dialog
  2. Element: 对应模块中的节点名 Confirm Button
  3. Modifier: 对应节点相关的状态,如disabled和highlight

如dialog__confirm-button--highlight。

实现CSS与JavaScript变量共享

:export关键字能够把CSS中的变量输出到JavaScript中

$primary-color: #f40;

:export {
  primaryColor: $primary-color;
}
// app.js
import style from 'config.scss'

console.log(style.primaryColor);

CSS Modules使用技巧

建议遵循以下原则

  1. 不使用选择器,只使用class名来定义样式
  2. 不层叠多个class,只使用一个class把全部样式定义好
  3. 全部样式经过composes组合来实现复用
  4. 不嵌套

常见问题
1.若是在一个style文件使用同名class?
虽然编译后多是随机码,但还是同名的。
2.若是在style文件中使用了id选择器、伪类和标签选择器等呢?
这些选择器不被转换,原封不动地出如今编译后的CSS中。也就是CSS Moudles只会转换class名相关的样式

CSS Modules结合历史遗留项目实践

1.外部如何覆盖局部样式

由于没法预知最终的class名,不能经过通常选择器覆盖样式。咱们能够给组件关键节点加上data-role属性,而后经过属性选择器来覆盖样式。

// dialog.js
return (
  <div className={styles.root} data-role="dialog-root"></div>
);
// dialog.css
[data-role="dialog-root"] {
  // override style
}

2.如何与全局样式共存

修改webpack配置

module: {
  loaders: [{
    test: /\.scss$/,
    exclude: path.resolve(__dirname, 'src/views'),
    loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true',
  }, {
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css!sass?sourceMap=true'
  }]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'

/* src/views/Component.js */
import './Component.scss'

CSS Modules结合React实践

import styles from './dialog.css';

class Dialog extends Component {
  render() {
    return (
      <div className={styles.root}></div>
    )
  }
}

若是不想频繁地输入styles.**,可使用react-css-modules

组件间通讯

父组件向子组件通讯

父组件能够经过props向子组件传递须要的信息

子组件向父组件通讯

有两种方法:1.利用回调函数。2.利用自定义事件机制:这种方法更通用,设计组件时考虑加入事件机制每每能够达到简化组件API的目的。

在React中,可使用任意一种方法,在简单场景下使用自定义事件过于复杂,通常利用回调函数。

跨级组件通讯

当须要让子组件跨级访问信息时,若像以前那样向更高级别的组件层层传递props,此时代码显得不那么优雅,甚至有些冗余。在React中,咱们还可使用context来实现跨级父子组件间的通讯。

class ListItem extends Component {
  static contextTypes = {
    color: PropTypes.string,
  }

  render () {
    return (
      <li style={{ background: this.context.color }}></li>
    )
  }
}
class List extends Component {
  static childContextTypes = {
    color: PropTypes.string,
  }

  getChildContext() {
    return {
      color: 'red'
    }
  }
  render() {

  }
}

React官方并不建议大量使用context,由于当组件结构复杂的时候,咱们很难知道context是从哪传过来的。使用context比较好的场景是真正意义上的全局信息且不会更改,例如界面主题、用户信息等。整体的原则是若是咱们真的须要它,那么建议写成高阶组件来实现。

没有嵌套关系的组件通讯

没有嵌套关系的,那只能经过能够影响全局的一些机制去考虑。以前讲的自定义事件机制不失为一种上佳的方法。

咱们在处理事件过程当中须要注意,在componentDidMount事件中,若是组件挂载完成,再订阅事件;当组件卸载的时候,在componentWillUnmount事件中取消事件的订阅。

对于React使用的场景,EventEmitter只须要单例就能够了

import { EventEmitter } from 'events';

export default new EventEmitter();
import emitter from './events';

emitter.emit('ItenChange', entry)
class App extends Component {
  componentDidMount() {
    this.itemChange = emitter.on('ItemChange', (data) => {
      console.log(data)
    })
  }
  componentWillUnmount() {
    emitter.removeListener(this.itemChange)
  }
}

通常来讲,程序中出现多级传递或跨级传递,那么要个从新审视一下是否有更合理的方式。Pub/Sub的模式可能也会带来逻辑关系的混乱。

跨级通讯每每是反模式的,应该尽可能避免仅仅经过例如Pub/Sub实现的设计思路,加入强依赖与约定来进一步梳理流程是更好的方法。(如使用Redux)

组件间抽象

经常有这样的场景,有一类功能须要被不一样的组件公用,此时就涉及抽象的话题。咱们重点讨论两种:mixin和高阶组件

封装mixin方法

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);

  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop]
    }
  }
}

const BigMixin = {
  fly: () => {
    console.log('fly');
  }
}

const Big = function() {
  console.log('new big');
}

consg FlyBig = mixin(Big, BigMixin)

const flyBig = new FlyBig();
flyBig.fly(); // => 'fly'

对于广义的mixin方法,就是用赋值的方式将mixin对象里的方法都挂载到原对象上,来实现对对象的混入。

看到上述实现,你可能会联想到underscore库中的extend或lodash库中的assign方法,或者说ES6中的Object.assign()方法。MDN上的解释是把任意多个源对象所拥有的自身可枚举属性复制给目标对象,而后返回目标对象。

在React中使用mixin

React在使用createClass构建组件时提供了mixin属性,好比官方封装的PureRenderMixin

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],
  render() {}
})

mixins数组也能够增长多个mixin,其若mixin方法之间有重合,对于普通方法,在控制台里会报一个ReactClassInterface的错误。对于生命周期方法,会将各个模块的生命周期方法叠加在一块儿顺序执行。

mixin为组件作了两件事:

  1. 工具方法。若是你想共享一些工具类方法,能够定义它们,直接在各个组件中使用。
  2. 生命周期继承,props与state合并,mixin也能够做用在getInitialState的结果上,做state的合并,而props也是这样合并的。

ES6 Classes与decorator

ES6 classes形式构建组件,它并不支持mixin。decorator语法糖能够实现class上的mixin。

core-decorators库为开发者提供了一些实用的decorator,其中实现了咱们正想要的@mixin

import { getOwnPropertyDescriptors } from './private/utils';

const { defineProperty } = Object;

function handleClass(target, mixins) {
  if (!mixins.length) {
    // throw error;
  }

  for(let i = 0, l = mixins.length; i < l; i ++) {
    const descs = getOwnPropertyDescriptors(mixins[i])

    for (const key in descs) {
      if (!(key in target.prototype)) {
        defineProperty(target.prototype, key, descs[key])
      }
    }
  }
}

export default function mixin(...mixins) {
  if (typeof mixins[0] === 'function') {
    return handleClass(mixins[0], [])
  } else {
    return target => {
      return handleClass(target, mixins)
    }
  }
}

原理也很简单,它将每个mixin对象的方法都叠加到target对象的原型上以达到mixin的目的。这样就能够用@mixin来作多个重用模块的叠加了。

const PureRender = {
  shouldComponentUpdate() {}
}

const Theme = {
  setTheme() {}
}

@mixin(PureRender, Theme)
class MyComponent extends Component {
  render() {}
}

mixin的逻辑和最先实现的简单逻辑很类似,以前直接给对象的prototype属性赋值,但这里用了getOwnPropertyDescriptor和defineProperty这两个方法,有什么区别呢?

这样实现的好在于definedProperty这个方法,也就是定义和赋值的区别,定义是对已有的定义,赋值是覆盖已有的定义。前者并不会覆盖已有方法,但后者会。本质上与官方的mixin方法都很不同,除了定义方法级别不能覆盖外,还得加上对生命周期方法的继承,以及对state的合并。

decorator还有做用在方法上的,它能够控制方法的自有属性,也能够做decorator的工厂方法。

mixin的问题

mixin存在不少问题,已经被官方弃用了,由高阶组件替代。

1.破坏的原有组件的封装

咱们知道mixin方法会混入方法,给原有组件带来新的特性,好比mixin中有一个renderList方法,给咱们带来了渲染List的能力,但它也可能带来新的state和props,这意味着组件有一些"不可见"的状态须要咱们去维护,但咱们在使用的时候并不清楚。此外renderList中的方法会调用组件中方法,但极可能被其余mixin截获,带来不少不可知。

2.不一样mixin的命名冲突

3.增长复杂性

咱们设计一个组件,引入PopupMixin的mixin,这样就给组件引进了PopupMixin生命周期方法。当咱们再引入HoverMixin,将有更多的方法被引进。固然咱们能够进一步抽象出TooltipMixin,将两个整合在一块儿,但咱们发现它们都有compomentDidUpdate方法。过一段时间,你会发现它的逻辑已经复杂到难以理解了。

咱们写React组件时,首先考虑的每每是单一的功能、简洁的设计和逻辑。当加入功能的时候,能够继续控制组件的输入和输出。若是说由于复杂性,咱们不断加入新的状态,那么组件会变得很是难维护。

高阶组件

高阶函数是函数式编程中的一个基本概念,这种函数接受函数做为输入,或是输出一个函数。
高阶组件相似高阶函数,它接受React组件做为输入,输出一个新的React组件。

高阶组件让咱们的代码更具备复用性、逻辑性与抽象特性,它能够对render方法做劫持,也能够控制props和state。

实现高阶组件的方法有两种:

  1. 属性代理:经过被包裹的React组件来操做props
  2. 反向继承:继承于被包裹的React组件

1.属性代理

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

这样,咱们就能够经过高阶组件来传递props,这种方法即为属性代理。
这样组件就能够一层层地做为参数被调用,原始组件就具有了高阶组件对它的修饰。保持了单个组件封装性同时还保留了易用性。固然,也能够用decorator来转换

@MyContainer
class MyComponent extends Component {
  render() {}
}

export default MyComponent

上述执行生命周期的过程相似于堆栈调用:
didmount -> HOC didmount -> (HOCs didmount) -> (HOCs will unmount) -> HOC will unmount -> unmount
从功能上,高阶组件同样能够作到像mixin对组件的控制,包括控制props、经过refs使用引用、抽象state和使用其余元素包裹WrappedComponent.

1.控制props

咱们能够读取、增长、编辑或是移除从WrappedComponent传进来的props,但须要当心删除与编辑重要的props。咱们应该尽量对高阶组件的props做新的命名以防止混淆。

例如,咱们须要增长一个新的prop:

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      const newProps = {
        text: newText,
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  }
}

2.经过refs使用引用

const MyContainer = (WrappedComponent) => {
  class extends Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method();
    }

    render() {
      const props = Object.assign({}, this.props, {
        ref: this.proc.bind(this),
      })
      return <WrappedComponent {...props} />
    }
  }
}

这样就能够方便地用于读取或增长实例的props,并调用实例的方法。
3.抽象state
高阶组件能够将原组件抽象为展现型组件,分离内部状态

const MyContainer = (WrappedComponent) => {
  class extends Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }

    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }

    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange,
        }
      }
      return <WrappedComponent {...this.props} {...newProps}>
    }
  }
}

这样就有效地抽象了一样的state操做。
4.使用其余元素包裹WrappedComponent
这既能够是为了加样式,也能够是为了布局

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      return (
        <div style={{ display: 'block' }}>
          <WrappedComponent {...this.props} />
        </div>
      )
    }
  }
}

反向继承

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent{
    render() {
      return super.render()
    }
  }
}

高阶组件返回的组件继承于WrappedComponent,由于被动地继承了WrappedComponent,全部调用都会反向,这也是这种方法的由来。
由于依赖于继承的机制,HOC的调用顺序和队列是同样的
didmount->HOC didmount->(HOCs didmount)->will unmount->HOC will unmount->(HOCs will unmount)

在反向继承方法中,高阶组件可使用WrappedComponent引用,这意味着它可使用WrappedComponent的state、props、生命周期、和render。但它不能保证完整的子组件树被解析。

它有两大特色

1.渲染劫持

高阶组件能够控制WrappedComponent的渲染过程。能够在这个过程当中在任何React元素输出的结果中读取;增长、修改。删除props,或读取或修改React元素树,或条件显示元素树,又或是用样式控制包裹元素树。

若是元素树中包括了函数类型的React组件,就不能操做组件的子组件。

条件渲染示例

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render();
      } else {
        return null;
      }
    }
  }
}

对render的输出结果进行修改

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      const elementsTree = super.render();
      let newProps;
      if (elementsTree && elementsTree.type === 'input') {
        newProps = { value: 'may the force be with you' }
      }
      const props = Object.assign({}, elementsTree.props, newProps);
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children);
      return newElementsTree;
    }
  }
}

2.控制state

高阶组件能够读取、修改或删除WrappedComponent实例中的state,若是须要的话,也能够增长state。但这样作,可能会让WrappedComponent组件内部状态变得一团糟。大部分高阶组件都应该限制读取或增长state,尤为是后者,能够经过重命名state,以防止混淆

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p><pre>{JSON.stringify(this.props, null, 2)}</pre>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}

在这个例子中,显示了WrappedComponent的props和state,方便咱们调试。

组件命名

高阶组件失去了原始的diplayName,咱们应该为高阶组件命名

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`

class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
}
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName ||
         WrappedComponent.name || 
         'Component'
}

组件参数

function HOCFactoryFactory(...params) {
  return function HOCFactory(WrappedComponent) {
    return class HOC extends Component {
      render() {
        return <WrappedComponent {...this.props} />
      }
    }
  }
}

组合式组件开发实践

咱们屡次提到,使用React开发组件时利用props传递参数。也就是说,用参数来配置组件时咱们最经常使用的封装方式。随着场景发生变化,组件的形态也发生变化,咱们必须不断增长props去应对变化,此时便会致使props的泛滥,而在拓展中又必须保证组件向下兼容,只增不减,使组件的可维护性下降。

咱们就能够利用上述高阶组件的思想,提出组件组合式开发模式,有效地解决了配置式所存在的一些问题。

1.组件再分离

SelectInput、SearchInput与List三个颗粒度更细的组件能够组合成功能丰富的组件,而每一个组件能够是纯粹的、木偶式的组件。

2.逻辑再抽象

组件中相同交互逻辑和业务逻辑也应该抽象

// 完成SearchInput与List的交互
const searchDecorator = WrappedComponent => {
  class SearchDecorator extends Component {
    constructor(props) {
      super(props)

      this.handleSearch = this.handleSearch.bind(this)
    }

    handleSearch(keyword) {
      this.setState({
        data: this.props.data,
        keyword,
      })

      this.props.onSearch(keyword)
    }

    render() {
      const { data, keyword } = this.state;
      return (
        <WrappedComponent
          {...this.props}
          data={data}
          keyword={keyword}
          onSearch={this.handleSearch}
        />
      )
    }
  }

  return SearchDecorator;
}

// 完成List数据请求
const asyncSelectDecorator = WrappedComponent => {
  class AsyncSelectDecorator extends Component {
    componentDidMount() {
      const { url, params } = this.props;

      fetch(url, { params }).then(data => {
        this.setState({
          data
        })
      })
    }

    render() {
      return (
        <WrappedComponent
          {...this.props}
          data={this.state.data}
        />
      )
    }
  }

  return AsyncSelectDecorator;
}

最后,咱们用compose将高阶组件层层包裹,将页面和逻辑完美结合在一块儿

const FinalSelector = compose(asyncSelectDecorator, searchDecorator, selectedItemDecorator)(Selector)

组件性能优化

从React的渲染过程当中,如何防止没必要要的渲染是最须要去解决的问题。
针对这个问题,React官方提供了一个便捷的方法来解决,那就是PureRender

纯函数

纯函数由三大原则构成

1.给定相同的输入,它老是返回相同的输出

2.过程没有反作用(咱们不能改变外部状态)

3.没有额外的状态依赖

纯函数也很是方便进行方法级别的测试以及重构,可让程序具备良好的扩展性及适应性。

PureRender

PureRender中的Pure指的就是组件知足纯函数的条件,即组件的渲染是被相同的props和state渲染进而获得相同的结果。

1.PureRender本质

官方在早期为开发者提供了一个react-addons-pure-render-mixin的插件,其原理为从新实现了shouldComponentUpdate生命周期方法,让当前传入的props和state与以前的做浅比较,若是返回false,那么组件就不会执行render方法。(若作深度比较,也很耗性能)

PuerRender源码中,只对新旧props做了浅比较,如下是shallowEqual的示例代码

function shallowEqual(obj, newObj) {
  if (obj === newObj) {
    return true;
  }

  const objKeys = Object.keys(obj);
  const newObjKeys = Object.keys(newObj);
  if (objKeys.length !== newObjKeysl.length) {
    return false;
  }

  return objKeys.every(key => {
    return newObj[key] === obj[key];
  })
}

3.优化PureRender

若是说props或state中有如下几种类型的状况,那么不管如何,它都会触发PureRender为true。

3.1直接为props设置对象或数组

引用的地址会改变

<Account style={{ color: 'black' }} />

避免这个问题

const defaultStyle = {};
<Account style={{ this.props.style || defaultStyle }} />

3.2设置props方法并经过事件绑定在元素上

class MyInput extends Component {
  constructor(props) {
    super(props)

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(e) {
    this.props.update(e.target.value)
  }

  render() {
    return <input onChange={this.handleChange} />
  }

}

3.3设置子组件

对于设置了子组件的React组件,在调用shouldComponentUpdate时,均返回true。

class NameItem extends Component {
  render() {
    return (
      <Item>
        <span>Arcthur</span>
      </Item>
    )
  }
}
<Item 
  children={React.createElement('span', {}, 'Arcthur')}
/>

怎么避免重复渲染覆盖呢?咱们在NameItem设置PureRender,也就是提到父级来判断。

Immutable

在传递数据时能够直接使用Immutable Data来进一步提高组件的渲染性能。

JavaScript中的对象通常是可变的,由于使用了引用赋值,新的对象简单地引用了原始对象,改变新的对象将影响到原始对象。

使用浅拷贝或深拷贝能够避免修改,但这样作又形成了内存和CPU的浪费。

1.Immutable Data一旦被建立,就不能再更改数据,对Immutable对象进行修改,添加或删除操做,都会返回一个新的Immutable对象。Immutable实现的原理是持久化的数据结构,也就是使用旧数据建立新数据,要保存旧数据同时可用且不变,同时为了不深拷贝把全部节点复制一遍带来的性能损耗,Immutable使用告终构共享,即若是对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其余节点则进行共享。

Immutable的优势

1.下降了可变带来的复杂度。

可变数据耦合了time和value的概念,形成了数据很难被回溯

function touchAndLog (touchFn) {
  let data = { key: '1' }
  touchFn(data);
  console.log(data.key)
}

若data是不可变的,能打印的结果是什么。

2.节省内存

Immutable使用的结构共享尽可能复用内存。没有引用的对象会被垃圾回收。

import { map } from 'immutable';

let a = Map({
  select: '1',
  filter: Map({ name: '2' }),
});

let b = a.set('select', 'people');

a === b
a.get('filter') === b.get('filter')// true

3.撤销/重作,复制/粘贴,甚至时间旅行这些功能都很容易实现。

由于每次数据都是不同的,那么只要把这些数据放到一个数组里存储起来,就能自由回退。

4.并发安全

数据天生是不可变的,后端经常使用的并发锁就不须要了,然而如今并无用,由于通常JavaScript是单线程运行的。

5.拥抱函数式编程

Immutable自己就是函数式编程中的概念,只要输入一致,输出必然一致。

使用Immutable的缺点

容易与原生对象混淆是使用Immutale的过程当中遇到的最大问题。
下面给出了一些办法

1.使用FlowType或TypeScript静态类型检查工具

2.约定变量命名规则,如Immutable类型对象以$$开头

3.使用Immutable.fromJS而不是Immutable.Map或Immutable.List来建立对象,这样能够避免Immutable对象和原生对象间的混用

Immutable.js

两个Immutable对象可使用===来比较,这样是直接比较内存地址,其性能最好。可是即便两个对象的值是同样的,也会返回false。

Immutable提供了Immutable.is来做"值比较",Immutable比较的是两个对象的hasCode或valueOf,因为Immutable内部使用了trie数据结构来存储,只要两个对象的hasCode相等,值就是同样的。这样的算法避免了深度遍历比较,所以性能很是好。

Immutable与cursor

这里的cursor和数据库中的游标是彻底不一样的概念。因为Immutable数据通常嵌套很是深,因此为了便于访问深层数据,cursor提供了能够直接访问这个深层数据的引用:

let data = Immutable.fromJS({ a: { b: { c: 1 } } });
let cursor = Cursor.from(data, ['a', 'b'], newData => {
  // 当cursor或其子cursor执行更新时调用
  console.log(newData)
})

// 1
cursor.get('c');
cursor = cursor.update('c', x => x + 1)
// 2
cursor.get('c');

Immutable与PureRender

React作性能优化时最经常使用的就是shouldComponentUpdate,使用深拷贝和深比较是很是昂贵的选择。而使使用Immutable.js,===和is是高效地判断数据是否变化的方法。

import { is } from 'immutable'

shouldComponentUpdate(nextProps, nextState) {
  const thisProps = this.props || {};
  const thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length)  {
    return true;
  }

  for (const key in nextProps) {
    if (nextProps.hasOwnProperty(key) && !is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (nextState.hasOwnProperty(key) && !is(thisState[key], nextState[key])) {
      return true;
    }
  }
}

Immutable与setState

React建议把this.state看成不可变的,所以修改前须要作一个深拷贝

import _ from 'lodash';

class App extends Component {
  this.state = {
    data: { time: 0 }
  }

  handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.time = data.time + 1;
    this.setState({ data });
  }
}

使用Immutable后,操做变得很简单

import { Map } from 'immutable';

class App extends Component {
  this.state = {
    data: Map({ time: 0 })
  }

  handleAdd() {
    this.setState(({ data }) => {
      data: data.update('times', v => v + 1)
    })
  }
}

Immutable能够给应用带来极大的性能提高。

key

若是每个组件是一个数组或迭代器的话,那么必须有一个惟一的key prop。它是用来标识当前项的惟一性的。若是使用index,它至关于一个随机键,不管有没有相同的项,更新都会渲染。若是key相同,react会抛警告,且只会渲染第一个key。

如有两个子组件须要渲染,能够用插件createFragment包裹来解决。

react-addons-perf

react-addons-perf经过Perf.start和Perf.stop两个API设置开始和结束的状态来做分析,它会把各组件渲染的各阶段的时间统计出来,而后打印出一张表格。

参考

《深刻React技术栈》

前端阅读室

相关文章
相关标签/搜索