preface
In the last chapter, we have made a rough analysis of the entire Vue source code (still in the draft box, which needs to be sorted out before being released), but there are still many things that have not been analyzed in depth. I will carry out further analysis through the following important points.
- In-depth understanding of Vue responsive principles (data interception)
- Dig deeper into how vue.js does “dependency collection” to accurately track all changes
- Learn more about the Virtual DOM
- Learn more about vue.js batch asynchronous update strategy
- In-depth understanding of vue.js internal operation mechanism, understand the principle behind calling each API
In this chapter, we analyze how vue.js performs “dependency collection” to accurately track all changes.
Initialize the Vue
We simply instantiate an instance of Vue. The following are our in-depth thoughts on this simple instance:
// app Vue instance
var app = new Vue({
data: {
newTodo: ' ',
},
// watch todos change for localStorage persistence
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false,
before: function () {
}
}
}
})
// mount
app.$mount('.todoapp')
Copy the code
initState
We have added a watch attribute configuration above:
We can see from the above code that we have configured a configuration item whose key is newTodo. We can understand from the above code:
When the value of newTodo changes, we need to execute the Hander method, so let’s look at how this works.
Let’s start with the initState method:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
Copy the code
InitWatch:
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for(var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}Copy the code
From the above code analysis, we can find that watch can have multiple Handers, written as follows:
watch: {
todos:
[
{
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
},
{
handler: function (todos) {
console.log(todos)
},
deep: true}},Copy the code
Let’s analyze the createWatcher method:
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
Copy the code
Conclusion:
- And from this method, we know that actually our
hanlder
It could be onestring
- And this
hander
isvm
A method on an object that we have examined previouslymethods
Methods inside are eventually mounted invm
On the instance object, you can directly passvm["method"]
Visit, so we found againwatch
Another way of writing phi is to give it directlywatch
thekey
Assign a string name directly, which can bemethods
A way to set one inside:
watch: {
todos: 'newTodo'
},
Copy the code
methods: {
handlerTodos: function (todos) {
todoStorage.save(todos)
}
}
Copy the code
Next, call the $watch method
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\" ")); }}return function unwatchFn() { watcher.teardown(); }};Copy the code
In this method, we see that there is an immediate attribute, which means “immediately”. If we set this to true, the watch hander will be executed immediately. If not, The watcher is executed asynchronously. So this attribute might be useful in some business scenarios.
In this method, a new Watcher object is created. This object is a highlight, and we need to take a look at this object. The code looks like this (delete the code that only keeps the core):
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
vm._watchers.push(this);
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if(! this.getter) { this.getter = noop; } } this.value = this.lazy ? undefined : this.get(); };Copy the code
We mainly did the following things:
- will
watcher
The object is saved invm._watchers
In the - To obtain
getter
.this.getter = parsePath(expOrFn);
- perform
this.get()
To obtainvalue
The parsePath method returns a function:
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('. ');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if(! obj) {return }
obj = obj[segments[i]];
}
return obj
}
}
Copy the code
Call value = this.get. call(vm, vm) in the call this.get() method;
Obj = obj[segments[I]]; To value, such as vm. NewTodo, we have an in-depth understanding of Vue response principle (data interception), we already know that Vue will intercept all data in data, as follows:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if(Array.isArray(value)) { dependArray(value); }}}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
}
/* eslint-enable no-self-compare */
if(customSetter) { customSetter(); } / /#7981: for accessor properties without setter
if(getter && ! setter) {return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
Copy the code
So when we call vm.newTodo, the getter is triggered, so let’s take a closer look at the getter methods
getter
The getter code looks like this:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if(Array.isArray(value)) { dependArray(value); }}}return value
}
Copy the code
- So we get the value
var value = getter ? getter.call(obj) : val;
- call
Dep
The object’sdepend
Methods,dep
The object is saved intarget
Properties of theDep.target.addDep(this);
whiletarget
Is aWatcher
Object with the following code:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if(! this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep);if(! this.depIds.has(id)) { dep.addSub(this); }}};Copy the code
The generated Dep object is shown below:
Now that we have completed dependency collection, let’s analyze how to accurately track all changes as data changes.
Track all changes accurately
We can try to modify the value of an attribute in data, such as newTodo, by entering the set method, which looks like this:
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(customSetter) { customSetter(); } / /#7981: for accessor properties without setter
if(getter && ! setter) {return }
if (setter) {
setter.call(obj, newVal);
} else{ val = newVal; } childOb = ! shallow && observe(newVal); dep.notify(); }Copy the code
Now LET me analyze this method.
- First check the new value and the old value. If they are equal, return
- call
dep.notify();
To inform allsubs
.subs
Is a type isWatcher
An array of objectssubs
The data in it, we analyzed abovegetter
Logically maintainedwatcher
Object.
The notify method iterates through the subs array and performs update().
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if(! config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); }};Copy the code
If it’s asynchronous, sort it, first in, first out, and iterate through the update() method.
Watcher.prototype.update = function update () {
/* istanbul ignore else* /if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else{ queueWatcher(this); }};Copy the code
The above method is divided into three cases:
- if
watch
Configure thelazy
(lazy), not executed immediately (when will be analyzed later) - If configured
sync
(Synchronization) istrue
It is executed immediatelyhander
methods - In the third case, it will be added to
watcher
Queue (queue
)
We’ll focus on the third case, and here’s the queueWatcher source code
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if(! flushing) { queue.push(watcher); }else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if(! waiting) { waiting =true;
if(! config.async) { flushSchedulerQueue();return} nextTick(flushSchedulerQueue); }}}Copy the code
- First of all,
flushing
The default isfalse
, so willwatcher
Stored in thequeue
In the array of. - then
waiting
The default isfalse
, so will goif(waiting)
branch config
isVue
Global configuration, itsasync
(Asynchronous) The default value istrue
, so it will be executednextTick
Function.
Now let’s analyze the nextTick function
nextTick
The nextTick code is as follows:
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
if (useMacroTask) {
macroTimerFunc();
} else{ microTimerFunc(); / /}}$flow-disable-line
if(! cb && typeof Promise ! = ='undefined') {
return new Promise(function(resolve) { _resolve = resolve; }}})Copy the code
NextTick mainly does the following:
- The parameters to be passed
cb
The execution of an anonymous function is stored in acallbacks
The array of pending
anduseMacroTask
Is the default value offalse
, so it will be executedmicroTimerFunc()
(the Task)microTimerFunc()
Is defined as follows:
if(typeof Promise ! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
Copy the code
In fact, use the Promise function (only analyze the Promise compatibility), and Promise is a micro Task that must wait for all macro tasks to be executed, that is, when the main thread is free to execute the micro Task;
Now let’s take a look at the flushCallbacks function:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for(var i = 0; i < copies.length; i++) { copies[i](); }}Copy the code
It’s very simple,
- The first is change
pending
The status offalse
- Traverse the execution
callbacks
The functions in the array, we remember in thenextTick
In the function, we willcb
Stored in thecallbacks
In the.
Let’s look at the definition of cb. We call nextTick(flushSchedulerQueue); , so cb refers to flushSchedulerQueue, whose code is as follows:
function flushSchedulerQueue () {
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
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();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if* /if (devtools && config.devtools) {
devtools.emit('flush'); }}Copy the code
- First of all to
flushing
The status switch becomestrue
- will
queue
Conducted in accordance with theID
Ascending sort,queue
Is in thequeueWatcher
Method, will correspond toWatcher
Preserved in it. - traverse
queue
To execute the correspondingwatcher
therun
Methods. - perform
resetSchedulerState()
Is to reset the status value, such aswaiting = flushing = false
- perform
callActivatedHooks(activatedQueue);
Update component ToDO: - perform
callUpdatedHooks(updatedQueue);
Call the lifecycle functionupdated
- perform
devtools.emit('flush');
Refresh the debugging tool.
We iterate through the queue to execute the corresponding Watcher’s run method in 3. We found two Watchers in queue, but we initialized Vue in app.js with watch code as follows:
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false}}Copy the code
NewTodo = newTodo (); newTodo = newTodo (); newTodo = newTodo (); newTodo ();
Conclusion:
- In our configuration
watch
Property, generated byWatcher
Object, which is only responsible for callinghanlder
Methods. Will not be responsible for UI rendering - another
watch
In fact, aVue
Built-in oneWatch
(Personal understanding), but when we callVue
the$mount
Method generated when, as we did in ourapp.js
Call this method directly in:app.$mount('.todoapp')
The other method does not call the method directly, but instead initializes itVue
In the configuration ofel: '.todoapp'
Properties will do. thisWatcher
Responsible for the final rendering of the UI, which is very important, and we’ll get into that laterWatcher
$mount
Method is the last method to execute, so it generatesWatcher
The object’sId
It’s the largest, so we’re traversingqueue
Before, we were going to do an ascending sort, restricting all of themWatch
Generated in configurationWatcher
Object is executed last$mount
Generated in theWatcher
Object to do UI rendering.
$mount
Let’s look at how the $mount method generates the Watcher object and what its CB is. The code is as follows:
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate'); }}},true /* isRenderWatcher */);
Copy the code
- From the code above, we can see the last parameter
isRenderWatcher
The value set istrue
Is a Render Watcherwatch
Configured and generated inWatcher
This value is bothfalse
, we are inWatcher
As can be seen in the constructor of:
if (isRenderWatcher) {
vm._watcher = this;
}
Copy the code
If isRenderWatcher is true, mount this particular Watcher directly to the _watcher property of the Vue instance, so in the flushSchedulerQueue method we call callUpdatedHooks, Only this Watcher executes the lifecycle function updated
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if(vm._watcher === watcher && vm._isMounted && ! vm._isDestroyed) { callHook(vm,'updated'); }}}Copy the code
- Second parameter
expOrFn
, that is,Watcher
thegetter
, will be instantiatedWatcher
Is called whenget
Method, and then executevalue = this.getter.call(vm, vm);
In this case, it will be executedupdateComponent
Method, which is a key UI rendering method that we won’t go into here. - The third parameter is
cb
, an empty method is passed in - The fourth argument passes one
options
Object, pass one herebefore
BeforeUpdate, a mid-life function that is executed before the UI is re-rendered
We have analyzed one working process of the Watch above, and now let’s analyze the differences between computed work and the Watch.
computed
First, when the Vue object is instantiated, also in the initState method, computed is handled and the initComputed method is executed with the following code:
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if(! isSSR) { // create internal watcherfor the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if(! (keyin vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); }}}}Copy the code
The code above is quite long, but we can summarize the following points:
var watchers = vm._computedWatchers = Object.create(null);
invm
One is mounted on the instance object_computedWatchers
Property, saved bycomputed
All of the generatedwatcher
- And then go through all of them
key
Each key generates onewatcher
var getter = typeof userDef === 'function' ? userDef : userDef.get;
You can extend from this codecomputed
As follows:
Computed: {// notation 1: Directly onefunction
// strLen: function () {
// console.log(this.newTodo.length)
// returnThis.newtodo.length //}, // it can be an object, but it must have a get method, // But it doesn't make sense to write it as an object, because other attributes are not used. strLen: { get:function () {
console.log(this.newTodo.length)
return this.newTodo.length
}
}
}
Copy the code
- If it is not a server rendering, one is generated
watcher
Object and is saved invm._computedWatchers
Property, but this has nothing to do withwatch
The generatedwatcher
One important difference is that you pass a propertycomputedWatcherOptions
Object that is configured with a lazy: ture
In Watcher’s constructor, we have the following logic:
this.value = this.lazy
? undefined
: this.get();
Copy the code
Because this.lazy is true, this.get() will not be executed; Therefore, the corresponding methods configured in computed are not executed immediately.
defineComputed(vm, key, userDef);
Is tocomputed
Properties directly mounted invm
Up, you can go straight throughvm.strLen
In this method, however, there is a distinction between server rendering and not server rendering, which is performed immediatelycomputed
Gets the value, but on the Web it is not executed immediately, but givenget
Assign a function:
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
If we reference computed properties in our template, such as:
, $mount is called to render the template, and the above computedGetter method is executed to obtain the value:
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
Copy the code
This.get () is the same as this.get() in watch analysis above.
Think about:
We have basically analyzed the basic process of computed logic above, but we still seem to have no connection. How can we notify computed updates when the value in our data changes? Our computed data is as follows:
computed: {
strLen: function () {
return this.newTodo.length
},
}
Copy the code
What about strLen’s method when we change this.newTodo?
The answer:
- Above we have analyzed our in our
template
In the referencestrLen
, such as<div>{{strLen}}</div>
, in the$mount
To render the templatestrLen
And then it will be executedcomputedGetter
Method to get the value, and then callget
The method, which is uscomputed
Configured functions:
computed: {
strLen: function () {
return this.newTodo.length
}
},
Copy the code
- When the above method is executed, the reference is called
this.newTodo
, will enterreactiveGetter
Methods (In-depth understanding of Vue responsive principles (data interception))
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if(Array.isArray(value)) { dependArray(value); }}}return value
}
Copy the code
The current Watcher object is added to the DEP.subs queue.
- when
this.newTodo
When the value changes, it is executedreactiveSetter
Method when executeddep.notify();
“, will be executedcomputed
Inside the way to achieve whendata
When the value inside changes, it references thisdata
Properties of thecomputed
It will be executed immediately. - If we define it
computed
But there is no reference to this anywherecomputed
Even if the correspondingdata
Property changes are not executedcomputed
Method, even if executed manuallycomputed
Methods, such as:app.strLen
It’s not going to work because it’sWatcher
theaddDep
Method that has been judged for the currentwatcher
Not a new onewatcher
了
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if(! this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep);if(! this.depIds.has(id)) { dep.addSub(this); }}};Copy the code