Vue 源码剖析 —— 变化侦测相关 API 实现原理

栏目: JavaScript · 发布时间: 5年前

内容简介:vm.$watch(expOrFn, callback, [options])由于前面还提到,

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

  • 返回值: unwatch { Function }
  • 用法:用于观察一个表达式或 computed 函数在 Vue.js 实例上的变化,同时给回调函数传入新数据和旧数据作为参数。
  • options参数: {deep, immediate} ,其中 deep 指定是否观察对象内部值的变化, immediate 指定是否立即执行回调函数

实现原理

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 unwatch() {
    watcher.teardown()
  }
}
复制代码

由于 $watch 函数的第一个参数 expOrFn 可以是函数也可以是字符串表达式,所以这里需要改写一下 Watcher 类:

class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    // 新增
    if (typeof expOrFn === 'function') {
    // 为函数时,不止可以返回动态数据,其中所有访问到的数据也都会被 Watcher 观察
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.cb = cb
    this.value = this.get()
  }
  ...
}
复制代码

前面还提到, vm.$watch 函数最后返回的是一个 unwatch 函数,顾名思义,它的作用是取消观察函数。执行 unwatch 函数其实就是执行当前 Watcher 实例的 teardown 函数。目前我们并没有在 Watcher 内部记录订阅的 Dep ,所以又要进行一次改写:

class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.deps = [] // 新增
    this.depIds = new Set() // 新增
    if (typeof expOrFn === 'function') {
    // 为函数时,不止可以返回动态数据,其中所有访问到的数据也都会被 Watcher 观察
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.cb = cb
    this.value = this.get()
  }
  ...
  addDep (dep) {
    const id = dep.id
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}
// 给 Dep 加上 id
let uid = 0
class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    // 新增
    if(somethingToWatch) {
      somethingToWatch.addDep(this)
    }
    //this.subs.push(sub)
  }
  ...
}

复制代码

到现在,不仅是 Dep 会记录数据发生变化时,需要通知哪些 Watcher ,而 Watcher 中也同样记录了自己会被那些 Dep 通知,也就是说,这是一个多对多的关系。添加了 deps 属性后, teardown 函数的实现就很简单了,这里就省略不写。

最后,可以看下 deep 参数的实现。当 deeptrue 时,需要监听当前对象的所有子值。实现原理和 Watcher 监听某个值的过程类似,通过访问这个数据,触发当前数据收集依赖的逻辑,把自己收集进去,故可以通过递归遍历当前要监听对象的所有子值来实现。

vm.$set

API 用法

vm.$set(target, key, value)

  • 用法: 在 target 中添加属性 key , 如果 target 是响应式的,那么新增的 key 值也是响应式的。

对于数组的处理

数组的处理比较直观,可以直接利用包裹好的 splice 函数实现:

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
  }
}
复制代码

对于对象的处理

function set(target, key, val) {
  ...
  // 新增
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

  const ob = target.__ob__
  // _isVue 标记是否是 Vue 实例, ob.vmCount 标记是否是根数据对象
  if (target._isVue || (ob && ob.vmCount)) {
    // 报错
    ...
    return val
  }

  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
复制代码

vm.$delete

API 用法

vm.$delete(target, key)

由于在 Vue.js 中,删除某个属性无法自动向依赖发送通知,所以包装了一个 vm.$delete 避免出现下面这种比较丑陋的代码 =. =:

delete this.obj.name
this.obj.__ob__.dep.notify()
复制代码

下面是 vm.$delete 的实现:

function delete(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = target.__ob__
  if (!hasOwn(target, key)) return
  delete ob[key]
  if (!ob) return
  ob.dep.notify()
}
复制代码

本系列文章均是深入浅出 Vue.js的学习笔记,有兴趣的小伙伴可以去看书哈。


以上所述就是小编给大家介绍的《Vue 源码剖析 —— 变化侦测相关 API 实现原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

写给大家看的设计书(第4版)

写给大家看的设计书(第4版)

Robin Williams / 苏金国、李盼 / 人民邮电出版社 / 2016-1 / 59.00元

畅销设计入门书最新版,让每个人都能成为设计师 在这个创意无处不在的时代,越来越多的人成为设计师。简历、论文、PPT、个人主页、博客、活动海报、给客人的邮件、名片……,处处都在考验你的设计能力。 美术功课不好?没有艺术细胞?毫无设计经验? 没关系!在设计大师RobinWilliams看来,设计其实很简单。在这部畅销全球多年、影响了一代设计师的经典著作中,RobinWilliams将......一起来看看 《写给大家看的设计书(第4版)》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试