origin
A few days ago, I was watching the implementation of some popular mini mvvm frameworks (such as lighter frameworks such as avalon.js, vue.js, rather than heavier frameworks such as Angularjs and Emberjs). The popular modern mvvm frameworks generally eliminate data bidirectional binding (two-ways data binding), which is a selling point of the framework itself (Ember.js seems to not support bidirectional binding of data.), and the implementation methods of bidirectional data binding of each framework are not very consistent. For example, Anguarjs uses dirty checking internally, while the essence of the internal implementation of avalon.js is to set property accessors.
We do not intend to discuss the specific implementation of bidirectional data binding by each framework here. We will only talk about several common methods of implementing bidirectional data binding at the front end, and focus on the technical selection of avalon.js to implement bidirectional data binding.
The conventional implementation of two-way data binding
First, let’s talk about what is front-end bidirectional data binding. Simply put, it is the controller layer of the framework (the controller layer here is a general term, which can be understood as the middleware that controls the view behavior and connects the model layer) and the UI display layer (view layer) to establish a bidirectional data channel. When either of these two layers changes, the other layer will automatically make corresponding changes immediately (or seem to be immediately).
Generally speaking, to realize this two-way data binding relationship (the correlation process between the controller layer and the display layer), there are currently three ways in the front-end.
1. Dirty check
2. Observation mechanism
3. Encapsulate property accessor
Dirty check
We say that Angularjs (here refers specifically to AngularJS 1.xx version, which does not represent AngularJS 2.xx version) is a technical implementation of two-way data binding. The general principle is that Angularjs will maintain a sequence and place all attributes that need to be monitored in this sequence. When certain specific events occur (note that this is not timed but triggered by some special events), Angularjs will call the $digest method. The logic inside this method is to traverse all watchers, compare the monitored attributes, and compare whether the attribute value has changed before and after the method call. If it changes, the corresponding handler will be called. There are many articles on the Internet that analyze the implementation principle of Angularjs' two-way data binding, such as this article, and so on.
The disadvantages of this method are obvious. Traversing and training watchers is very performance-consuming, especially when the number of monitoring of a single page reaches an order of magnitude.
Observation mechanism
The blogger had a reprinted and translated article, the data binding change brought about by Object.observe(), which refers to using the Object.observe method in ECMAScript7 to monitor and observe the object (or its properties). Once it changes, the corresponding handler will be executed.
This is the most perfect way to monitor attribute data changes at present. The language (browser) native support is nothing better than this. The only regret is that the current breadth of support is not good enough and needs to be fully promoted.
Encapsulation property accessor
There is a concept of magic methods in php, such as the __get() and __set() methods in php. There is a similar concept in JavaScript, but it is not called a magic method, but an accessor. Let's look at a sample code.
var data = {name: "erik",getName: function() {return this.name;},setName: function(name) {this.name = name;}};From the above code, we can get a glimpse of the leap, such as the getName() and setName() methods in data. We can simply regard it as the accessor (or an accessor) of data.name.
In fact, for the above code, if it is more strict, it is not allowed to directly access the data.name property. All reading and writing to data.name must be passed through the data.getName() and data.setName() methods. So, imagine that once a property does not allow direct reading and writing to it, but must be read and written through the accessor, then of course I do some extras by rewriting the accessor method of the property, such as monitoring of property value change. This is the principle of using property accessors to do two-way data binding.
Of course, this method also has its drawbacks. The most prominent thing is that every time an attribute monitoring is added, a corresponding accessor method must be added to this attribute, otherwise the change of this attribute will not be captured.
Object.defineProperty method
The principle of the domestic mvvm framework avalon.js implementing data bidirectional binding is the property accessor. But of course it won't be as original as the above example code. It uses the standard property Object.defineProperty method defined in ECMAScript 5.1 (ECMA-262). In response to domestic market conditions, some do not support Object.defineProperty. Low-level browsers use VBScript for perfect compatibility, unlike other mvvm frameworks that have gradually abandoned support for low-end browsers.
Let’s first define the Object.defineProperty method on MDN.
The Object.defineProperty() method defines a new property directly on an object, or modify an existing property on an object, and returns the object.
The meaning is clear, and the Object.defineProperty method provides a direct way to define object properties or modify existing object properties. The prototype of the method is as follows:
Object.defineProperty(obj, prop, descriptor)
in,
obj , object to be modified
prop, with modified attribute name
descriptor , the relevant description of the attributes to be modified
Descriptor requires an object to be passed in, its default value is as follows
/*** @{param} descriptor*/{configurable: false,enumerable: false,writable: false,value: null,set: undefined,get: undefined}configurable , whether the property is configurable. The configurable meanings include: whether the attribute can be deleted, whether the writable, enumerable, and configurable properties of the attribute can be modified.
enumerable , whether the attribute is enumerable. The meaning of enumerable includes: whether it can be traversed through for...in, and whether it can be obtained through the Object.keys() method.
writable , whether the attribute can be rewritable. The meaning of rewritable includes: whether the attribute can be reassigned.
value , the default value of the property.
set , a property rewriter (for now, that's it). Once the attribute is reassigned, this method is automatically called.
get , the reader of the property (for now, that's it). Once the property is accessed and read, this method is automatically called.
Here is a sample code,
var o = {};Object.defineProperty(o, 'name', {value: 'erik'});console.log(Object.getOwnPropertyDescriptor(o, 'name')); // Object {value: "erik", writable: false, enumerable: false, configurable: false}Object.defineProperty(o, 'age', {value: 26,configurable: true,writable: true});console.log(o.age); // 26o.age = 18;console.log(o.age); // 18. Because the age property is a rewritable console.log(Object.keys(o)); // []. neither name nor age property is an enumerable Object.defineProperty(o, 'sex', {value: 'male',writable: false});o.sex = 'female'; // The assignment here is actually ineffective console.log(o.sex); // 'male';delete o.sex; // false, the property delete action is also invalidAfter the above example, under normal circumstances, the use of Object.definePropert() is relatively simple.
However, there is still one thing that needs additional attention. When the Object.defineProperty() method sets properties, the property cannot declare the accessor attributes (set and get) and the writable or value attributes at the same time. It means that if a certain property is set with a writable or value attribute, then this property cannot declare get and set, and vice versa.
Because when Object.defineProperty() declares a property, more than two types of access controls are not allowed for the same property.
Sample code,
var o = {},myName = 'erik';Object.defineProperty(o, 'name', {value: myName,set: function(name) {myName = name;},get: function() {return myName;}});The above code seems to be fine, but it will report an error when it is actually executed, and the error will be reported as follows.
TypeError: Invalid property. A property cannot both have accessors and be writable or have a value, #<Object>
Because the name attribute here declares the value attribute, the set and get attributes at the same time, both of which provide two read and write controls on the name attribute. If the value feature is not declared here, but the writable feature is declared, the result will be the same and an error will be reported.