JavaScript 装饰器 - Decorator

环境搭建

因为装饰器属于一个在提案中的语法,所以不管是node还是浏览器,现在都没有直接支持这个语法,我们要想使用该语法,就必须要通过babel将它进行一个编译转换,所以我们需要搭建一个babel编译环境。

1、安装babel相关包

npm i @babel/cli @babel/core @babel/plugin-proposal-decorators @babel/preset-env -D

2、在项目根目录下创建.babelrc

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ]
  ]
}

基础环境搭建好以后,接下来我们就可以尽情的使用装饰器了

类装饰器

类装饰器,顾名思义就是用来装饰整个类的,可以用来修改类的一些行为。

简单类装饰器

// src/demo01.js
// 类装饰器的简单应用
function log(target) {
  console.log(\'target: \', target);
}

@log
class App {
}

编译,执行

 // 使用babel编译,将代码编译输出到dist文件夹
npx babel src/demo01.js -d dist   
// 执行编译后的代码
node dist/demo01.js
// 编译后的代码
"use strict";

var _class;

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

// src/demo01.js
// 类装饰器的简单应用
function log(target) {
  console.log(\'target: \', target);
}

var App = log(_class = function App() {
  _classCallCheck(this, App);
}) || _class;

​ 这是babel编译后的源代码,其实babel加了一下额外的逻辑,删掉这些逻辑后,装饰器转换后的代码其实是下面这样子的:

function log(target) {
  console.log(\'target: \', target);
}

class App {};

log(App);

执行输出:

target:  [Function: App]

可以看到其实类装饰器就是一个函数,接受一个类作为参数,装饰器函数内部的target参数就是被装饰的类本身,我们可以在装饰器函数内部对这个类进行一些修改,比如:添加静态属性,给原型添加函数等等。

带参数的类装饰器

带参数的装饰器,需要在外面再套一层接受参数的函数,像下面这样:

// src/demo02.js
function log(msg) {
  console.log(\'msg: \', msg);
  return function(target) {
    console.log(\'target: \', target);
    target.msg = msg;
  }
}

@log(\'Jameswain\')
class App {
}
console.log(\'App: \', App);
// 编译
npx babel src/demo02.js -d dist   
// 执行
node src/demo02.js

​ 为了方便大家理解,我将babel编译后的代码进行了简化,删除了干扰逻辑

// dist/demo02.js
"use strict";

function log(msg) {
  console.log(\'msg: \', msg);
  return function _dec (target) {
    console.log(\'target: \', target);
    target.msg = msg;
  };
}

var _dec = log(\'Jameswain\');

function App() {
}

_dec(App);

console.log(\'App: \', App);

执行结果:

msg:  Jameswain
target:  [Function: App]
App:  [Function: App] { msg: \'Jameswain\' }

模拟react-redux的connect实现

我们平时开发中使用的react-redux就有一个connect装饰器,它可以把redux中的变量注入到指定类创建的实例中,下面我们就通过一个例子模拟实现connect的功能:

// src/demo03.js => 模拟实现react-redux的connect功能

// connect装饰器
const connect = (mapStateToProps, mapDispatchToProps) => target => {
  const defaultState = {
    name: \'Jameswain\',
    text: \'redux默认信息\'
  };
  // 模拟dispatch函数
  const dispatch = payload => console.log(\'payload: \', payload);

  const { props } = target.prototype;
  target.prototype.props = { ...props, ...mapStateToProps(defaultState), ...mapDispatchToProps(dispatch) };
}

const mapStateToProps = state => state;
const mapDispatchToProps = dispatch => ({
  setUser: () => dispatch({ type: \'SET_USER\' })
})

@connect(mapStateToProps, mapDispatchToProps)
class App {
  render() {
    console.log(\'渲染函数\');
  }
}

const app = new App();
console.log(\'app: \', app);
console.log(\'app.props: \', app.props);
// 编译
npx babel src/demo03.js
// 执行
node dist/demo03.js

输出结果:

app:  App {}
app.props:  { name: \'Jameswain\', text: \'redux默认信息\', setUser: [Function: setUser] }

从输出结果中可以看到,效果跟react-reduxconnect装饰器一样,返回值都被注入到App实例中的props属性中,下面我们来看看编译出来的代码长什么样子,老规矩为了方便大家理解,我删除掉babel的干扰代码,只保留核心逻辑:

// dist/demo03.js
"use strict";
// 模拟实现react-redux的connect功能
// connect装饰器
function connect(mapStateToProps, mapDispatchToProps) {
  return function (target) {
    var defaultState = {
      name: \'Jameswain\',
      text: \'redux默认信息\'
    };

    function dispatch(payload) {
      return console.log(\'payload: \', payload);
    };

    var props = target.prototype.props;
    target.prototype.props = { ...props, ...mapStateToProps(defaultState), ...mapDispatchToProps(dispatch) };
  };
};


function mapStateToProps(state) {
  return state;
};

function mapDispatchToProps(dispatch) {
  return {
    setUser: function setUser() {
      return dispatch({
        type: \'SET_USER\'
      });
    }
  };
};

function App() {}
App.prototype.render = function() {
  console.log(\'渲染函数\');
}

connect(mapStateToProps, mapDispatchToProps)(App);

var app = new App();
console.log(\'app: \', app);
console.log(\'app.props: \', app.props);

对比编译后的代码,可以发现其实装饰器就是一个语法糖而已,实现一模一样,只是调用的方式不一样。

// 装饰器用法
@connect(mapStateToProps, mapDispatchToProps)
class App {}

// 函数式用法
@connect(mapStateToProps, mapDispatchToProps)(class App {})

装饰器的执行顺序

一个类中可以有多个装饰器,装饰器的执行顺序是:从下往上,从右往左执行。比如下面这个例子:

// src/demo04.js 装饰器的执行顺序
function log(target) {
  console.log(\'log: \', target);
}

function connect(target) {
  console.log(\'connect: \', target);
}

function withRouter(target) {
  console.log(\'withRouter: \', target);
}

@log
@withRouter
@connect
class App {
}
// 编译
npx babel src/demo04.js -d dist
// 执行
node dist/demo04.js

运行结果:

 # 从下往上执行
connect:  [Function: App]
withRouter:  [Function: App]
log:  [Function: App]

编译后的代码:

// src/demo04.js 装饰器的执行顺序
"use strict";
function log(target) {
  console.log(\'log: \', target);
}

function connect(target) {
  console.log(\'connect: \', target);
}

function withRouter(target) {
  console.log(\'withRouter: \', target);
}

var _class;
var App = log(_class = withRouter(_class = connect(_class = function App() {
}) || _class) || _class) || _class;

从编译后的代码中可以看出,多个装饰器其实就是一层层的函数嵌套,从里往外执行,但是显然是装饰逻辑更清晰,易读。

上一篇:粗浅聊聊Python装饰器


下一篇:06.类视图