To get into the inner workings of a responsive system, we devoted an entire section to how data (including Data, computed, and props) is initialized as responsive objects. With the knowledge of responsive data objects, we built a responsive system using data as data in the second half of the previous section on the basis of retaining the source code structure. In this section, we continue to dig into the details of the internal construction of a responsive system and analyze in detail Vue’s processing of data and computed in a responsive system.
7.8 Related Concepts
In building simple responsive systems, we have introduced several important concepts that are at the heart of responsive principle design. Let’s briefly review them:
Observer
Class, instantiate oneObserver
Class will passObject.defineProperty
Of the datagetter,setter
Method to rewrite ingetter
phasesCollection of dependenciesIn the data update phase, triggersetter
methodsDependent updatewatcher
Class, instantiatedwatcher
A class creates a dependency, and the simple understanding is that a dependency is created where the data is used. Each dependency is notified to update when the data changes, as mentioned earlier in renderingwathcer
Is renderingdom
When using data generated dependencies.Dep
Class, sincewatcher
Understood as the dependencies that each piece of data needs to listen to, the collection and notification of those dependencies needs to be managed by another class, this oneDep
.Dep
There are only two things you need to do: collect dependencies and distribute update dependencies.
These are the three basic core concepts of responsive system construction and are the basis of this section. If you are not impressed, please review the previous section on minimalist wind responsive system construction.
7.9 the data
7.9.1 Question thinking
Before we start to analyze the data, we will throw out a few questions for the reader to think about, and the answers will be included in the following content analysis.
-
We already know that the Dep is used as a container for managing dependencies, so when is this container generated? So when does the instantiation Dep happen?
-
What types of dependencies does THE Dep collect? What are Watcher’s categories of dependencies, what are the scenarios, and what are the differences?
-
What exactly does the Observer class do with getter,setter methods?
-
If both the hand-written Watcher and the page data rendering watch are listening for changes in data, what is the priority?
-
With the collection of dependence, is there still dependency relief, dependency relief in the meaning of where?
With these questions in mind, we begin to analyze the responsive details of Data.
7.9.2 Dependency Collection
During initialization, data instantiates an Observer class defined as follows (ignoring data of array type):
// initData
function initData(data) {
···
observe(data, true)}// observe
function observe(value, asRootData) {... ob =new Observer(value);
return ob
}
Getter and setter methods are overridden for all properties of the object as long as the object is set to have an observer property. Getter and setter methods collect and distribute dependent updates for all properties of the object
var Observer = function Observer (value) {...// Set the __ob__ property to non-enumerable. External cannot be obtained by traversal.
def(value, '__ob__'.this);
// Array processing
if (Array.isArray(value)) {
···
} else {
// Object processing
this.walk(value); }};function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable:!!!!! enumerable,// Whether enumerable
writable: true.configurable: true
});
}
Copy the code
The Observer adds an __ob__ attribute to data. The __ob__ attribute is used as an indication of a responsive object, and the def method ensures that the attribute is non-enumerable, meaning that it cannot be iterated to obtain its value. In addition to marking reactive objects, the Observer class also calls the walk method on the prototype, iterating through getter and setter overrides for each property on the object.
Observer.prototype. Walk = function walk (obj) {var keys = object.keys (obj); for (var i = 0; i < keys.length; i++) { defineReactive###1(obj, keys[i]); }};Copy the code
DefineReactive# ##1 is the core of reactive build. It instantiates a Dep class, creating a dependent management for each data, and then overrides getter and setter methods using object.defineproperty. Here we only analyze code that depends on the collection.
function defineReactive# # # 1 (obj,key,val,customSetter,shallow) {
// Create a dependency management by instantiating one Dep class per data
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
// Attributes must be configurable
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
// This part of the logic is for deep objects. If the object's property is an object, the recursive call instantiates the Observe class to convert its property value to a responsive object
varchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
enumerable: true.configurable: true,s
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
// Add deP data for the current watcher
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) { dependArray(value); }}}return value
},
set: function reactiveSetter (newVal) {}}); }Copy the code
We know that when a property value in data is accessed, it will be intercepted by the getter function. According to our old knowledge system, we can know that the instance will create a rendering watcher before mounting.
new Watcher(vm, updateComponent, noop, {
before: function before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate'); }}},true /* isRenderWatcher */);
Copy the code
At the same time, the updateComponent logic performs the instance mount, during which the template is parsed first into the Render function, which, when converted to Vnode, accesses the defined data, triggering gettter for dependency collection. The data collection relies on the render Watcher itself.
The dependency collection phase in your code does several things:
- Add owned data for the current Watcher (in this case, the render Watcher).
- Dependencies that need to be listened on for the current data collection
How to understand these two points? Let’s look at the implementation in the code first. The getter phase executes dep.depend(), which is the method dep class defines on the prototype.
dep.depend();
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); }};Copy the code
Target is the currently executing watcher. In the render phase, dep. target is the render Watcher instantiated when the component is mounted, so the Depend method will call the addDep method of the current Watcher to add dependent data to the Watcher.
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
// newDepIds and newDeps record the data owned by Watcher
this.newDepIds.add(id);
this.newDeps.push(dep);
// Avoid adding the same data collector repeatedly
if (!this.depIds.has(id)) {
dep.addSub(this); }}};Copy the code
Where newDepIds are data structures with unique members of Set and newDeps are arrays, they are used to record the data currently owned by watcher. This process will make logical judgment to avoid adding the same data more than once.
AddSub adds the Watcher that needs to be listened on for each data-dependent collector.
Dep.prototype.addSub = function addSub (sub) {
// Add the current watcher to the data dependency collector
this.subs.push(sub);
};
Copy the code
getter
If an object is encountered with an attribute value, dependencies are collected for each value of that object
If we change a basic type of reactive data into an object, the properties in the new object must also be set to reactive data.
- We do special processing when we encounter an array of property values, which we’ll talk about later.
To summarise the dependency collection process in a layman’s way, each data is a dependency manager, and each place where data is used is a dependency. When data is accessed, the currently accessed scenario is collected as a dependency into the dependency manager, as well as the owned data for the dependencies for that scenario.
7.9.3 Sending updates
In analyzing a dependency collection, there may be some confusion as to why so many relationships should be maintained. What role do these relationships play when the data is updated? With that in mind, let’s take a look at the process of distributing updates. When the data changes, the defined setter methods are executed, so let’s look at the source code.
ObjectDefineProperty (obj, key, {... set:function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
// The new value is equal to the old value
if(newVal === value || (newVal ! == newVal && value ! == value)) {return}...// When the new value is an object, the dependency collection process is performed for the new object
childOb = !shallow && observe(newVal);
dep.notify();
}
})
Copy the code
The distribution phase does the following:
- Check whether the data changes are consistent. If the data changes are consistent, no update operation is performed.
- When the new value is an object, the dependency collection process is performed on the properties of that value.
- Notifying the data collection
watcher
Dependency, iterating through eachwatcher
Update dataThis is the phase that invokes the data-dependent collectordep.notify
Method for the distribution of updates.
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
if(! config.async) {// Sort by dependent id
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
// Iterate over each dependency to update the data.subs[i].update(); }};Copy the code
- Each will be updated when
watcher
Push into the queue and wait for the next onetick
Take each out when it arriveswatcher
forrun
operation
Watcher.prototype.update = function update () {
···
queueWatcher(this);
};
Copy the code
The call to the queueWatcher method pushes the dependencies collected by the data in turn into the Queue array, which updates the view based on the cached results in the next event loop ‘TICK’. When performing a view update, it is inevitable that new dependencies will be added to the render template due to data changes, which in turn will perform queueWatcher procedures. So you need a flag bit to record whether you are in the queue for an asynchronous update process. This flag bit is flushing, so when we do an asynchronous update, the new watcher is inserted into the queue.
function queueWatcher (watcher) {
var id = watcher.id;
// Execute the same watcher only once
if (has[id] == null) {
has[id] = true;
if(! flushing) { queue.push(watcher); }else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1.0, watcher); }... nextTick (flushSchedulerQueue); }}Copy the code
The principle and implementation of nextTick will not be discussed. Generally speaking, nextTick will buffer multiple data processing processes and then execute DOM operations in the next event cycle TICK. The principle of nextTick is to realize asynchronous update by using the microtask queue of the event cycle.
When the next tick arrives, the flushSchedulerQueue method is executed, which takes the collected queue array (which is a collection of Watchers) and sorts the array dependencies. Why do I sort? The source code explains three points:
- Component creation is parent, so component updates are parent, so parent rendering needs to be guaranteed
watcher
Precedence over child renderingwatcher
The update.- The user – defined Watcher is called user Watcher. User Watcher and Render Watcher are implemented first. Since User Watchers was created before Render Watcher, user Watcher is implemented first.
- If a component is in the parent component’s
watcher
The execution phase is destroyed, then it corresponds towatcher
Execution can be skipped.
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// Sort the watcher of the queue
queue.sort(function (a, b) { return a.id - b.id; });
// Loop through queue.length to ensure that the length of queue changes due to new dependencies being added at render time.
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// If watcher defines the before configuration, the before method takes precedence
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if(has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\" ")
: "in a component render function."
),
watcher.vm
);
break}}}// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// Reset the recovery state and empty the queue
resetSchedulerState();
// Call other hooks after view changes
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush'); }}Copy the code
In flushSchedulerQueue, four important processes can be summarized as follows:
- right
queue
In thewatcher
For reasons summarized above.- traverse
watcher
, if the currentwatcher
There arebefore
If yes, run the commandbefore
Method corresponding to the previous renderwatcher
: the renderingwatcher
When we instantiate, we passbefore
The function, which is the next onetick
Is called before updating the viewbeforeUpdate
Lifecycle hooks.- perform
watcher.run
Modify operations.- Reset the recovery state, which restores some process-controlled state variables to their initial values and clears records
watcher
In the queue.
new Watcher(vm, updateComponent, noop, {
before: function before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate'); }}},true /* isRenderWatcher */);
Copy the code
Focus on the operation watcher.run().
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if( value ! = =this.value || isObject(value) || this.deep ) {
// Set the new value
var oldValue = this.value;
this.value = value;
// For user watcher, do not analyze for now
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); }}}};Copy the code
The new value will be evaluated. If the new value meets the criteria,cb will be executed. Cb is the callback passed in when watcher is instantiated.
Before looking at the GET method, let’s look back at a few property definitions of the Watcher constructor
var watcher = function Watcher(
vm, //Component instance expOrFn,//I'm going to do cb,//The callback options,//Configuration isRenderWatcher//Whether to render watcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !! options.deep;this.user = !! options.user;this.lazy = !! options.lazy;this.sync = !! options.sync;this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.', vm ); }}// Lazy is the evaluation attribute flag. When watcher is the evaluation watcher, it is not understood to execute the get method to evaluate
this.value = this.lazy
? undefined
: this.get();
}
Copy the code
The get method is defined as follows:
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch(e) {...}finally{...// Restore dep. target to its previous state, depending on the collection process
popTarget();
this.cleanupDeps();
}
return value
};
Copy the code
The get method evaluates this. Getter, which performs view updates under the current render watcher condition. This stage rerenders the page components
new Watcher(vm, updateComponent, noop, { before: (a)= >{}},true);
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
Copy the code
After the getter method is executed, the last step is to cleanup the dependency, which is cleanupDeps.
Here’s a scenario for dependency cleanup: We often use the v – if for template switch, the switch can perform different template in the process of rendering, if A template to monitor data, A template to monitor data, B B when rendering the template B, without relying on old removal, in B template scenarios, the change of the data can also cause A dependent to apply colours to A drawing update, which can cause the performance of the waste. Therefore, the removal of old dependencies is necessary during the optimization phase.
// Depend on the cleanup process
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this); }}var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
Copy the code
Summarize the above analysis into the last two points that rely on distributed updates
- perform
run
The action will be executedgetter
Method, that is, recalculate the new value, for renderingwatcher
Is re-executedupdateComponent
Update the view - recalculate
getter
After, the dependency is cleared
7.10 the computed
Calculated attributes are designed for simple calculations, because putting too much logic into a template can make it too heavy and difficult to maintain. For computed analysis, we still follow the two processes of dependent collection and distributed update.
7.10.1 Relying on Collection
Initialization of computed iterates through every attribute value of computed and instantiates a computed Watcher for each attribute, with {lazy: True} is a symbol for computed Watcher, and you end up calling defineComputed to set the data to reactive, with the source code as follows:
function initComputed() {...for(var key in computed) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
if(! (keyinvm)) { defineComputed(vm, key, userDef); }}For computed Watcher, the lazy attribute is true
var computedWatcherOptions = { lazy: true };
Copy the code
The logic of defineComputed is similar to that of analyzing data, and eventually object.defineProperty is called for data interception. Specific definitions are as follows:
function defineComputed (target,key,userDef) {
// Non-server rendering caches the getter
varshouldCache = ! isServerRendering();if (typeof userDef === 'function') {
//
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
Copy the code
The situation in the rendering of a service, calculation results will be cached attributes, the sense of the cache is that only in the relevant responsive data changes, the computed to evaluate, the rest of the multiple access attribute’s value will be calculated before returning the result of calculation, this is the cache optimization, computed attribute has two kinds of writing, One is a function, and the other is an object, which is written to provide getters and setters.
When a computed property is accessed, a getter method is triggered for dependency collection. Look at the implementation of createComputedGetter.
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
Copy the code
The function returned by the createComputedGetter gets a computed watcher attribute first during execution. Dirty indicates whether a calculation has been performed, and watcher. Evaluate does not evaluate, which is how caching works.
Watcher.prototype.evaluate = function evaluate () {
Evaluate is used to perform the evaluate callback for the evaluate attribute
this.value = this.get();
this.dirty = false;
};
Copy the code
The get method, as described earlier, calls the execution function passed when the Watcher is instantiated. In the case of the Computer Watcher, the execution function is the calculation function that evaluates the property. It can be either a function or a getter method for an object.
Enumerating a scenario to avoid disconnection with data processing. Computed In the computing phase, if an attribute value of data is accessed, the getter method of data data is triggered for dependency collection. According to the previous analysis, the Dep collector of data collects the current Watcher as a dependency. This watcher is computed Watcher, and the accessed data Dep is added to the current watcher
Return to the this.get() method of calculating the execution function. After the getter is executed, the dependency will also be cleared. The principle and purpose refer to the analysis of the data stage. After get is executed, it enters Watcher. Depend for dependency collection. The collection process is consistent with data, with the current computed Watcher collected as a dependency collector in THE Dep.
This is the whole process of computed dependent collection, which caches the results of operations and avoids repeated operations, as opposed to data dependent collection.
7.10.2 Sending updates
The condition for distributing updates is that the data in the data has changed, so most of the logics are consistent with the data analysis. Let’s make a summary.
- When the calculated attribute dependency data is updated, due to the data
Dep
collectcomputed watch
This dependency, so will be calleddep
thenotify
Method to update the status of dependencies. - At this time
computed watcher
And what we introduced beforewatcher
Instead, it does not perform the dependent update operation immediately, but through onedirty
Mark it. Let’s go backDepend on the update
The code.
Dep.prototype.notify = function() {...for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
Watcher.prototype.update = function update () {
// Compute the property branch
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); }};Copy the code
Due to the lazy attribute, the update procedure does not perform a status update and only marks dirty as true.
- Due to the
data
Data ownership renderingwatcher
This dependency is executed at the same timeupdateComponent
To re-render the view, whilerender
Procedure will access the calculated properties, at this point due tothis.dirty
A value oftrue
, and the evaluated property is reevaluated.
7.11 summary
On the theoretical basis of the previous section, we analyzed in depth how Vue uses data and computed to build responsive systems. The core of the responsive system is to intercept the getter and setter of the data by using Object.defineProperty. The core of the processing is to collect the dependencies of the scene where the data is accessed and inform the collected dependencies to update when the data is changed. In this section, we have taken a closer look at responsive processing in data and computed, both of which have very similar but different processing logic. Computed results are cached in the source code to avoid the problem of frequent double-counting when used in multiple places. Due to space constraints, we’ll leave the analysis of user – defined Watcher to the next section. There is still a puzzle left in this article, which will be solved one by one in the future.
- An in-depth analysis of Vue source code – option merge (1)
- An in-depth analysis of Vue source code – option merge (2)
- In-depth analysis of Vue source code – data agents, associated child and parent components
- In-depth analysis of Vue source code – instance mount, compile process
- In-depth analysis of Vue source code – complete rendering process
- In-depth analysis of Vue source code – component foundation
- In-depth analysis of Vue source code – components advanced
- An in-depth analysis of Vue source code – Responsive System Building (PART 1)
- In – Depth Analysis of Vue source code – Responsive System Building (Middle)
- An in-depth analysis of Vue source code – Responsive System Building (Part 2)
- In-depth analysis of Vue source code – to implement diff algorithm with me!
- In-depth analysis of Vue source code – reveal Vue event mechanism
- In-depth analysis of Vue source code – Vue slot, you want to know all here!
- In-depth analysis of Vue source code – Do you understand the SYNTAX of V-Model sugar?
- In-depth analysis of Vue source – Vue dynamic component concept, you will be confused?
- Thoroughly understand the keep-alive magic in Vue (part 1)
- Thoroughly understand the keep-alive magic in Vue (2)