JavaScript 裝飾器 - Decorator

環境搭建

由於裝飾器屬於一個在提案中的語法,因此無論是node仍是瀏覽器,如今都沒有直接支持這個語法,咱們要想使用該語法,就必需要經過babel將它進行一個編譯轉換,因此咱們須要搭建一個babel編譯環境。javascript

一、安裝babel相關包java

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

二、在項目根目錄下建立.babelrcnode

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

基礎環境搭建好之後,接下來咱們就能夠盡情的使用裝飾器了react

類裝飾器

類裝飾器,顧名思義就是用來裝飾整個類的,能夠用來修改類的一些行爲。shell

簡單類裝飾器

// src/demo01.js
// 類裝飾器的簡單應用
function log(target) {
  console.log('target: ', target);
}

@log
class App {
}

編譯,執行npm

// 使用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加了一下額外的邏輯,刪掉這些邏輯後,裝飾器轉換後的代碼實際上是下面這樣子的:json

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

class App {};

log(App);

執行輸出:redux

target:  [Function: App]

能夠看到其實類裝飾器就是一個函數,接受一個類做爲參數,裝飾器函數內部的target參數就是被裝飾的類自己,咱們能夠在裝飾器函數內部對這個類進行一些修改,好比:添加靜態屬性,給原型添加函數等等。瀏覽器

帶參數的類裝飾器

帶參數的裝飾器,須要在外面再套一層接受參數的函數,像下面這樣:babel

// 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;

從編譯後的代碼中能夠看出,多個裝飾器其實就是一層層的函數嵌套,從裏往外執行,可是顯然是裝飾邏輯更清晰,易讀。

相關文章
相關標籤/搜索