【深入浅出Vue.js篇】之变化侦测相关API实现原理

本文详细探讨了Vue.js中变化侦测的相关API,包括$watch的内部实现,如deep和immediate参数的工作原理,以及$set和$delete的内部逻辑。通过分析源码,揭示了Vue.js如何确保响应式数据的正确更新和视图同步。
摘要由CSDN通过智能技术生成

【深入浅出Vue.js篇】之变化侦测相关API实现原理



一、watch 的内部原理

回顾一下

/* 参数

  • { string | Function} expOrFn
  • { Function | Object} callback
  • { Object } [options]
  • *{boolean} deep 发现对象内部值的变化,监听数组变化时不需要设置
  • *{boolean} immediate 立即以表达式的当前值触发回调
    */

vm.$watch(expOrFn, callback, [options])

用法: 用于观察一个表达式活computed函数在Vue.js实例上的变化。回调函数调用时,会从参数得到新数据(new Value) //和旧数据(old Value).表达式只接受以点分隔的路径,如a.b.c.如果是一个比较复杂的表达式,可以用函数代替表达式。

vm.$watch(‘a.b.c’, function (newVal, oldVal) {
// 做点什么
})

1.watch 的内部原理

代码如下(示例):

Vue.prototype.$watch = function (expOrFn, cb, options) {
  const vm = this
  options = options || {}
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判断用户是否使用了immediate 参数,如果使用了,则立即执行一次cb 
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  // 取消观察数据
  return function unwatchFn () {
    // 执行了watcher.teardown() 来取消观察数据
    watcher.teardown()
  }
}

//Watcher 想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知Watcher
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.deps = []
    this.depIds = new Set()
    this.getter = parsePath(expOrFn)
    // // expOrFn参数支持函数
    // if (typeof expOrFn === 'function') {
    //   this.getter = expOrFn
    // } else {
    //   this.getter = parsePath(expOrFn)
    // }
    this.cb = cb
    this.value = this.get()
  }
  get () {
    window.target = this
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
  // 记录了自己会被哪些Dep 通知
  addDep (dep) {
    const id = dep.id
    // 如果当前Watcher 已经订阅了该Dep ,则不会重复订阅
    if (!this.depIds.has(id)) {
      // 记录当前Watcher 已经订阅了这个Dep 。
      this.depIds.add(id)
      // 记录自己都订阅了哪些Dep
      this.deps.push(dep)
      // 将自己订阅到Dep 中
      dep.addSub(this)
    }
  }
   /**
   * 从所有依赖项的Dep列表中将自己一处
   */
  teardown () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
  }
}

let uid = 0
// Dep 会记录数据发生变化时,需要通知哪些Watcher
class Dep{
  constructor() {
    this.id = uid++
    this.subs = []
  }
  //...这里省略了部分代码 可以去 Object侦测原理篇 查看完成代码
  depend () {
    if (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)
    }
  }
}

2.deep参数的实现原理

要想实现deep 的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前Watcher 了。

代码如下(示例):

class Watcher{
  constructor(vm, expOrFn, ob, 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 = this.cb
    this.value = this.get()
  }
  get () {
    window.target = this
    let value = this.getter.call(vm, vm)
    if (this.deep) {
      // 处理deep的逻辑
      traverse(value)
    }
    window.target = undefined
    return value
  }
  // 省略部分代码
}

// 递归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)
  // 如果不是Array 和Object ,或者已经被冻结,那么直接返回,什么都不干。
  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
    return
  }
  if (val.__ob__) {
    // 拿到val的dep.id
    const depId = val.__ob__.dep.id
    // 用id来保证不会重复收集依赖
    if (seen.has(depId)) {
      return
    }

    seen.add(depId)
  }

  if (isA) {
    i = val.length
    // 递归调用_traverse
    while (i--) {
      _traverse(val[i],seen)
    }
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) {
      // 循环Object中的左右key,然后执行一次读取操作 
      //val[keys[i]]会触发收集依赖的操作
      _traverse(val[keys[i]],seen)
    }
  }
}

二、vm.$set 的内部原理

回顾一下

/**
 * 参数
 * {objdct | Array} target
 * {string | number} key
 * {any} value
 */
vm.set(target, key, values)
  • 用法
  • 在objet上设置一个属性,如果object是响应式的,Vue.js会保证属性被创建后也是响应式的
  • 并且触发试图更新

vm.$set的具体实现其实就是在observer中抛出set方法

代码如下(示例):

function set (target, key, val) {
  // 如果target是数组且key是一个有效的索引值
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 通过splice方法把val设置到target中的指定位置
    //当使用splice方法把val设置到target中的时候,数组拦截器会侦测到target发生了变化
    //接下来就会将新增的val转换成响应式的
    target.splice(key, 1, val)
    return val
  }

  // key已经存在于target中 说明这个key已经被侦测了变化
  //直接用key和val修改数据即可
  //修改数据的动作会被Vue.js侦测到
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

  // 获取target的__ ob __属性
  const ob = target.__ob__
  // target不能是Vue.js实例或Vue.js实例的根数据对象
  // 使用target.__isVue判断是否为Vue.js实例,使用ob.vmCount判断是否为根数据对象(this.$data就是根函数)
  if (target.__isVue || (ob && ob.vmCount)) {
    process.env.NODE_EVN !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance pr root $data' +
      'at runtime - declare it upfornt in this data option.'
    )
      return val
  }

  // target不是响应式
  if (!ob) {
    target[key] = val
    return val
  }
  // 新增属性 需追踪该属性的变化 
  //使用defineReactive将新增属性转换成getter/setter形式
  defineReactive(ob.value, key, val)
  // 触发变化通知
  ob.dep.notify()
  return val
}

三、vm.$delete 的内部原理

回顾一下

  • 参数
  • {Object | Array} target
  • {string | number} key/index
vm.set(target, key, values)
  • 用法
  • 删除对象的属性。如果对象是响应式的,确保删除能触发视图更新

代码如下(示例):

function del (target, key) {
  // 如果target是数组且key是一个有效的索引值
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 使用splice 方法 触发拦截器向依赖发送通知
    target.splice(key, 1)
    return
  }
  // 获取target的__ ob __属性
  const ob = target.__ob__
  // target不能是Vue.js实例或Vue.js实例的根数据对象
  // 使用target.__isVue判断是否为Vue.js实例,ob.vmCount 数量大于1
  if (target.__isVue || (ob && ob.vmCount)) {
    process.env.NODE_EVN !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
      return
  }

  // 如果key不是target自身的属性,则终止程序继续执行
  if (!hasOwn(target, key)) {
    return
  }

  delete target[key]

  // 如果数据是非响应式的 终止程序继续执行
  if (!ob) {
    return  
  }

  ob.dep.notify()
}

总结

文章中,我们详细介绍了变化侦测相关API 的内部实现原理。

先介绍了vm.$watch 的内部实现及其相关参数的实现原理,包括deep 、immediate 和unwatch 。

随后介绍了vm.$set 的内部实现。这里介绍了几种情况,分别为Array 的处理逻辑,key 已经存在的处理逻辑,以及最重要的新增属性的处理逻辑。

最后,介绍了vm.$delete 的内部实现原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值