《React设计模式与最佳实践》读书笔记

《React设计模式与最佳实践》这本书是去年双11的时候在图灵社区入手的了,趁着周末看了一遍,对于了解React基础知识,以及编写React组件,还是能学到一些东西的,下面是整理的随书笔记。

<!--more-->

1. 基础

1.1. React的特性

在传统的前端开发中,“关注点分离”一般指的是文档结构、样式表和脚本行为的分离;而在React中,需要我们重新思考关注点分离的原则。

命令式编程描述代码如何工作,而声明式编程则表明想要实现什么目的。声明式编程中无需使用变量,也不用再执行过程中持续更新变量的值。

React推行声明式编程范式,其表现就在于使用组件来表明需要的UI

React使用了元素这种特殊类型的对象来控制UI流程,元素只包含了展示界面所需的必备信息

{
    type: Title,
    props: {
        color: 'red',
        children: 'Hello World' // 可以是其他元素
    }
}

其中type属性表示的是元素的类型,props表示的是渲染该节点需要的参数。React DOMReact Native都利用他在各自的平台上创建UI。

为什么React要与"模板样式行为分离"的原则背道而驰呢?因为在大多数情况下,传统意义上的文件分离只是假象,很难在不影响其他文件的前提下修改某个文件,这就是耦合。React尝试更近一步,建议你通过编写名为组件的小型代码块来组织应用。

第一章最后列出了人们对于React的一些常见误解

积累了一些 Web 开发经验后,我们了解到掌握一切知识是不可能的,但应该用正确的方法 学习新知识,以避免疲劳感。只要能跟上所有的新趋势,就不需要为了掌握新类库而实际运用它, 除非我们有时间做业余项目。

(页码21).

1.2. 代码整理

我们可以将HTML写在JSX文件中,通过babel,可以编译出让浏览器识别JSX语法的JavaScript代码。

Babel 可以将 ES2015 的 JavaScript 代码编译成 ES5 的,也可以将 JSX 编译成 JavaScript 函数。 这个过程称为转译,因为它将源代码编译成另一份新源代码,而不是可执行文件。

JSX仅仅只是语法糖,比如下面代码

{
  [1,2,3].map(item=>{
    return <h2>{item}</h2>
  })
}

会被编译成

 [1,2,3].map(item=>{
     return React.createElement("h2", {}, [item])
 })

编写JSX时有一些有用的约定和技巧

  • 需要嵌套子元素的任何情况下都应该多行书写,这种写法更容易阅读;注意多行书写元素时,需要用括号将他们包围起来,避免自动插入的分号;产生意料之外的结果

    return (
        <div>
            <Alert>hello</Alert>
        </div>
    )
  • 遇见需要传递多个属性的组件,最好是一行书写一个属性,同时缩进一个层级,并保持结尾括号和开始标签对齐

    <button 
        foo="bar" 
        veryLongPropertyName="baz" 
        onSomething={this.handleSomething} 
    />
  • 不要在组件中添加过多逻辑,如条件判断分支等,可以通过三元运算符、getter函数、render-if库等方式保持组件渲染函数的简洁性

  • 组件应该足够小、功能单一,渲染函数也应该简单明了

上面连续提到了保持组件代码简洁的问题,另外一个方式是:遵循函数式编程风格

高阶函数接受一个函数作为参数,也可以传入其他参数, 最后返回另一个函数。返回的函数通常会添加一些增强的特殊行为。React中一个常用的模式是高阶组件,将组件作为参数,并为他们增加一些常用行为

纯函数是指函数不产生副作用,它不会修改自身作用域以外的任何东西,同样的参数可以获得相同的结果。

不可变性是指一种操作变量的方式:函数不会修改变量的值,而是创建新的变量,赋新值然后再返回变量。

柯里化是函数式编程的常用技巧。柯里化过程就是将多参数函数转换成单参数函数,这些单 参数函数的返回值也是函数。

组合就是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。

React构建UI的方式和函数式编程原则有很多相似之处

UI = f(state)

2. 开发清晰可复用的组件

2.1. 创建组件的方式

编写组件有React.createClass和ES6的Class标准化方法,此外还有函数式组件。

通过createClass创建的方法,会自动绑定this到当前组件,因此不会发生this绑定丢失的问题;相反第,通过Class定义的方法会存在这个问题,解决办法有

  • 通过箭头函数

    <Button onClick={()=>this.btnClick()} />
  • 硬绑定

    <Button onClick={this.btnClick.bind(this)}/>

上面的缺点在于:如果某个方法传递给子组件,则每次触发父元素的render函数时,都会传递新的prop,导致子组件的更新,因此最好的解决办法是在构造函数中就进行硬绑定,然后在render函数中调用render函数

constructor(props) { 
    super(props) 
    this.handleClick = this.handleClick.bind(this) 
}

函数式组件指的是可以通过函数来创建的组件

const Button = props => <button>{props.text}</button>

