0. Preface
There are 3 instance methods related to data, which are vm.$set
, , vm.$delete
and vm.$watch
. They are stateMixin
mounted on the prototype in the function Vue
, the code is as follows:
import {
set, del } from "../observer/index";
export function stateMixin(Vue) {
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function(expOrFn, cb, options) {
};
}
After the function is executed stateMixin
, Vue
the above three instance methods will be mounted on the prototype.
Next, let's analyze the internal principles of these three data-related instance methods.
1. vm.$watch
1.1 Usage review
Before introducing the internal principle of the method, let's review its usage based on the official documentation example.
vm.$watch(expOrFn, callback, [options]);
-
Parameters :
-
{string | Function} expOrFn
-
{Function | Object} callback
-
{Object} [options]
{boolean} deep
{boolean} immediate
-
-
return value :
{Function} unwatch
-
Usage :
An expression or computed property function that observes
Vue
instance changes. The parameters obtained by the callback function are the new value and the old value. Expressions only accept supervised key paths. For more complex expressions, use a function instead.NOTE: When mutating (not replacing) an object or array, the old value will be the same as the new value, since their references point to the same object/array.
Vue
A copy of the value before mutation is not kept. -
Example :
// 键路径 vm.$watch("a.b.c", function(newVal, oldVal) { // 做点什么 }); // 函数 vm.$watch( function() { // 表达式 `this.a + this.b` 每次得出一个不同的结果时 // 处理函数都会被调用。 // 这就像监听一个未被定义的计算属性 return this.a + this.b; }, function(newVal, oldVal) { // 做点什么 } );
vm.$watch
Returns a cancel observer function to stop firing callbacks:var unwatch = vm.$watch("a", cb); // 之后取消观察 unwatch();
-
option: deep
In order to discover changes in the object's internal values, it can be specified in the options parameter
deep: true
. Note that monitoring array changes does not require this.vm.$watch("someObject", callback, { deep: true }); vm.someObject.nestedValue = 123; // callback is fired
-
Option: immediate
Specifying in the options argument
immediate: true
will trigger the callback immediately with the current value of the expression:vm.$watch("a", callback, { immediate: true }); // 立即以 `a` 的当前值触发回调
Note that with
immediate
the option, you cannot unlisten the given property on the first callback.// 这会导致报错 var unwatch = vm.$watch( "value", function() { doSomething(); unwatch(); }, { immediate: true } );
If you still want to call an unlisten function inside the callback, you should check the availability of its function first:
var unwatch = vm.$watch( "value", function() { doSomething(); if (unwatch) { unwatch(); } }, { immediate: true } );
1.2 Internal Principles
$watch
The definition of is located in the source code src/core/instance/state.js
, as follows:
Vue.prototype.$watch = function(expOrFn, cb, options) {
const vm: Component = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {
};
options.user = true;
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() {
watcher.teardown();
};
};
It can be seen that $watch
the code of the method is not much, and the logic is not very complicated.
Inside the function, first determine whether the incoming callback function is an object, like the following form:
vm.$watch("a.b.c", {
handler: function(val, oldVal) {
/* ... */
},
deep: true
});
If the callback function passed in is an object, it means that the user passed in the second parameter callback function cb
and the third parameter option options
together. At this time, createWatcher
the function is called. The function is defined as follows:
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);
}
cb
It can be seen that inside the function, the callback function and parameters are stripped from the object passed in by the user options
, and then the method is called in a conventional way $watch
and the stripped parameters are inserted.
Then get the value passed in by the user options
. If the user does not pass it in, it will be assigned a default empty object, as follows:
options = options || {
};
$watch
An instance will be created inside the method watcher
. Since the instance is $watch
created by the user manually calling the method, attributes are options
added and assigned a value to distinguish the instance created by the user from the instance created internally , as follows:user
true
watcher
Vue
watcher
options.user = true;
Next, pass in the parameters to create an watcher
instance, as follows:
const watcher = new Watcher(vm, expOrFn, cb, options);
Then judge if the user options
specifies in the option immediate
parameter true
, then immediately trigger the callback with the current value of the observed data, as follows:
if (options.immediate) {
cb.call(vm, watcher.value);
}
Finally, a cancel observation function is returned unwatchFn
to stop triggering the callback. as follows:
return function unwatchFn() {
watcher.teardown();
};
This cancel observation function unwatchFn
actually calls the method watcher
of the instance teardown
, so let's take a look at teardown
how this method is implemented. Its code is as follows:
export default class Watcher {
constructor(/* ... */) {
// ...
this.deps = [];
}
teardown() {
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
}
}
In the previous article introducing change detection, we said that whoever reads the data means that whoever relies on the data, then who will exist in the dependency list of the data, and will be notified when the data changes who. In other words, if anyone does not want to rely on this data, then just delete whoever is from the dependency list of this data.
In the above code, watcher
when an instance is created, the observed data will be read. Reading the data means that it depends on the data, so the watcher
instance will exist in the data dependency list, and watcher
the instance also records which data it depends on. In addition, we also said that each data has its own dependency manager dep
. watcher
The instance records which data it depends on. In fact, it dep
stores the dependency manager of these data in the properties watcher
of the instance this.deps = []
. When the observation is cancelled, watcher
the instance does not want to rely on these data, then traverse the dependency manager of these data recorded by yourself, and tell these data to delete me from your dependency list.
for example:
vm.$watch(
function() {
return this.a + this.b;
},
function(newVal, oldVal) {
// 做点什么
}
);
For example, in the above watcher
example, it observes data a
and data b
, then it depends on data a
and data b
, then this watcher
instance exists in the dependency manager of data and data a
, and these two dependency managers are also recorded in the properties of the instance , that is ,b
depA
depB
watcher
deps
this.deps=[depA,depB]
When canceling the observation, it traverses this.deps
, and let each dependency manager call its removeSub
method to watcher
delete this instance from its own dependency list.
There is one last question below, when the attribute options
in the option parameter isdeep
, how to achieve in-depth observation?true
First of all, let's take a look at what is in-depth observation. If there is the following observed data:
obj = {
a: 2
};
The so-called in-depth observation means that obj
we will be notified when the object changes, and obj.a
we should also be notified when the property changes. Simply put, it is to observe the change of the internal value of the object.
It is not difficult to realize this function. We know that if we want to notify us when the data changes, then we only need to become a dependency of this data, because when the data changes, all its dependencies will be notified, so how to become a data dependency? , very simple, just read the data. That is to say, we only need to recursively read all the values inside the object watcher
when creating an instance , then this instance will be added to the dependency list of all values in the object, and then when any value in the object changes when you will be notified.obj
watcher
After having a preliminary idea, let's see how it is implemented in the code. We know that watcher
when an instance is created, the method Watcher
in the class will be executed get
to read the observed data, as follows:
export default class Watcher {
constructor(/* ... */) {
// ...
this.value = this.get();
}
get() {
// ...
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
return value;
}
}
It can be seen that in get
the method, if the input deep
is true
, the function will be called traverse
, and in the source code, there is a very vivid comment on this step:
"touch" every property so they are all tracked as dependencies for deep watching
“触摸”每个属性,以便将它们全部作为深度监视的依赖项进行跟踪
The so-called "touching" each attribute means reading each attribute once? Ha ha
Back to the code, traverse
the function definition is as follows:
const seenObjects = new Set();
export function traverse(val: any) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse(val: any, seen: SimpleSet) {
let i, keys;
const isA = Array.isArray(val);
if (
(!isA && !isObject(val)) ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return;
}
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
}
It can be seen that this function is actually a recursive traversal process, recursively traversing and reading all the internal values of the observed data.
First judge the incoming val
type, if it is not Array
or object
, or has been frozen, then return directly and exit the program. as follows:
const isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return;
}
Then get it val
and dep.id
store it in the created collection seen
, because the collection has a natural deduplication effect compared to the data, so as to ensure that dep.id
there is no duplication of the stored data, and it will not cause repeated collection dependencies, as follows:
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
Next, judge if it is an array, loop the array, and recursively call each item in the array _traverse
; if it is an object, take out all the objects key
, then perform a read operation, and then recurse the internal value, as follows:
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
In this way, after recursively reading all the values in the observed data, this watcher
instance will be added to the dependency list of all values in the object, and then you will be notified when any value in the object changes up.
2. vm.$set
vm.$set
is an aliasVue.set
for the global and is used in the same way.
2.1 Usage review
Before introducing the internal principle of the method, let's review its usage based on the official documentation example.
vm.$set(target, propertyName / index, value);
-
Parameters :
{Object | Array} target
{string | number} propertyName/index
{any} value
-
Return value : the set value.
-
Usage :
Add a property to the reactive object and make sure the new property is also reactive and triggers a view update. It must be used to add new properties to reactive objects, since
Vue
there is no way to detect normal new properties (egthis.myObject.newProperty = 'hi'
) -
Note : The object cannot be
Vue
an instance, orVue
the root data object of an instance.
2.2 Internal Principles
Remember when we introduced data change detection, we said that for object
type data, when we object
add a new pair to the data key/value
or delete a pair of existing ones key/value
, Vue
it is impossible to observe; and for Array
type data, when we When the data in the array is modified through the array subscript, Vue
it is also unobservable;
It is precisely because of this problem that these two methods Vue
are designed to solve this problem. Let's take a look at the internal implementation principle of the method first.set
delete
set
set
The definition of the method is located in the source code src/core/observer/index.js
, as follows:
export function set(target, key, val) {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${
(target: any)}`
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
It can be seen that the logic inside the method is not complicated, it just makes different processing according to different situations.
Firstly, judge whether the incoming type target
is undefined
or null
is the original type in a non-production environment, and if so, throw a warning, as follows:
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${
(target: any)}`
);
}
Then judge if the input target
is an array and the input key
is a valid index, then take the current array length and key
the maximum value of the two as the new length of the array, and then use the array method to add the value corresponding to splice
the input index into the array. Pay attention here, why use the method? Remember when we introduced the change detection method of type data, we said that the method of the array has been rewritten by the interceptor we created, that is to say, when the method is used to add an element to the array, the element will be automatically changed into responsive. as follows:key
val
splice
Array
splice
splice
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
If the input target
is not an array, it is treated as an object.
First, determine whether the incoming attribute key
already exists target
. If it exists, it indicates that this operation is not a new attribute, but a simple modification of the existing attribute value. Then only modify the attribute value, as follows:
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
The attribute obtained next target
, __ob__
as we said, whether the attribute is true
a sign of target
whether it is a responsive object, and then judges if it tragte
is Vue
an instance, or Vue
the root data object of an instance, then throws a warning and exits the program, as follows:
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
Then judge that if ob
the property is false
, then it target
is not a responsive object, then we only need to simply add new properties to it, without converting the new properties into responsive, as follows:
if (!ob) {
target[key] = val;
return val;
}
Finally, if target
it is an object and it is responsive, then call defineReactive
the method to add the new attribute value to target
it, defineReactive
then after the new attribute is added, it will be converted into a responsive type, and finally the dependency update will be notified, as follows:
defineReactive(ob.value, key, val);
ob.dep.notify();
The above is set
the internal principle of the method. Its logic flow chart is as follows:
3. vm.$delete
vm.$delete
is an aliasVue.delete
for the global and its usage is the same.
3.1 Usage review
Before introducing the internal principle of the method, let's review its usage based on the official documentation example.
vm.$delete(target, propertyName / index);
-
Parameters :
{Object | Array} target
{string | number} propertyName/index
Array + index usage is only supported in version 2.2.0+.
-
Usage :
Delete an attribute of an object. If the object is reactive, make sure deletion triggers an update of the view. This method is mainly used to avoid
Vue
the limitation of not being able to detect that the attribute is deleted, but you should rarely use it.Working with arrays is also supported in 2.2.0+.
-
Note : The target object cannot be an
Vue
instance orVue
the root data object of an instance.
3.2 Internal Principles
delete
The method is used to solve Vue
the limitation of not being able to detect that the attribute is deleted. The definition of this method is located in the source code src/core.observer/index.js
, as follows:
export function del(target, key) {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot delete reactive property on undefined, null, or primitive value: ${
(target: any)}`
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid deleting properties on a Vue instance or its root $data " +
"- just set it to null."
);
return;
}
if (!hasOwn(target, key)) {
return;
}
delete target[key];
if (!ob) {
return;
}
ob.dep.notify();
}
The internal principle of this method set
is somewhat similar to the method, and different treatments are made according to different situations.
target
First judge that if the input does not exist or target
is an original value in a non-production environment , a warning will be thrown, as follows:
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${
(target: any)}`
);
}
Then judge that if the input target
is an array and the input key
is a valid index, use the splice
method of the array to key
delete the value corresponding to the index. Why use splice
the method is also explained above, because splice
the method of the array has been created by us The interceptor is rewritten, so using this method will automatically notify the relevant dependencies. as follows:
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
If the input target
is not an array, it is treated as an object.
The attribute obtained next target
, __ob__
as we said, whether the attribute is true
a sign of target
whether it is a responsive object, and then judges if it tragte
is Vue
an instance, or Vue
the root data object of an instance, then throws a warning and exits the program, as follows:
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
Then judge whether the input key
exists target
in , if key
it does not exist target
in , then you don’t need to delete it, just exit the program directly, as follows:
if (!hasOwn(target, key)) {
return;
}
Finally, if target
it is an object, and the incoming one key
also exists in target
it, then delete the attribute target
from it , and at the same time judge target
whether the current one is a responsive object, if it is a responsive object, notify the dependency update; if not, delete it Then return directly without notification update, as follows:
delete target[key];
if (!ob) {
return;
}
ob.dep.notify();
The above is delete
the internal principle of the method.