因为装饰器属于一个在提案中的语法,所以不管是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
装饰器,它可以把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-redux
的connect
装饰器一样,返回值都被注入到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;
从编译后的代码中可以看出,多个装饰器其实就是一层层的函数嵌套,从里往外执行,但是显然是装饰逻辑更清晰,易读。