1.vm.$set
vm.$set(target, key, value)
处理target是数组的情况
export 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;
}
//如果key在target中已经存在,则属于修改数据
if(key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
//处理新增的属性
const ob = target.__ob__
//target不能是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;
}
//还不是响应式数据,通过key和val在target上设置就行
if(!ob) {
target[key] = val;
return val;
}
//用户在响应式数据上新增了一个属性,用defineReactive将新增属性转化为响应式的
defineReactive(ob.value, key, val)
//最后,向target的依赖发送变化通知,并返回val
ob.dep.notify()
return val
通过splice方法改变数组可以被数组拦截器侦测到,并且把新增的值变为响应式的。
2.vm.$delete
vm.$delete(target, key)
export function del (target, key) {
//如果是一个数组,使用splice方法,数组拦截器会自动向依赖发送通知
if(Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return
}
const ob = (target).__ob__
//与增加属性一样,target不能是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
}
//如果key不是target自身的属性,则终止程序继续执行
delete target[key]
//最后判断target是不是响应式数据,非响应式数据只需要执行删除操作即可
if(!ob) {
return
}
ob.dep.notify()
}
3. vm.$watch
用法: vm.$watch(expOrFn, callback, [options])
vm.$watch返回一个取消观察函数,用来停止触发回调
vm.$watch是对Watcher的一种封装。通过watcher完全可以实现vm.$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()
}
}
当watcher是函数的时候,不止可以动态返回数据,其中读取的所有的所有数据也都会被Watcher观察。也就是说,函数中读取vue实例上的任何数据,watcher都会观察。针对expOrfn,需要进行一次判断
if(typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
执行new Watcher之后,会判断用户是否使用了immediate参数,如果使用了,则立即执行一次cb。而unwatch就是将watcher实例从当前正在观察的状态的依赖列表中删除。
watcher中的teardown方法,现在要在watcher中添加该方法来实现unwatch的功能。首先需要Watcher中记录自己都订阅了谁,也就是watcher实例被收集进了哪些Dep里。然后当Watcher不想继续订阅这些Dep的时候,循环自己记录的订阅列表来通知它们将自己从它们的Dep的依赖列表中移除掉。
在watcher中,我们需要新增一个addDep方法,该方法的作用是在Watcher中记录自己都订阅过哪些Dep。
this.deps = [];
this.depIds = new Set();
//我们用depIds来判断如果当前Watcher是否订阅了Dep,保证不会重复订阅,depIds中存放的就是当前Watcher订阅了哪些Dep,deps是对应的Dep,addSub将Watcher加入到dep的依赖列表中
addDep(dep) {
const id = dep.id
if(!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
当Dep数据发生变化的时候,会通知对应的Watcher,Watcher和Dep是多对多的关系。
teardown方法的实现
teardown() {
let i = this.deps.length;
while(i--) {
this.deps[i].removeSub(this)
}
}
removeSub() {
const index = this.subs.indexOf(sub)
if(index > -1) {
return this.subs.splice(index,1)
}
}
deep参数的实现
if(options) {
this.deep = !!options.deep
} else {
this.deep = false;
}
//有options参数,则把options.deep转换成布尔值,否则deep置为false
在get方法中收集依赖的时候,deep收集子集依赖的时候必须在window.target = undefined之前,之后收集target就已经被改变了。deep因为要遍历所有的value子值,所以需要递归
//收集依赖的id
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
}
//有__ob__属性,已经是一个响应式的对象,将DepId加入seen
if(val.__ob__) {
const depId = val.__ob__.dep.id
if(seen.has(depId)) {
return
}
seen.add(depId)
}
//对于对象和数组进行不同的遍历,对象进行读取的时候会触发getter
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)
}
}