$watch、$set、$delete原理解析

源码位置:src > core > observer > index.js

$watch

$watch 其实就是对 Watcher 的一种封装

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options) // 调用 new Watcher来实现 vm.$watch的基本功能
    if (options.immediate) { // 判断是否使用了 immediate参数,如果使用了,则立即执行一次 cb
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () { // 取消观察数据
      watcher.teardown() // 其本质是把watcher实例从当前正在观察的状态的依赖列表中移除
    }
  }
复制代码

watch 的内部原理

Wathcer 中记录自己都订阅了谁,也就是 watcher 实例被收集进了哪些 Dep 里,然后当 Watcher不想继续订阅这些 Dep 时,循环自己记录的订阅列表来通知它们(Dep)将自己从它们(Dep)的依赖列表中移除掉

export default class Watcher {
    constructor (vm, expOrFn, cb) {
        this.vm = vm
        this.deps = []
        this.depIds = new Set()
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    ......
    addDep (dep) {
        const id = dep.id
        if (!this.depIds.has(id)) { // 通过 depIds 来判断当前 Watcher 已经订阅了该 Dep,则不会重复订阅
            this.depIds.add(id) // 记录当前 Watcher 已经订阅了这个 Dep
            this.deps.push(dep) // 记录自己都订阅了哪些 Dep
            dep.addSub(this) // 将自己订阅到 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 中新增 teardown 方法来通知这些订阅的 Dep,让它们把自己从依赖列表中移除掉

/**
* 从所有依赖项的 Dep 列表中将自己移除
*/
teardown () {
    let i = this.deps.length
    while (i--) { // 循环Deps列表,分别执行 removeSub 方法将自己移除
        this.deps[i].removeSub(this)
    }
}
复制代码
export default class Dep {
    ......
    
    removeSub (sub) {
        const index = this.subs.indexOf(sub)
        if (index > -1) {
            return this.subs.splice(index, 1)
        }
    }
}

复制代码

unwatche 的原理:循环 Watcher 的订阅列表,分别执行 removeSub 方法,在该方法中将自己从 subs(订阅列表) 中删除

deep 参数的实现原理

想要实现 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(vm, vm)
        
        // 新增代码
        if (this.deep) {
            traverse(value) // deep 核心方法 ,在window.target = undefined 之前调用
        }
        window.target = undefined
        return value
    }
}
复制代码

接下来,要递归 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)
    // 如果是非数组、非对象、或者是冻结的对象直接return
    if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
        return
    }
    
    if (val.__ob__) { // 拿到dep.id,用这个id来保证不会重复收集依赖
        const depId = val.__ob__.dep.id
        if (seen.has(depId)) {
            return
        }
        seen.add(depId)
    }
    
    if (isA) { // 如果是数组,则循环数组,将数组每一项递归调用 _traverse
        i = val.length
        while(i--) _traverse(val[i], seen)
    } else { // 如果是Object,则循环Object中所有key,然后执行一次读取操作,再递归子值
        keys = Object.keys(val)
        i = keys.length
        while(i--) _traverse(val[keys[i]], seen)
    }
}
复制代码

$set

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

  /**
   * 如果 target 是数组,并且 key 是个有效的索引,就先设置 length 属性
   * 如果传递的索引 Key  大于 target.length 那么 target.length 就等于传入的索引
   * 使用 splice 把传入的 val 设置到 target 指定的位置(传入的索引的位置)
   * 当我们使用 splice 把 val 设置到target中时,数组拦截器会侦测到 target 发生了变化,会自动帮我们把 val 转化为响应式,
   * 最后返回 val
   */
  if (Array.isArray(target) && isValidArrayIndex(key)) { // target为数组时
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  
  /**
  * 如果 key 存在与 target中说明这个值已经被侦测了变化
  * 这种情况直接用 key 和 val 修改数据就行了,修改后的数据变化会被vue侦测到
  */
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  
  
  const ob = (target: any).__ob__ // 获取 target 的 __ob__ 属性
  if (target._isVue || (ob && ob.vmCount)) { // 避免向 Vue 实例或者$data根数据对象上使用 $set
    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
  }
  if (!ob) { // 如果 target 不是响应式的话,只需要使用 key 和 val 设置新值就行了
    target[key] = val
    return val
  }
  // 如果 target 是响应式的话,这种情况需要追踪这个新增属性的变化,即使用 defineReactive 将新增属性转换成 getter/setter的形式即可
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
复制代码

$delete

删除属性后自动向依赖发送消息,通知 Watcher 数据发生了变化

import { del } from '../observer/index'
Vue.prototype.$delete = del
复制代码

上面代码中在 Vue的原型对象上挂载$delete方法

export function del (target, key) {
    const ob = target.__ob__
    delete target[key] // 先从target中删除key,然后向依赖发送消息
    ob.dep.notify()
}

复制代码

接下来,处理数组的情况

export function del (target, key) {
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key ,1)
        return
    }
    const ob = target.__ob__
    delete target[key]
    ob.dep.notify()
}

复制代码

处理数组的情况,使用 splice 将指定位置的元素删除即可,使用了 splice 方法,无需再调用 notify() 向依赖发送消息,因为数组拦截器会自动向依赖发送通知

与vm.set一样,vm.set一样,vm.delete也不可以在Vue实例或者根数据对象上使用

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享