函数式组件与状态组件的一项区别在于,this 在无状态函数式组件的执行过程中不 指向组件本身。因此setState和声明周期等方法都无法使用。正因如此,函数式组件也被称为无状态组件

2.2. 组件状态:State

每个有状态的React组件都可以拥有初始状态。

在组件的生命周期中,可以使用生命周期方法或者事件处理器中的setState多次修改状 态。当状态发生变化时,React 就用新状态渲染组件,这也是文档经常提到 React 组件类似状态 机的原因。

需要注意的是,应该总是将setState方法当作是异步调用的,因为在内部React会优化事件处理器内部的状态更新。

使用状态应该遵循以下规则:只能将满足需求的最少数据放在状态中

  • 只要能根据 props 计算最终值,就不应该将任何数据保存在状态中,我们可以通过计算属性的方式,通过调用计算属性在render函数中展示对应的数据
  • 设置状态会触发组件重新渲染。因此,应该只将渲染方法要用到的值保存在状态中。如果数据不会影响到渲染过程(如接口url或者超时参数),则应该考虑将他们放在独立的模块中。

2.3. 组件接口:props类型

如果希望整个应用可以复用组件,关键要确保清晰地定义组件及其参数

通过propTypes定义组件的属性

Button.propTypes = { 
  text: React.PropTypes.string.isRequired, 
}

如果意识到某个组件声明了太多 prop,而且它们之间没有关联,更好的做法是将组件纵向拆 分为多个组件,然后每个组件附带少量的 prop 和职责。

允许组件接收任何对象的做法不太好,这意味着组件了解对象长什么对象,如果对象结构发生改变,则组件很可能无法正常工作。

如果清晰的定义了propTypes,还可以使用react-docgen等工具快速生成接口文档

创建 API 清晰的可复用组件能很好地在应用内避免代码重复。

实际上,创建接受清晰的 prop 并与数据解耦的简洁组件是与团队其他成员共享基础组件库 的最佳方式。基础通用且可复用的组件可以作为开箱即用组件。

问题在于:新的开发人员有时候很难确定某些组件是否已经存在或者需要新增,解决问题的通用指南是创建一套风格指南。story-book是一个不错的选择。

3. 组合组件

React 非常强大,因为它允许你组合可测试且可复用的小型组件来构建复杂的应用。

组件组合方式相当直观,将他们放在render函数中即可。父组件通过props向子组件传递数据

3.1. children

children是一个特殊的prop,类似于Vue中的slot,这种便捷方式允许组件接收任何 children 元素,并将它们封装在预先定义好的父组件中。

// 定义
const Button = ({ children }) => ( 
  <button className="btn">{children}</button> 
)

Button.propTypes = { 
  children: React.PropTypes.oneOfType([ 
    React.PropTypes.array, 
    React.PropTypes.element, 
  ]), 
}

(页码75). 

// 使用
<Button> 
  <img src="..." alt="..." /> 
  <span>Click me!</span> 
</Button>

3.2. 容器组件与表现组件模式

React 组件通常包含杂合在一起的逻辑与表现,容器组件与表现组件模式可以分离这两个关注点。在这个模式中,每个组件都拆分成两个小组件,每个小组件各自都有清晰的职责。

  • 容器组件包含有关组件逻辑的一切,API 的调用就在容器组件中进行。此外,它还负责处理 数据操作以及事件处理。
  • UI 定义在表现组件中,并且表现组件以 prop 的形式从容器组件接收数据,由于表现组件一般不包含逻辑,因此可以将它定义为函数式无状态组件
  • 在容器组件中进行逻辑处理(如获取数据,处理事件等),然后渲染表现组件

这种模式带来的问题是:如非真正需要,过度拆分组件会导致代码库中文件和组件过多。

3.3. 高阶组件

当高阶函数概念应用在组件上时,我们将它简称为高阶组件。高阶组件其实就是函数,它接收组件作为参数,对组件进行增强后返回新的组件。

const HoC = Component => EnhancedComponent

高阶组件的命名方式一般遵循原则:就是给为组件提供信息的高阶组件名称加上 with 前缀。

高阶组件可以将多个组件之间公共的逻辑封装在一些,并通过同一个函数进行封装,避免了使用mixin存在的一些问题:污染state、命名冲突等。

高阶组件可以从context中获取数据,转换成props后再传递给组件。这样子组件不需要知道context的存在,也能够轻易地复用到应用的各个部位。

recompose提供了一些有用的高阶组件,可以配置自定义的高阶组件一起使用,这样可以使得组件在实现上进来少包含逻辑。

3.4. 函数子组件

这种模式的主要概念是:不按组件的形式传递子组件,而是定义一个可以从父组件接收参数 的函数。

const FunctionAsChild = ({ children }) => {
  // 这里可以传递一些参数到组件上
  return children();
}

FunctionAsChild.propTypes = { 
    children: React.PropTypes.func.isRequired 
};

函数子组件的优势主要表现在

  • 可以像高阶组件那样封装组件,在运行时为它们传递变量而不是固定属性。
  • 其次,以这种方式组合组件不要求 children 函数使用预定义的 prop 名称,开发者可以自行决定函数接收到的变量

