关于前端:React学习React漫谈

3次阅读

共计 23464 个字符,预计需要花费 59 分钟才能阅读完成。

事件零碎

React 基于 Virtual DOM 实现了一个 SyntheticEvent(合成事件)层,咱们定义的处理器会接管一个 SyntheticEvent 对象的实例,它完全符合 W3C 规范,不会存在任何 IE 的兼容性问题。并且与原生的浏览器事件一样领有同样的接口,同样反对事件的冒泡机制,我门能够应用 stopPropagation()和 preventDefault()来中断它。如果须要拜访原生事件对象,能够应用 nativeEvent 属性。

合成事件的绑定形式

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

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

合成事件的实现机制

在 React 底层,次要对合成事件做了两件事:事件委派和主动绑定。
1. 事件委派
React 不会把事件处理函数间接绑定到实在的节点上,而是把所有事件绑定到构造的最外层,应用一个对立的事件监听器,这个事件监听器上维持了一个映射来保留所有组件外部的事件监听和处理函数。当组件挂载或卸载时,只是在这个对立的事件监听器上插入或删除一些对象;当事件产生时,首先被这个对立的事件监听器解决,而后在映射里找到真正的事件处理函数并调用。(实现原理:对最外层的容器进行绑定,依赖事件的冒泡机制实现委派。)这样简化了事件处理和回收机制,效率也有很大晋升。
2. 自定绑定
在 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 技术栈》

正文完
 0