vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]

时间:2023-03-09 14:47:15
vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]

其他章节请看:

vue 快速入门 系列

侦测数据的变化 - [vue api 原理]

前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会通知外界,因此 vue 提供了 vm.$set() 和 vm.$delete() 来解决这个问题。

vm.$watch() 方法赋予我们监听实例上数据变化的能力。

下面依次对这三个方法的使用以及原理进行介绍。

Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。

vm.$set

这是全局 Vue.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。

语法:

  • vm.$set( target, propertyName/index, value )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

以下是相关源码:

Vue.prototype.$set = set;

/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
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))));
}
// 如果 target 是数组,并且 key 是一个有效的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果传递的索引比数组长度的值大,则将其设置为 length
target.length = Math.max(target.length, key);
// 触发拦截器的行为,会自动将新增的 val 转为响应式
target.splice(key, 1, val);
return val
}
// 如果 key 已经存在,说明这个 key 已经被侦测了,直接修改即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
// 取得数据的 Observer 实例
var ob = (target).__ob__;
// 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“
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
}
// 如果数据没有 __ob__,说明不是响应式的,也就不需要做任何特殊处理
if (!ob) {
target[key] = val;
return val
}
// 通过 defineReactive$$1() 方法在响应式数据上新增一个属性,该方法会将新增属性
// 转成 getter/setter
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
} /**
* Check if val is a valid array index.
* 检查 val 是否是一个有效的数组索引
*/
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
}

vm.$delete

这是全局 Vue.delete 的别名。删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。你应该很少会使用它。

语法:

  • Vue.delete( target, propertyName/index )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index

实现思路与 vm.$set 类似。请看:

Vue.prototype.$delete = del;
/**
* Delete a property and trigger change if necessary.
* 删除属性,并在必要时触发更改。
*/
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))));
}
// 如果 target 是数组,并且 key 是一个有效的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 触发拦截器的行为
target.splice(key, 1);
return
}
// 取得数据的 Observer 实例
var ob = (target).__ob__;
// 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“
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
}
// key 不是 target 自身属性,直接返回
if (!hasOwn(target, key)) {
return
}
delete target[key];
// 不是响应式数据,终止程序
if (!ob) {
return
}
// 通知依赖
ob.dep.notify();
}

vm.$watch

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

语法:

  • vm.$watch( expOrFn, callback, [options] )

参数:

  • {string | Function} expOrFn
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate

返回值:

  • {Function} unwatch

例如:

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
}) // 函数
vm.$watch(
function () {
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)

相关源码请看:

Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
// 通过 Watcher() 来实现 vm.$watch 的基本功能
var watcher = new Watcher(vm, expOrFn, cb, options);
// 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
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();
}
}; /**
* Remove self from all dependencies' subscriber list.
* 取消观察。也就是从所有依赖(Dep)中把自己删除
*/
Watcher.prototype.teardown = function teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
// this.deps 中记录这收集了自己(Wtacher)的依赖
var i = this.deps.length;
while (i--) {
// 依赖中删除自己
this.deps[i].removeSub(this);
}
this.active = false;
}
};
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
// deep 监听对象内部值的变化
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$1; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
// 存储依赖(Dep)。Watcher 可以通过 deps 得知自己被哪些 Dep 收集了。
// 可用于取消观察
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '';
// parse expression for getter
// expOrFn可以是简单的键路径或函数。本质上都是读取数据的时候收集依赖,
// 所以函数可以同时监听多个数据的变化
// 函数: vm.$watch(() => {return this.a + this.b},...)
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
// 键路径: vm.$watch('a.b.c',...)
} else {
// 返回一个读取键路径(a.b.c)的函数
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== 'production' && warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
}; /**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
// 把自己入栈,读数据的时候就可以收集到自己
pushTarget(this);
var value;
var vm = this.vm;
try {
// 收集依赖
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// 对象内部的值发生变化,也需要通知依赖。
if (this.deep) {
// 把当前值的子值都触发一遍收集依赖的逻辑即可
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
} function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
// 不是数组和对象、已经被冻结,或者虚拟节点,直接返回
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
var depId = val.__ob__.dep.id;
// 拿到 val 的 dep.id,防止重复收集依赖
if (seen.has(depId)) {
return
}
seen.add(depId);
}
// 如果是数组,循环数组,将数组中的每一项递归调用 _traverse
if (isA) {
i = val.length;
while (i--) { _traverse(val[i], seen); }
} else {
keys = Object.keys(val);
i = keys.length;
// 重点来了:读取数据(val[keys[i]])触发收集依赖的逻辑
while (i--) { _traverse(val[keys[i]], seen); }
}
}

其他章节请看:

vue 快速入门 系列