这是我参与更文挑战的第4天,活动详情查看: 更文挑战。
前言
因为 Object 和 Array 的变化侦测有一些缺陷,所以 Vue.js 又提供了 $set
和 $delete
方法。本文,我们将深入学习 $watch
、$set
和 $delete
的实现原理。
$watch
用法
观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。
vm.$watch(expOrFn, callback, [options]);
vm.$watch('a.b.c',function(newVal, oldVal){
//...
});
选项:deep
为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, {
deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
选项:immediate
在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
vm.$watch('a', callback, {
immediate: true
})
// 立即以 `a` 的当前值触发回调
实现原理
vm.$watch 实际是对 Watcher 的一种封装。
Vue.prototype.$watch = function(expOrFn, cb, options){
const vm = this;
options = options || {};
const watcher = new Watcher(vm, expOrFn, cn, options);
if (options.immediate){
cb.call(vm, watcher.value)
}
return function unwatchFn() {
watcher.teardown();
}
}
expOrFn 我们之前见过,它可以是一个形如 'a.b.c' 的 keyPath,也可以是个函数。执行 $watch 的最后会返回一个 unwatch 函数,内部会调用 watcher.teardown()
。
这个方法是取消监听,之前没有实现过。实际上取消监听,就是从 dep 中移除该 watcher。那么我们要如何实现呢?
export default class Watcher {
constructor(vm, expOrFn, cb){
this.vm = vm;
this.deps = []; // 新增
this.depIds = new Set(); // 新增
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb;
this.value = this.get();
}
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)){
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
}
我们在 watcher 中新增 deps 列表,用来记录哪些 dep 收集了该 watcher。我们使用 depIds 来判断如果当前 Watcher 已经订阅了该 Dep,则不会重复订阅。直接 this.ddepIds.add 记录当前 Watcher 已经订阅了 Dep。
Watcher 新增了 addDep 方法后,Dep 中收集依赖的逻辑也需要有所改变:
let uid = 0;
export default class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
depend() {
if (window.target) {
window.target.addDep(this);
}
}
}
Dep 记录了需要通知哪些 Watcher,同时 Watcher 中也记录自己会被哪些 Dep 通知。Watcher 和 Dep,它们是多对多的关系。
为什么是多对多的关系?
如果 watcher 中的 expOrFn 参数是一个表达式,那么就只会记录一个 Dep。但如果 expOrFn 是一个函数,函数内部使用了多个数据,那么 Watcher 中就会记录多个 Dep。
this.$watch(function(){
return this.surname + this.firstName
})
现在,我们可以来实现 teardown
:
teardown(){
let i = this.deps.length;
while(i--){
this.deps[i].removeSub(this)
}
}
deep 的实现原理
export default class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
if (options) {
this.deep = !!options.deep;
} else {
this.deep = false;
}
this.deps = [];
this.depIds = new Set();
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
let value = this.getter.call(this.vm, this.vm);
if (this.deep) {
traverse(value);
}
window.target = undefined;
return value;
}
}
我们需要在 window.target = undefined
之前,使用 traverse
递归 value 的所有子值来触发它们收集依赖功能。
const seenObjects = new Set();
export function traverse(val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse(val, seen) {
let i, keys;
const isA = Array.isArray(val);
if ((!isA && isObject(val)) || Object.isFrozen(val)) {
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);
while (i--) {
__traverse(val[keys[i]], seen);
}
}
}
$set
用法
vm.$set(target, key, value);
之前我们已经学过,只有已经存在的属性的变化会被追踪到,新增的属性无法被追踪到。
vm.$set 就是为了解决这个问题而出现的。它可以将新增的属性也转换成响应式的。
实现原理
Array 的处理
function set(target, key, val) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
}
Object 的处理
1. key 已经存在
function set(target, key, val) {
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;
}
}
2. key 是新增的
function set(target, key, val) {
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.__ob__;
if (target.__isVue ||(ob && ob.vmCount)) return val;
if(!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
首先,我们先尝试获取 value 的 __ob__
属性。
如果不存在,就是普通对象,直接赋值返回即可。如果存在,则表明这是响应式的,需要调用 defineReative
将新增属性转换成 getter/setter
的形式。
最后,向 target 的依赖进行通知。
$delete
用法
vm.$delete(target, key)
实现原理
Array 的处理
export function del(target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return val;
}
}
Object 的处理
1. key 存在
export function del(target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return val;
}
// 新增
const ob = target.__ob__;
if (target.__isVue ||(ob && ob.vmCount)) return;
delete target[key];
ob.dep.notify();
}
2. key 不存在
export function del(target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return val;
}
const ob = target.__ob__;
// 新增
if (!hasOwn(target.key)) {
return;
}
delete target[key];
ob.dep.notify();
}
3. 不是响应式对象
export function del(target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return val;
}
const ob = target.__ob__;
if (!hasOwn(target.key)) {
return;
}
delete target[key];
// 新增
if (!ob) return;
ob.dep.notify();
}
总结
$watch
/$set
/$delete
是如何实现的
$watch
是对 Watcher 调用形式的一种封装。同时,我们还知道了 Watcher 和 Dep 是多对多的关系。
$set
和 $delete
类似,都要对 Array 和 Object 做不同的处理,处理完之后都是通过 value.__ob__.dep.notify()
来完成对依赖的通知。