下面是通过函数子组件封装网络请求数据的一个例子

<Fetch url="..."> {data => <List data={data} />} </Fetch>

4. 恰当地获取数据

React推行数据从根节点流向叶节点,这种模式被称为单向数据流

4.1. 组件之间的通信

每个组件都以 prop 的形式从父组件接收数据,并且 prop 无法修改。获取数据后,可以将其 转换为新的形式或者往下传给其他子组件。每个子组件都能保存内部状态,也可以将状态作为自身嵌套组件的 prop。

当子组件需要向父组件推送信息或触发事件时,React 通常采用回调函数来实现,具体实现为:将父组件的方法通过prop的形式传递给子组件,子组件在其内部调用父组件的方法

当子组件需要向兄弟元素进行通信时,可以通过公共的父组件保存状态。

总结一下:数据始终从父组件流向子组件,但子组件可以发送通知给父组件,以便组件树按照新的数据 重新渲染。

5. 浏览器

5.1. 事件

为了避免编写更少的模板代码,最常见的做法是为每个组件编写单个事件处理器,以根据合成事件的类型触发不同的操作。

合成事件将被回收,且存在唯一的全局处理器

  • 不能保存合成事件稍后再用,为了优化性能,在操作完成后就会被置为null(除非调用了合成事件的persist方法)
  • 元素上监听on开头的属性表示事件监听,而实际上React本身并不会在DOM节点上添加真正的事件处理器,通过事件冒泡机制进行事件代理,优化内存和速度

5.2. CSS

大型项目的CSS代码库存在下面问题

  • 无论使用怎样的命名空间或者BEM,都会污染全局命名空间
  • 很难清晰地声明某个特定组件依赖某个特定的CSS代码
  • 无用的代码只能通过手动的方式去移除
  • 样式表与模板文件通过选择器名和类名关联在一起,导致选择器名和类名无法压缩
  • css和js无法共享常量,如页头高度等
  • css解析方式的不确定性,导致实现按需加载css文件十分麻烦

React官方推荐在组件上使用行内样式!!!没错,行内样式!!React的关注点是将应用从文件分离转向组件分离,模板、样式和逻辑本身就是缺一不可的,将他们放在多个文件只能保持项目结构清晰,对于分离而言仅仅只是假象而已。因此,在React中使用行内样式是可行的。

当然,使用行内样式也存在一些问题,比如

  • 无法使用伪类、媒介查询等CSS特定功能
  • 样式调试非常困难
  • 页面体积增大(SSR时十分明显)

CSS Module解决了这个问题,通过wepack引入css module(一个js对象),然后将模块上的类型添加为组件的className属性

import styles from './index.css'
// 样式模块打包类似于下面的对象
// { button: "_2wpxM3yizfwbWee6k0UlD4" }

// 为组件添加类名,该内名与文件内容相关,在代码库内唯一
const Button = () => ( <button className={styles.button}>Click me!</button> )

这种做法非常强大,我们拥有了CSS的完整能力及表现性,又结合了局部作用域类名与显式依赖的优点

6. 性能优化

React 通过对比渲染结果前后元素树来计算出使屏幕上产生预期变化所需的最小操作集合,尽可能少地操作 DOM,因为直接操作文档对象模型的性能开销非常大。

然而,比较两棵元素树并非没有开销,因此 React 通过两项设定降低了其中的复杂度:

  • 如果两个元素的类型不同,则它们渲染的树也不同
  • 开发人员可以用 key 属性标记子组件,使它们在不同渲染过程得以保留。

6.1. 列表项渲染增加key属性

对于列表增删元素引起的渲染而言,React只渐染前后两次子元素是否一致,如果发现第一个元素不同,则会认为所有的列表项都是新的,因此会导致整个列表重新渲染。通过key属性,可以帮助React判断哪些元素进行了修改,以及新增或移除了哪些元素。key值的特殊性在于:列表项中每一项的key值是唯一的,且在每次渲染过程中保持不变

6.2. 必要时手动判断是否需要更新

为了进行diff操作,React必须触发所有组件的渲染方法,并比较前后两次的结果:如果没改变,则不修改DOM,这在大部分情形下是好的,但是如果render函数十分复杂,需要消耗一些时间才能得到不需要修改DOM的结论,就比较浪费了,因此可以通过shouldComponentUpdate手动控制是否调用组件及其子组件的渲染方法。

此外,在渲染函数中,最好不要使用内联的匿名函数,因为每次调用渲染函数都会创建新的函数,会触发子组件props更新,可能会导致不必要的更新。

7. 小结

本书后面还整理了为组件编写测试用例和react应用调试方法,以及编写react时需要注意的一些反模式,比如不能直接修改state等。

一直以来在项目中都是使用Vue居多,学习React感觉总是断断续续的,导致整个知识体系并不是很完整,这本书对于学习React还是有一些帮助的,接下来还是应该多写写React的项目练练手,趁热打铁~