环境搭建
因为装饰器属于一个在提案中的语法,所以不管是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-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;
从编译后的代码中可以看出,多个装饰器其实就是一层层的函数嵌套,从里往外执行,但是显然是装饰逻辑更清晰,易读。