# vm.$watch
先回顾下vm.$watch的用法
vm.$watch( expOrFn, callback, [options] )
参数:
- {string | Function} expOrFn
- {Function | Object} callback
- {Object} [options]
- {boolean} deep 为了发现对象内部值的变化,可以在选项参数中指定deep: true,监听数组的变动不需要这么做
- {boolean} immediate 在选项参数中指定immediate: true,将立即以表达式的当前值触发回调
返回值:
{Function} unwatch
用法:
用于观察一个表达式或computed函数在Vue.js实例上的变化。回调函数调用时,会从参数得到新数据(new value)和旧数据(old value)。表达式只接受以点分隔的路径,例如a.b.c。如果是一个比较复杂的表达式,可以用函数代替表达式。
# watch的内部原理
下面是$watch的核心代码
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this
options = options || {}
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
上述代码
- 调用了new Watcher来实现$watcher的重要功能
- 当immediate为真的时候立即执行回调函数
- 返回了一个unwatchFn方法,用于取消观察数据
由于expOrFn可能是函数,我们需要对Watcher类进行修改,以便支持expOrFn为函数的情况。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// expOrFn参数支持函数,当expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有Vue.js实例上的响应式数据
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb
this.value = this.get()
}
……
}
unwatchFn方法取消数据观察实际执行的是watcher.teardown()方法,本质是把watcher实例从当前正在观察的状态的依赖列表中移除。因此就需要Watcher知道都被哪些Dep收集了
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 存放watcher都记录了哪些Dep
this.deps = []
// depIds用来判断当前Watcher是否重复订阅Dep
this.depIds = new Set() // 新增
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
……
addDep (dep) {
const id = dep.id
// 当依赖发生变化时,会通知Watcher重新读取最新的数据。如果没有这个判断,就会发现每当数据发生了变化,Watcher都会读取最新的数据。而读数据就会再次收集依赖,这就会导致Dep中的依赖有重复。这样当数据发生变化时,会同时通知多个Watcher。为了避免这个问题,只有第一次触发getter的时候才会收集依赖。
if (!this.depIds.has(id)) {
this.depIds.add(id)
// watcher记录了哪些Dep
this.deps.push(dep)
// 将watcher记录到Dep中
dep.addSub(this)
}
}
/**
* 从所有依赖项的Dep列表中将自己移除
*/
teardown () {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
}
……
}
Dep中的修改
// 用于区分不同的Dep
let uid = 0
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
……
depend () {
if (window.target) {
// this.addSub(window.target)
window.target.addDep(this)
}
}
// 把Watcher从sub中删除掉,当数据方法变化的时候不再通知这个watcher
removeSub (sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
return this.subs.splice(index, 1)
}
}
……
}
从上面的代码中可以看出我们存放watcher和dep都是用的数组,dep和watcher是多对多的关系,为什么是多对多的关系呢?
如下,当expOrFn是函数的时候,并且在函数内部使用了多个数据,Watcher就需要收集多个Dep,同时每个Dep也要收集Watcher,这样当任意一个数据变化都会通知Watcher。
this.$watch(function () {
return this.name + this.age
}, function (newValue, oldValue) {
console.log(newValue, oldValue)
})
# deep参数的实现原理
vm.$watch中的可选参数deep为了发现对象内部值的变化。实现deep的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。
export default class Watcher {
constructor (vm, expOrFn, cb, options) {
this.vm = vm
// 新增
if (options) {
// 如果用户设置了deep属性
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(vm, vm)
// 如果deep为真
if (this.deep) {
// 递归value的所有子值来触发它们收集依赖的功能
traverse(value)
}
window.target = undefined
return value
}
……
}
如果用户使用了deep参数,则在window.target = undefined之前调用traverse来处理deep的逻辑。
const seenObjects = new Set()
export function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
// _traverse函数一个递归操作,所以这个value的子值也会触发同样的逻辑,这样就可以实现通过deep参数来监听所有子值的变化。
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
// val不是Array和Object,或者已经被冻结,那么直接返回
if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
return
}
if (val.__ob__) {
// 用这个id保证不重复收集依赖
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
// 数组中的每一项递归调用 _traverse
while (i--) _traverse(val[i], seen)
} else {
// 循环Object中的所有key,然后执行一次读取操作,再递归子值
keys = Object.keys(val)
i = keys.length
// val[keys[i]] 会触发getter,触发收集依赖的操作
while (i--) _traverse(val[keys[i]], seen)
}
}
# vm.$set
基本用法:
vm.$set( target, key, value )
参数:
- {Object | Array} target target不能是Vue.js实例或者Vue.js实例的根数据对象。
- {string | number} key
- {any} value
返回值:{Function} unwatch
用法:在object上设置一个属性,如果object是响应式的,Vue.js会保证属性被创建后也是响应式的,并且触发视图更新。这个方法主要用来避开Vue.js不能侦测属性被添加的限制。
我们来看下vm.$set的核心实现原理
import { set } from '../observer/index'
Vue.prototype.$set = set
export function set (target, key, val) {
// 如果target是数组且key是有效的数组索引值,设置length属性,然后通过splice方法将val设置到target中的指定位置把这个新增的val转成响应式的。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 数组拦截器会侦测到target发生了变化,将val转为响应式的
target.splice(key, 1, val)
return val
}
// 如果key已经存在target中,这个key已经被侦测到了,实际相当于修改数据,直接改就好。
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = target.__ob__
// target._isVue来判断target是不是Vue.js实例
// ob.vmCount来判断它是不是根数据对象即可,this.$data就是根数据
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
}
// 如果target身上没有 __ob__ 属性,说明不是响应式数据,直接设置值即可
if (!ob) {
target[key] = val
return val
}
// 如果是响应式数据直接调用defineReactive将新增属性转为getter和setter
defineReactive(ob.value, key, val)
// 向target依赖触发变化通知
ob.dep.notify()
return val
}
# vm.$delete
用法:
vm.$delete( target, key )
参数:
- {Object | Array} target 目标对象不能是Vue.js实例或Vue.js实例的根数据对象。
- {string | number} key/index
仅在2.2.0+ 版本中支持Array+index的用法。
用法:删除对象的属性。如果对象是响应式的,需要确保删除能触发更新视图。这个方法主要用于避开Vue.js不能检测到属性被删除的限制,但是你应该很少会使用它。
我们来看下vm.$delete的核心实现原理
import { del } from '../observer/index'
Vue.prototype.$delete = del
export function del (target, key) {
// 如果是数组直接调用splice删除索引值即可,数组拦截器中会侦测到target的变化
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = target.__ob__
// 如果目标对象是Vue.js实例或Vue.js实例的根数据对象直接返回
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
}
// 如果taget上不存在这个key属性,也就谈不上删除,直接return
if (!hasOwn(target, key)) {
return
}
delete target[key]
// 如果__ob__不存在,说明数据不是响应式的,不用发送通知,直接返回
if (!ob) {
return
}
ob.dep.notify()
}
# 总结
我们先介绍了vm.$watch的内部实现及其相关参数的实现原理,包括deep、immediate和unwatch。
随后介绍了vm.$set的内部实现。这里介绍了几种情况,分别为Array的处理逻辑,key已经存在的处理逻辑,以及最重要的新增属性的处理逻辑。
最后,介绍了vm.$delete的内部实现原理。
← Array的变化侦测 虚拟DOM简介 →