# 装饰器模式初相识

装饰器模式

装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”

拿生活中的例子来举例,就比如很多女生她们都喜欢美甲,将指甲染成花花绿绿或者添加一些东西进行装饰。这样使得指甲看起来更好看,但是这也并没有改变指甲原来的功能,只是让它看起来更好看而已。这些修饰指甲的物件儿,在这里就是装饰器。

比如说我们在开发中接到了一个新的任务,需要在老代码的基础上添加一些功能,这个时候是一个很恐怖的事情,这种祖宗级的代码可不是谁都敢动的,这个时候我们就可以考虑使用装饰器模式进行试试。

比如我们的老代码,如下,调用它输出 1(这里只是用来举一个简单的例子,不必较真儿)。

function log() {
  console.log(1)
}

我们新的需求是再输出一个 2。这个时候我们应该怎么办呢?你可能会想到我们直接在这个码里在新增一个输出的代码不就好了吗?

function log() {
  console.log(1)
  console.log(2)
}

那我只能说恭喜你,你完蛋了。首先,上面的代码属于祖宗级的老代码,贸然改动它,可能会出现很多意想不到的bug。其次,直接在 log 函数中进行修改,违反了 “开放封闭” 原则。

我们可以,新建一个方法来实现输出 2 的功能。

function log2() {
  console.log(2)
}

分别调用两个方法,输出了 1 和 2

log()
log2()

这样我们就实现了 “只添加,不修改” 的装饰器模式。

# ES7的装饰器

如果你之前一点也没有了解过 JS 中的装饰器,可以看下阮一峰老师的文章 装饰器 (opens new window) 快速入门下。

在 ES7 中,我们可以通过 @ 语法糖给类添加一个装饰器。下面我们通过一个小 Demo 来看下效果。

新建一个 demo 工程,初始化工程

npm init -y

安装依赖:

npm install babel-preset-env babel-plugin-transform-decorators-legacy babel-cli --save-dev

新增配置文件 .babelrc

{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}

新建 index.js 文件,把以下内容粘贴到文件中

// 装饰器函数,第一个参数是目标类
function classDecorator(target) {
  target.hasDecorator = true;
  return target;
}
@classDecorator
class Demo {}

console.log('Demo 类是否被装饰了', Demo.hasDecorator);

终端执行,如下命令

npx babel index.js --out-file index_babel.js

然后运行编译后的 index_babel.js 文件,控制台输出如下内容

Demo 类是否被装饰了 true

# 深入装饰器语法糖

上面我们简单看了一下 ES7 中的装饰器。@ 操作符这个语法糖,帮我们做了些什么呢?实际最主要做的就是函数传参和调用。

# 类装饰器的参数

还是看上面的例子

function classDecorator(target) {
  target.hasDecorator = true;
  return target;
}
@classDecorator
class Demo {}

classDecorator 方法的参数,target 就是被装饰的类,即 Demo 类。

# 方法装饰器的参数

在给方法添加装饰器的时候一般至少需要三个参数

function funcDecorator(target, name, descriptor) {
  let originalMethod = descriptor.value
    descriptor.value = function() {
    console.log('test方法装饰逻辑')
    return originalMethod.apply(this, arguments)
  }
}

class Demo {
  @funcDecorator
  test() {
    console.log('test方法原逻辑')
  }
}

const demo = new Demo()
demo.test()

第一个参数,第二个参数 name 是装饰的属性名,第三个参数是一个属性描述对象,它和 Object.defineProperty 的第三个参数是一样的。通过修改 descriptor ,就可以对目标方法进行扩展了。

# 调用时机

装饰器函数在编译阶段就执行了。

# React中的装饰器:HOC

import React, { Component } from 'react'
const BorderHoc = WrappedComponent => class extends Component {
  render() {
    return <div style={{ border: 'solid 1px red' }}>
      <WrappedComponent />
    </div>
  }
}
export default borderHoc

装饰目标组件

import React, { Component } from 'react'
import BorderHoc from './BorderHoc'

// 用BorderHoc装饰目标组件
@BorderHoc 
class TargetComponent extends React.Component {
  render() {
    // 目标组件具体的业务逻辑
  }
}

// export出去的其实是一个被包裹后的组件
export default TargetComponent

高阶组件从实现层面看就是类装饰器。

# 使用装饰器改写Redux connect

一般我们在 react 中使用 redux 时。一般会像下面这样使用:

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

class App extends Component {
  render() {
    // App的业务逻辑
  }
}

function mapStateToProps(state) {
  // 假设App的状态对应状态树上的app节点
  return state.app
}

function mapDispatchToProps(dispatch) {
  // 这段看不懂也没关系,下面会有解释。重点理解connect的调用即可
  return bindActionCreators(action, dispatch)
}

// 把App组件与Redux绑在一起
export default connect(mapStateToProps, mapDispatchToProps)(App)

调用 connect 方法后会返回一个具有装饰作用的函数,这个函数可以接收一个 React 组件作为参数,使这个传入的组件可以和 redux 相结合,具备 Redux 提供的数据和能力。

我们可以将 connect 的方法进行抽出,使用装饰器的形式来完成上面的事情。

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

function mapStateToProps(state) {
  return state.app
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(action, dispatch)
}

// 将connect调用后的结果作为一个装饰器导出
export default connect(mapStateToProps, mapDispatchToProps)

组件内引入 connect

import React, { Component } from 'react'
import connect from './connect.js'   

@connect
export default class App extends Component {
  render() {
    // App的业务逻辑
  }
}