preface
The object of JavaScript is a set of key value pairs, which can have any number of unique keys. The key can be of String type or tag type (Symbol, the basic data type newly added in ES6). Each key corresponds to a value, and the value can be any value of any type. For the properties in the object, JavaScript provides a property descriptor interface, PropertyDescriptor. Most developers do not need to use it directly, but many internal implementations of frameworks and class libraries use it, such as Avalon js,Vue.js, this article introduces the attribute descriptor and related applications.
Define object properties
Before introducing the description of object attributes, let's introduce how to define object attributes. The most common way is to use the following methods:
var a = { name: 'jh' }; // or var b = {}; b.name = 'jh'; // or var c = {}; var key = 'name'; c[key] = 'jh';
This article uses literal methods to create objects, but JavaScript also provides other methods, such as new Object(), object Create(), for more information, see Object initialization.
Object.defineProperty()
The above common methods cannot operate on the property descriptor. We need to use the defineProperty() method, which defines a new property or modifies a defined property for an object and accepts three parameters object Defineproperty (obj, prop, descriptor), the return value is the object after operation:
- obj, object to be operated
- Attribute name
- Property description object for the action property
var x = {}; Object.defineProperty(x, 'count', {}); console.log(x); // Object {count: undefined}
Since an empty property description object is passed in, the property value of the output object is undefined. When using the defineProperty() method to operate the property, the default value of the description object is:
- value: undefined
- set: undefined
- get: undefined
- writable: false
- enumerable: false,
- configurable: false
If you do not use this method to define an attribute, the default description of the attribute is:
- value: undefined
- set: undefined
- get: undefined
- writable: true
- enumerable: true,
- configurable: true
Default values can be overridden by explicit parameter value settings.
Of course, it also supports batch definition of object properties and description objects, using ` ` object Defineproperties() ` method, such as:
var x = {}; Object.defineProperties(x, { count: { value: 0 }, name: { value: 'jh' } }); console.log(x); // Object {count: 0, name: 'jh'}
Read property description object
JavaScript supports us to read the description object of an object attribute, using object Getownpropertydescriptor (obj, prop) method:
var x = { name: 'jh' }; Object.defineProperty(x, 'count', {}); Object.getOwnPropertyDescriptor(x, 'count'); Object.getOwnPropertyDescriptor(x, 'name'); // Object {value: undefined, writable: false, enumerable: false, configurable: false} // Object {value: "jh", writable: true, enumerable: true, configurable: true}
This example also proves that the default attribute description object is different when defining attributes in different ways described above.
Property description object
The PropertyDescriptor API provides six instance properties to describe object properties, including configurable, enumerable, get, set, value and writable
value
Specify object attribute values:
var x = {}; Object.defineProperty(x, 'count', { value: 0 }); console.log(x); // Object {count: 0}
writable
Specify whether object properties are mutable:
var x = {}; Object.defineProperty(x, 'count', { value: 0 }); console.log(x); // Object {count: 0} x.count = 1; // If silence fails, no error will be reported console.log(x); // Object {count: 0}
When using the defineProperty() method, writable: false is set by default, and writable: true needs to be set.
Accessor function (getter/setter)
Object properties can set accessor functions, use get to declare accessor getter functions, and set to declare accessor setter functions; If there is an accessor function, the corresponding accessor function will be called when accessing or setting this property:
get
When reading the attribute value, call the function and assign the return value of the function to the attribute value;
var x = {}; Object.defineProperty(x, 'count', { get: function() { console.log('read count attribute +1'); return 0; } }); console.log(x); // Object {count: 0} x.count = 1; // 'read count attribute + 1 ' console.log(x.count); // 0
set
Call the function when setting the function value, and the function receives the set attribute value as a parameter:
var x = {}; Object.defineProperty(x, 'count', { set: function(val) { this.count = val; } }); console.log(x); x.count = 1;
When the appeal code is executed, an error will be reported and the execution stack overflows:
When setting the count attribute, the above code will call the set method, and assigning a value to the count attribute in this method will trigger the set method again, so this is not feasible. JavaScript uses another method. Usually, the accessor function must be declared at the same time. The code is as follows:
var x = {}; Object.defineProperty(x, 'count', { get: function() { return this._count; }, set: function(val) { console.log('set up count attribute +1'); this._count = val; } }); console.log(x); // Object {count: undefined} x.count = 1; // 'set count attribute + 1 ' console.log(x.count); 1
In fact, when using the defineProperty() method to set a property, it is usually necessary to maintain a new internal variable inside the object (starting with the underscore to indicate that you do not want to be accessed externally) as the intermediary of the accessor function.
Note: when accessor description is set, value and writable description cannot be set.
We found that after setting the attribute accessor function, we can realize the real-time monitoring of the attribute, which is very useful in practice, which will be confirmed later.
enumerable
Specify whether a property in the object can be enumerated, that is, whether the for in operation can be traversed:
var x = { name: 'jh' }; Object.defineProperty(x, 'count', { value: 0 }); for (var key in x) { console.log(key + ' is ' + x[key]); } // name is jh
The count property cannot be traversed above, because when using the defineProperty() method, enumerable: false is displayed by default, and the declaration description needs to be displayed:
var x = { name: 'jh' }; Object.defineProperty(x, 'count', { value: 0, enumerable: true }); for (var key in x) { console.log(key + ' is ' + x[key]); } // name is jh // count is 0 x.propertyIsEnumerable('count'); // true
configurable
This value specifies whether the object property description is mutable:
var x = {}; Object.defineProperty(x, 'count', { value: 0, writable: false }); Object.defineProperty(x, 'count', { value: 0, writable: true });
An error will be reported when executing the above code, because when using the defineProperty() method, the default is configurable: false, and the output is as shown in the figure:
Modify as follows:
var x = {}; Object.defineProperty(x, 'count', { value: 0, writable: false, configurable: true }); x.count = 1; console.log(x.count); // 0 Object.defineProperty(x, 'count', { writable: true }); x.count = 1; console.log(x.count); // 1
Property description is bound to the view model
After introducing the attribute description object, let's take a look at its application in modern JavaScript framework and class library. At present, there are many frameworks and class libraries to realize one-way or even two-way binding between data and DOM view, such as React and angular js,avalon.js,,Vue.js, etc. it is easy to use them to update the DOM view in response to data changes, and even the view and model can realize two-way binding and synchronous update. Of course, the internal implementation principles of these frameworks and class libraries are mainly divided into three camps. This paper takes Vue JS as an example, Vue JS is a popular responsive view layer class library at present. Its internal implementation of responsive principle is the specific application of attribute description in technology introduced in this paper.
sure Click here , view a one-way binding instance of simple data view implemented by native JavaScript. In this instance, click the button to realize self increment of count. The input content in the input box will be synchronously updated to the display dom. Even if the attribute value of data object is changed on the console, the DOM will respond to the update, as shown in the figure:
Click to view the complete instance code.
Data view unidirectional binding
The existing codes are as follows:
var data = {}; var contentEl = document.querySelector('.content'); Object.defineProperty(data, 'text', { writable: true, configurable: true, enumerable: true, get: function() { return contentEl.innerHTML; }, set: function(val) { contentEl.innerHTML = val; } });
It is easy to see that when we set the text attribute of the data object, the value will be set as the content of the view DOM element, and when we access the attribute value, the content of the view DOM element will be returned, which simply realizes the one-way binding of data to the view, that is, when the data changes, the view will be updated.
The above is only data view binding for one element, but slightly experienced developers can package according to the above ideas, and easily implement a simple tool class for one-way data to view binding.
Abstract encapsulation
Next, the above examples are simply abstracted and encapsulated, Click to view the complete instance code.
First declare the data structure:
window.data = { title: 'Data view unidirectional binding', content: 'Data view binding using attribute descriptor', count: 0 }; var attr = 'data-on'; // The agreed syntax declares the DOM binding object properties
Then, the encapsulation function processes the object in batch, traverses the object properties, sets the description object, and registers the callback when changing the properties:
// Set the description object for each property in the object, especially the accessor function function defineDescriptors(obj) { for (var key in obj) { // traversal attributes defineDescriptor(obj, key, obj[key]); } // Sets a description object for a specific property function defineDescriptor(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { var value = val; return value; }, set: function(newVal) { if (newVal !== val) { // Only when the value changes val = newVal; Observer.emit(key, newVal); // Trigger update DOM } } }); Observer.subscribe(key); // Register a callback for this property } }
Manage events
Manage attribute change events and callbacks in publish subscribe mode:
// Use publish / subscribe mode to centrally manage, monitor and trigger callback events var Observer = { watchers: {}, subscribe: function(key) { var el = document.querySelector('[' + attr + '="'+ key + '"]'); // demo var cb = function react(val) { el.innerHTML = val; } if (this.watchers[key]) { this.watchers[key].push(cb); } else { this.watchers[key] = [].concat(cb); } }, emit: function(key, val) { var len = this.watchers[key] && this.watchers[key].length; if (len && len > 0) { for(var i = 0; i < len; i++) { this.watchers[key][i](val); } } } };
Initialize instance
Finally, initialize the instance:
// Initialize demo function init() { defineDescriptors(data); // Processing data objects var eles = document.querySelectorAll('[' + attr + ']'); // Initial traversal of DOM display data // In fact, you can put this operation into the get method of the property description object, so you only need to traverse and access the property during initialization for (var i = 0, len = eles.length; i < len; i++) { eles[i].innerHTML = data[eles[i].getAttribute(attr)]; } // Auxiliary test example document.querySelector('.add').addEventListener('click', function(e) { data.count += 1; }); } init();
html code reference is as follows:
<h2 class="title" data-on="title"></h2> <div class="content" data-on="content"></div> <div class="count" data-on="count"></div> <div> Please enter: <input type="text" class="content-input" placeholder="Please enter the content"> </div> <button class="add" onclick="">Plus 1</button>
Vue. Response principle of JS
In the previous section, we implemented a simple one-way binding instance of data view. Now for Vue A brief analysis of the responsive one-way binding of JS mainly needs to understand how to track data changes.
Dependency tracking
Vue.js supports us to pass a JavaScript object as component data through the data parameter, and then Vue JS will traverse this object property, using object The defineproperty method sets the description object. The change of the property can be tracked through the accessor function. The essence is similar to the example in the previous section, but the difference is Vue JS creates a Watcher layer, records the attributes as dependencies during component rendering, and then notifies Watcher to recalculate when the setter of dependencies is called, so that its associated components can be updated, as shown in the following figure:
When the component is mounted, the watcher instance is instantiated and passed to the dependency management class. When the component is rendered, the object observation interface is used to traverse the incoming data object, create a dependency management instance for each attribute and set the attribute description object. In the accessor function get function, the dependency management instance adds (records) the attribute as a dependency, Then, when the dependency changes, trigger the set function to notify the dependency management instance in the function. The dependency management instance distributes the change to all the watcher instances stored in it, and the watcher instance recalculates and updates the components.
Therefore, it can be concluded that Vue The responsive principle of JS is dependency tracking. Set accessor functions and register a dependency management instance dep for each attribute through an observation object. A watchdog instance is maintained for each component instance in the dep. When the attribute changes, the dep instance is notified through the setter, and the dep instance distributes the change to each watcher instance. The watcher instance calculates and updates the component instance respectively, That is, the watcher tracks the dependency added by DEP, object The defineproperty () method provides technical support for this tracking, and the dep instance maintains this tracking relationship.
Simple analysis of source code
Next, for Vue JS source code for simple analysis, starting with the processing of JavaScript objects and attributes:
Observer
First, Vue JS also provides an abstract interface to observe objects, set memory functions for object attributes, collect attribute dependencies, and then distribute dependency updates:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); // Manage object dependencies this.vmCount = 0; def(value, '__ob__', this); // Cache the processed object and mark it as processed if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } };
The above code focuses on two nodes, this Observearray (value) and this walk(value);:
-
If it is an object, call the walk() method to traverse the object property and convert the property into a response:
Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i], obj[keys[i]]); } };
You can see that the final property description object is set by calling the defineReactive$ () method.
-
If value is an object array, additional processing is required. Call the observeArray() method to generate an Observer instance for each object, and traverse and listen to the object properties:
Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
The core is to call the observe function for each array item:
function observe(value, asRootData) { if (!isObject(value)) { return // Only objects need to be processed } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; // The processed data is directly read from the cache } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { ob = new Observer(value); // Process the object } if (asRootData && ob) { ob.vmCount++; } return ob }
Call ob = new Observer(value); Then we return to the result of the first case: call the defineReactive$ () method to generate a responsive attribute.
Generate responsive properties
The source code is as follows:
function defineReactive$$1 (obj,key,val,customSetter) { var dep = new Dep(); // Manage attribute dependencies var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // The previously set get/set needs to be merged and called var getter = property && property.get; var setter = property && property.set; var childOb = observe(val); // The attribute value may also be an object, which requires recursive observation and processing Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { // There is a watcher instance pointed to by the management dependent object dep.depend(); // Add dependency (record) if (childOb) { // The property value is an object childOb.dep.depend(); // Attribute value objects also need to add dependencies } if (Array.isArray(value)) { dependArray(value); // Processing array } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return // There is no change and no need to implement it later } /* eslint-enable no-self-compare */ if ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); // Update attribute values } else { val = newVal; // Update attribute values } childOb = observe(newVal); // Each time the value changes, it needs to be observed again, because the possible value is an object dep.notify(); // Publish update events } }); }
This method uses object The defineproperty() method sets the property description object. The logic is concentrated in the property accessor function:
- get: returns the attribute value. If the watcher exists, it recursively records the dependency;
- set: when the attribute value changes, update the attribute value and call the dep.notify() method to publish the update event;
Management dependency
Vue.js needs to manage object dependencies, notify the watcher to update the component when the attribute is updated, and then update the view, Vue JS management dependency interface is implemented in publish subscribe mode. The source code is as follows:
var uid$1 = 0; var Dep = function Dep () { this.id = uid$1++; // Dependency management instance id this.subs = []; // Subscribe to the watcher instance array of the dependency management instance }; Dep.prototype.depend = function depend () { // Add dependency if (Dep.target) { Dep.target.addDep(this); // Call the watcher instance method to subscribe to this dependency management instance } }; Dep.target = null; // watcher instance var targetStack = []; // Maintain watcher instance stack function pushTarget (_target) { if (Dep.target) { targetStack.push(Dep.target); } Dep.target = _target; // Initialize the watcher instance pointed to by Dep } function popTarget () { Dep.target = targetStack.pop(); }
subscribe
As before, when generating the responsive attribute and setting the accessor function for the attribute, call the dep.depend() method in the get function to add the dependency, and call dep.target addDep(this);, That is, call the adddep method of the pointed watcher instance to subscribe to this dependency management instance:
Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { // Subscribed this.newDepIds.add(id); // The collection of dependency management instance IDS maintained by the watcher instance this.newDeps.push(dep); // Dependency management instance array maintained by watcher instance if (!this.depIds.has(id)) { // The collection of dependency management instance IDS maintained by the watcher instance // Call the passed dependency management instance method and add this watcher instance as the subscriber dep.addSub(this); } } };
watcher instances may track multiple attributes at the same time (that is, subscribing multiple dependency management instances), so it is necessary to maintain an array, store dependency management instances of multiple subscriptions, and record the id of each instance to make sure that the subscription has been subscribed, and then invoke the addSub method depending on the management instance:
Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); // Implement the subscription relationship between the watcher and the dependency management instance };
This method simply adds a watcher instance that subscribes to the dependency management instance in the subscription array.
release
When the property is changed, the dep.notify() method is called in the set function of the accessor of the property to publish the property change:
Dep.prototype.notify = function notify () { // Copy subscriber array var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); // Distribution change } };
Trigger update
As mentioned earlier, Vue In JS, the watcher layer tracks dependency changes and notifies the component to update when changes occur:
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { // synchronization this.run(); } else { // asynchronous queueWatcher(this); // Finally, the run() method is called } };
Call the run method to notify the component to update:
Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); // Get new property value if (value !== this.value || // If value isObject(value) || this.deep) { var oldValue = this.value; // Cache old values this.value = value; // Set new value if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) { handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); } } else { this.cb.call(this.vm, value, oldValue); } } } };
Call this In fact, we will see later that the update of the attribute value and the update of the component are handled in this method. Here, it is judged that when the attribute is changed, the cb callback function passed to the instance during initialization is called, and the callback function accepts two parameters of the old and new values of the attribute. This callback usually exists only for the listening attribute declared by watch, otherwise it is empty by default.
Trace dependent interface instantiation
Each responsive attribute is tracked by a Watcher instance. For different attributes (data, computed, watch), Vue JS handles some differences. The main logic of the interface is as follows:
var Watcher = function Watcher (vm,expOrFn,cb,options) { this.cb = cb; ... // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); } this.value = this.lazy ? undefined : this.get(); };
When initializing the Watcher instance, the expOrFn parameter (expression or function) will be parsed to expand getterthis Getter, and then call this.. The get () method returns the value as this Value value:
Watcher.prototype.get = function get () { pushTarget(this); // Stack watcher instance var value; var vm = this.vm; if (this.user) { try { value = this.getter.call(vm, vm); // Through this Getter get new value } catch (e) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } } else { value = this.getter.call(vm, vm); // Through this Getter get new value } if (this.deep) { // Deep recursive traversal object tracking dependency traverse(value); } popTarget(); // Out of stack watcher instance this.cleanupDeps(); // Clear cache dependency return value // Return new value };
It should be noted here that for the data attribute, rather than the computed attribute or the watch attribute, the this. Of its watcher instance The getter is usually the updateComponent function, that is, the render update component. The get method returns undefined. For the computed calculation attribute, the corresponding specified function will be passed to this Getter, whose return value is the return value of this get method.
data general properties
Vue. The jsdata attribute is an object. You need to call the object observation interface new Observer(value):
function observe (value, asRootData) { if (!isObject(value)) { return } var ob; ob = new Observer(value); // Object observation instance return ob; } // Initial processing data attribute function initData (vm) { // Call the observe function observe(data, true /* asRootData */); }
Calculation properties
Vue.js has different processing of calculation attributes. It is a variable. You can directly call the Watcher interface to pass the calculation rules specified by its attributes as the extended getter of the attributes, that is:
// Initial processing computed calculation property function initComputed (vm, computed) { for (var key in computed) { var userDef = computed[key]; // Corresponding calculation rules // This. Passed to the watcher instance Getter -- expand getter var getter = typeof userDef === 'function' ? userDef : userDef.get; watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions); } }
watch property
The watch attribute is different. It is a variable or expression. Different from the calculated attribute, it needs to specify a callback function after the change event:
function initWatch (vm, watch) { for (var key in watch) { var handler = watch[key]; createWatcher(vm, key, handler[i]); // Delivery callback } } function createWatcher (vm, key, handler) { vm.$watch(key, handler, options); // Callback } Vue.prototype.$watch = function (expOrFn, cb, options) { // Instantiate the watcher and pass the callback var watcher = new Watcher(vm, expOrFn, cb, options); }
Initialize the connection between Watcher and the dependency management interface
In the end, the watcher interface implements the tracking dependency of any attribute. When the component is mounted, the watcher instance will be initialized and bound to Dep.target, that is, the watcher and Dep will be connected, so that the attribute dependency can be tracked when the component is rendered:
function mountComponent (vm, el, hydrating) { ... updateComponent = function () { vm._update(vm._render(), hydrating); ... }; ... vm._watcher = new Watcher(vm, updateComponent, noop); ... }
As above, pass the updateComponent method to the watcher instance, which triggers the VM of the component instance_ The render () render method triggers the component update. This mountComponent() method will be called in the $mount() mount component expose method:
// public mount method Vue$3.prototype.$mount = function (el, hydrating) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) };
summary
So far, the introduction and application of JavaScript attribute descriptor interface, as well as its application in Vue The principle of responsive practice in JS is basically explained. This summary takes a lot of energy from principle to application and then to practical analysis, but the harvest is proportional. I not only have a deeper understanding of the basis of JavaScript, but also become more familiar with Vue JS responsive design principle, and the familiarity with its source code has also been greatly improved. Later, more summary and sharing will be carried out in the process of work and learning.
reference resources
Link address of this article: Parsing Vue. From JavaScript attribute descriptor JS responsive view