乐趣区

Vue侦测相关api

vm.$watch

用法:vm.$watch(expOrFn, callback, [options] ),返回值为 unwatch 是一个函数用来取消观察;下面主要理解 options 中的两个参数 deepimmediate以及unwatch

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

immediate

从上面代码中可以看出当 immediatetrue时,就会直接进行执行回调函数

unwatch

实现方式是:

  1. 将被访问到的数据 dep 收集到 watchs 实例对象上,通过 this.deps 存起来
  2. 将被访问到的数据 dep.id 收集到 watchs 实例对象上,通过 this.depIds 存起来
  3. 最后通过 watchs 实例对象的 teardown 进行删除
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             // 参数 dep 是 Dep 实例对象
        if(!this.depIds.has(id)) {    // 判断是否存在避免重复添加
            this.depIds.add(id)       
            this.deps.push(dep)
            dep.addSub(this)         //this 是依赖
        }
    }
    teardown () {
        let i = this.deps.length
        while (i--) {this.deps[i].removeSub(this)
        }
    }
}
let uid = 0
class Dep {constructor () {
        this.id = uid++
        ...
    }
    ...
    depend () {if(window.target) {window.target.addDep(this)    // 将 this 即当前 dep 对象加入到 watcher 对象上
        }
    }
    removeSub (sub) {const index = this.subs.indexOf(sub)
        if(index > -1) {return this.subs.splice(index, 1)
        }
    }
}

分析

当执行 teardown() 时需要循环;因为例如expOrFn = function () { return this.name + this.age},这时会有两个dep 分别是 nameage分别都加入了 watcher 依赖 (this),都会加入到this.deps 中,所以需要循环将含有依赖的 dep 都删除其依赖

deep

需要明白的是

  1. deep干啥用的,例如 data = {arr: [1, 2, {b: 6]},当我们只是监听data.arr 时,在 [1, 2, {b: 66}] 这个数值内部发生变化时,也需要触发,即b = 888

怎么做呢?

class Watcher {constructor (vm, expOrFn, cb, options) {
        this.vm = vm
        this.deps = []
        this.depIds = new Set()
        if(typeof expOrFn === 'function') {this.getter = expOrFn}else {this.getter = parsePath(expOrFn)
        }
        if(options) {                    // 取值
            this.deep = !!options.deep
        }else {this.deep = false}
        this.cb = cb
        this.value = this.get()}
    get () {
        window.target = this
        let value = this.getter.call(vm, vm)
        if(this.deep) {traverse(value)
        }
        window.target = undefined
        return value
    }
    ...
}
const seenObjects = new Set()
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)) {  // 判断 val 是否是对象或者数组以及是否被冻结
        return
    }
    if(val._ob_) {const depId = val._ob_.dep.id     // 可以看前面一篇我们对 Observer 类添加了 this.dep = new Dep(),所以能访问其 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)
        i = keys.length
        while (i--) _traverse(val[i], seen)
    }
}

分析

  1. window.target = this,寄存依赖
  2. let value = this.getter.call(vm, vm) 访问当前 val,并执行get

dep.depend(),如果发现val 为数组,则将依赖加入到 observerdep中,也就实现了对当前数组的拦截

  1. traverse(value) 也就是执行 _traverse(val, seenObjects);核心就是对被Observerval通过 val[i] 通过这种操作,间接触发 get,将依赖添加到当前数值的dep 中,这样也就实现了,当内部数据发生变化,也会循环 subs 执行依赖的 update,从而触发回调;当是数组时,只需进行遍历,看内部是否有Object 对象即可,因为在第二步的时候,会对 val 进行判断是否是数组,变改变七个方法的 value,在遍历;所以这边只要是内部数组都会进行拦截操作,添加依赖,即对象 {} 这种没没添加依赖。
  2. seenObjects.clear()当内部所以类型数据都添加好其依赖后,就清空。
  3. window.target = undefined消除依赖

vm.$set

用法:vm.$set(target, key, value)

作用

  1. 对于数组,进行 set 则是添加新元素,并需要触发依赖更新
  2. 对于对象,如果 key 值存在,则是修改value;不存在,则是添加新元素,需新元素要进行响应式处理,以及触发更新
  3. 对于对象本身不是响应式,则直接添加key-value,无需处理
Vue.prototype.$set = function (target, key, val) {if(Array.isArray(target) && isValidArrayIndex(key)) {    // 是数组并且 key 有效
        target.length = Math.max(target.length, key)   // 处理 key > target.length
        target.splice(key, 1, val)   // 添加新元素,并输出依赖更新同时新元素也会进行 `Obsever` 处理
        return val
   }
   if(key in targert && !(key in Object.prototype) {  // 能遍历并且是自身 key
        target[key] = val   // 触发 set,执行依赖更新
        return val
   }
   const ob = target._ob_
   if(target.isVue || (ob && ob.vm.Count) {// 不是 vue 实例也不是 vue 实例的根对象(即不是 this.$data 跟对象)
       // 触发警告
       return
   }
   if(!ob) {    // 只添加
       target[key] = val
       return val
   }
   defineReactive(ob.value, key, val)  // 进行响应式处理
   ob.dep.notify()  // 触发依赖更新
   returnv val
}

vm.$delete

用法:vm.$delete(target, key)

作用

  1. 对于数组,进行 delete 则是删除新元素,并需要触发依赖更新
  2. 对于对象,如果 key 值不存在,直接return,存在,删除元素,
  3. 对于对象本身不是响应式,则只删除key-value,无需其他处理
Vue.prototype.$delete = function (target, key) {if(Array.isArray(target) && isValidArrayIndex(key)) {target.splice(key, 1)
        return
    }
    const ob = target._ob_
    if(target.isVue || (ob && ob.vm.Count) {// 不是 vue 实例也不是 vue 实例的根对象(即不是 this.$data 跟对象)
        // 触发警告
        return
   }
   if(!hasOwn(target, key)) {return}
    delete target[key]
    if(!ob) {return}
    ob.dep.notify()}

掘金地址

退出移动版