vue变化相关API实现原理(vm.$watch, vm.$set,vm.$delete)

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)    
    }
}    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值