This article will not cover the concept and basic usage of decorators, but how our team applies decorators to practical development and the implementation of some advanced usages.
Introduction to decorators
Decorators are a new syntax in ES7 that, as the term “Decorator” suggests, can wrap objects and return a wrapped object. Decorators include classes, properties, methods, and so on. Decorators are written very much like Annotations in Java, but don’t call JS decorators annotations because they work differently. In Java, annotations are basically annotations on an object. Then, at run time or compile time, you can take the labeled object and wrap it with some logic through mechanisms such as reflection. A Decorator, on the other hand, wraps an object and returns a new object descriptor. It basically gets a limited amount of information about the host of the wrapped object, its keys and values.
Details see the article about Decorator: zhuanlan.zhihu.com/FrontendMag…
In short, JS decorators can be used to “decorate” three types of objects: class properties/methods, accessors, and the class itself, for a few examples.
Decorators for properties/methods
// A decorator can be wrapped around a function that takes arguments
function Decorator(type){
/** * Here is the prototype of the class described in the property of the actual decorator * @target decorator, note, not the instantiated class. If you're decorating a property of Car, the value of the target is Car. Prototype * the key of the property of the @name decoration * the description object of the object to be decorated */
return function (target, name, descriptor){
// To get the default value for this attribute at instantiation time
let v = descriptor.initializer && descriptor.initializer.call(this);
// Return a new descriptor, or modify descriptor directly
return {
enumerable: true.configurable: true.get: function() {
return v;
},
set: function(c) { v = c; }}}}Copy the code
If you are decorating A property of class A, and class A inherits from class B, then you print target and get a. protoType. The structure looks like this.
[image: A944761A – c04 E0FA – 4 – BD90 – c5/187 fcc2a BE179C46B641-35651-00001223828250-8 cc4-46 c4 – B8A3 – A7FD5E0376F6. PNG] if need be Target might need to figure that out.
Decoration for access operators
Similar to attribute methods, I won’t go into detail.
class Person {
@nonenumerable
get kidCount() { return this.children.length; }}function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}Copy the code
Decoration for classes
// For example, @observer in mobx
/** * wrap the react component * @param target */
function observer(target) {
target.prototype.componentWillMount = function() {
targetCWM && targetCWM.call(this);
ReactMixin.componentWillMount.call(this);
};
}Copy the code
Where target is the class itself (not its prototype)
Application in real scenarios
Today, we’ll focus on applying the Decorator feature to the data definition layer to implement functions such as type checking, field mapping, and so on.
As for the data definition layer (Model), it is actually the definition of various entity data in the application, which is also the M layer in MVVM. Note that it is distinguished from the VM layer. Model does not provide data management and circulation, but is only responsible for defining the attributes and methods of an entity itself, for example, there is a car module in the page. Let’s define a CarModel that describes the car’s color, price, brand, and so on.
As for why a clear Model should be defined in the front-end application, I have also discussed this on Zhihu before. The core points are as follows:
- Improve maintainability. A fixed entity do the data source and accurate description of this series for understanding the whole application is very important, especially in the reconstruction or take charge of other people’s code, you need to know an accurate page (or a module) what data, it will contain the data what field respectively, it is easier to understand the data of the application logic.
- Improve certainty. When are you going to give you the interface fields, add a few vehicles before you don’t know whether these fields have been defined, the server will return to these fields, may want to request the (and should have access to all of the fields) to know, but if there is a clear definition of the model, what are the fields will be clear at a glance.
- Improve development efficiency. Do some data mapping and type checking in this layer, which is the focus of today’s talk.
In the case of our team’s implementation of the Model part of the RN development framework, we provided at least three basic decorator-based capabilities: type checking, unit conversion, and field mapping. I’ll start with a brief overview of what these functions do and then show you how to implement these decorators.
Let’s take a look at the code for the final call
class CarModel extends BaseModel {
/** * price * @type {number} */
@observable
@Check(CheckType.Number)
@Unit(UnitType.PRICE_UNIT_WY)
price = 0;
/** * seller name * @type {string} */
@observable
@Check(CheckType.String)
@ServerName('seller_name')
sellerName = ' ';
}Copy the code
You can see that we have three custom decorators:
@unit, // Unit conversion decorator @check, // type Check decorator, @servername // Data field mapping decorator, used when the field name of the current backend definition is inconsistentCopy the code
@ Unit is a relatively special decorator, its role is in the automatic conversion between front and rear end Unit, also is the front-end and back-end data exchange some belt Unit, the according to each of the annotations and decorators, bring the real value into Unit of value to the other end, and then the other end at the framework layer automatically into its defined units, In this way, the front and back end units are inconsistent and the data exchange is chaotic.
Attributes decorated with @unit are read and written in front-end units, and then converted to JSON in formats such as 12.3_$wy, indicating that the number is in ten thousand yuan. @check is easier to understand and is used to Check the type of a field, or to Check the format of a field, or for custom checks such as regular expressions. @ServerName is used for mapping. For example, if the front and back ends name the same interface element differently, you can use another attribute name in the front end and then decorate it with the field name of the server side.
Basic implementation
Our goal is to implement these decorators, and according to our previous tutorial on decorators, it’s actually quite easy to implement these functions independently. So at sign Check, for example, we overwrite the descriptor of the wrapped property, return a new descriptor, redefine the getter and setter of the wrapped property, and then Check the type and format of the parameter that’s passed in when it calls the setter, Let’s do something about it.
/** * This annotation will display a warning on the console if there is a problem with the type defined in @param type CheckType * @returns {Function} * @constructor */
function CheckerDecorator(type){
return function (target, name, descriptor){
let v = descriptor.initializer && descriptor.initializer.call(this);
return {
enumerable: true.configurable: true.get: function() {
return v;
},
set: function(c) {
// Do various checks on the value of c passed in here
var cType = typeof(c);
// ...v = c; }}}}Copy the code
Quite simply, several other decorators have similar implementations, perhaps a bit more complicated like @Unit, but just remember the Unit of each attribute in the Decorator, get the corresponding Unit of the attribute at serialization time and do the conversion.
Problems with the underlying implementation
But, here, the problem is not over! We did implement a working Decorator, but can these decorators be superimposed? Can I also mix it with some of the common decorators in the industry? Such as @Observable in Mobx. That’s how my original example above is used:
@observable
@Check(CheckType.String)
@ServerName('seller_name')
sellerName = ' ';Copy the code
If you implement @check and @servername the way I just did, you’ll see two fatal problems:
- These two self-implemented decorators are not superimposed in the first place.
- Neither Decorator can be used in conjunction with the @Observable. Why is that? The problem is how we implement getters and setters for rewriting properties. First, each getter and setter defined for a property overrides the previous definition, meaning that the action can only be done once. However, the mobx implementation relies heavily on the definition of getters and setters (see my previous article: How to Implement a Mobx yourself – Principle Analysis).
In fact, the Decorator itself is fine when it’s used superimposed, because every time you wrap it, you’re going to return the property descriptor to the next wrapper, and you’re going to end up with a package of function descriptor effects, and you’re going to return the property descriptor again.
Advanced implementation
So we need to get rid of the implementation that defines getters and setters. In fact, in addition to this way, there are many ways to achieve the above functions, the core is, in a decorator function, you will need to deal with the attributes and handling of this property needs to be done to the corresponding relations are recorded, then the process instance data and the sequential data, taking out corresponding relation, perform related logic.
Without further ado, let’s go straight to an implementation that mounts this correspondence to a class’s prototype.
function Check (type) {
return function (target, name, descriptor) {
let v = descriptor.initializer && descriptor.initializer.call(this);
/** * Records the attribute name and the required type mapping on the prototype of the class */
if(! target.constructor.__checkers__) {// Define the hidden property as not Enumerable, which you cannot retrieve when iterating.
Object.defineProperty(target.constructor, "__checkers__", {
value: {},
enumerable: false.writeable: true.configurable: true
});
}
target.constructor.__checkers__[name] = {
type: type
};
return descriptor
}
}Copy the code
Note that I mentioned earlier that the first argument to the decorator function, target, is the prototype of the class to which the wrapper attribute belongs, as you can see when Babel is compiled. The reason why I attach the mapping to target.constructor is that all of my Model classes inherit from a self-provided Model base class. Target gets the prototype of the base class instead of the prototype of the subclass. Constructor gets the final subclass. That is, I mount the correspondence to the subclass of the development definition.
Looking at the code for the base class, the core provides two methods, mapping data and serialization.
class BaseModel {
/** * maps the back-end data directly to the current example */
__map (json) {
let alias = this.constructor.__aliasNames__;
let units = this.constructor.__unitOriginals__;
let checkers = this.constructor.__checkers__;
for (let i in this) {
if (!this.hasOwnProperty(i)) return;
// If you have multiple decorators, you need to go through multiple logical processes to produce a final realValue
let realValue = json[i];
// Process the data step by step
// First check alias data and do the mapping
if (alias && typeof(alias[i]) ! = ='undefined') {
/ /...
}
// Then check the type against the data
if (checkers && checkers[i]) {
/ /...
}
// Finally, convert the data to units
if (units && units[i]) {
/ /...
}
/ / assignment
this[i] = realValue; }}/** * the function automatically called when rewriting json.stringify */
toJSON () {
let result = {};
let units = this.constructor.__unitOriginals__;
for (let i in this) {
if (!this.hasOwnProperty(i)) return;
if (units && units[i]) {
// When serializing, add units if necessary
result[i] = this[i] + '_ $' + units[i];
} else {
result[i] = this[i]; }}returnresult; }}Copy the code
In the __map function, we take all the mappings from the current class (this.constructor) and do the data checksum mapping, which should make sense here.
The final applied code is the code we posted at the beginning, as long as the corresponding Model class inherits from BaseModel.
A Decorator implemented this way, because it doesn’t use any getter setter-related functionality, can blend perfectly with a library like Mobx and can be stacked indefinitely, but if you use multiple tripartite libraries, they all provide corresponding decorators. And then you’ve changed the getter and setter, and there’s nothing you can do about it!
conclusion
Although the principle of decorators is very simple, it does provide a lot of utility and convenience. Many frameworks and libraries use this feature on a large scale in the front-end domain, but it is expected that these libraries implement decorators with consideration for generality, superposition and coexistence. Like the @Observable in Mobx above, it cannot be superimposed, and the order of my own decorators should be in the outermost layer, because it changes the nature of the entire property, and when it is not written in the outermost layer, I will find some puzzling problems.