Vue2源码解析 变化侦测相关的API实现原理-vm.$watch

目录

1  用法

2  watch的内部原理

3  deep参数的实现原理

1  用法

用法:vm.$watch( expOrFn , callback , [ options ] );

          用于观察一个表达式或computed函数在vue.js实例上的变化。回调函数调用时,会从参数得到新数据和旧数据。表达式只接受以点分割的路径,例如a.b.c。比较复杂的表达式,可以用函数代替表达式。

var unwatch = vm.$watch("a", (newVal, oldVal) => {});
// 之后取消观察
unwatch();

vm.$watch返回一个取消观察函数,用来停止触发回调。

deep:为了发现对象内部值的变化,可以在选项参数中指定deep:true;

immediate:在选项参数中指定immediate:true,将立即以表达式的当前值触发回调;

2  watch的内部原理

vm.$watch其实就是对Watcher的一种封装。通过Watcher完全可以实现vm.$watch的功能,但是vm.$watch中的参数deep和immediate是Watcher中所没有的。

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();
  };
};
export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.deps = []; // 新增
    this.depIds = new Set(); // 新增
    // expOrFn参数支持函数
    // 如果expOrFn是函数,则直接将它赋值给getter;
    // 如果不是函数,再使用parsePath函数来读取keypath中的数据;
    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;
    if (!this.depIds.has(id)) {
      // 用来记录当前Watcher已经订阅了这个Dep
      this.depIds.add(id);
      // 记录自己都订阅了哪些Dep
      this.deps.push(dep);
      // 触发将自己订阅到Dep中
      dep.addSub(this);
    }
  }
  // ...
}

当expOrFn是函数时,会发生很神奇的事情。它不只可以动态返回数据,其中读取的所有数据也都会被Watcher观察。当expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有vue.js实例上的响应式数据。也就是说,如果函数从vue.js实例上读取了两个数据,那么Watcher会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。

取消观察,本质上是把watcher实例从当前正在观察的状态的依赖列表中移除。

在Watcher中新增addDep方法后,Dep中收集依赖的逻辑也需要有所改变:

let uid = 0 // 新增
export default class Dep{
  constructor(){
    this.id = uid++; // 新增
    this.subs = []
  }
  // ...
  depend(){
    if(window.target){
      this.addSub(window.target) //废弃
      window.target.addDep(this) // 新增
    }
  }
  // ...
}

此时,Dep会记录数据发生变化时,需要通知哪些Watcher。而Watcher中也同样记录了自己会被哪些Dep通知。Watcher与Dep的关系如下图所示:

为什么是多对多的关系?Watcher每次只读一个数据,不是应该只有一个Dep吗?

其实不是。如果Watcher中的expOrFn参数是一个表达式,那么肯定只收集一个Dep,并且大部分都是这样。但凡事总有例外,expOrFn可以是一个函数,此时如果该函数中使用了多个数据,那么这时Watcher就要收集多个Dep了,例如:

this.$watch(function (){
  return this.name + this.age
},function (newValue,oldValue){
  console.log(newValue,oldValue)
})

如果表达式是一个函数,并且函数中访问了name和age两个数据,这种清情况下Watcher内部收集两个Dep —— name和age的Dep,同时这两个Dep中也会收集Watcher,这导致age和name中的任意一个数据发生变化时,Watcher都会收到通知。

当我们已经在Watcher中记录自己订阅了哪些Dep之后,就可以在Watcher中新增teardown方法来通知这些订阅的Dep,让它们把自己从依赖列表中移除掉:

/**
 * 从所有依赖项的Dep列表中将自己移除
 */
teardown(){
  let i = this.deps.length
  while(i--){
    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)
    }
  }
  // ...
}

unwatch原理:把Watcher从sub中删除掉,然后当数据发生变化时,将不再通知这个已经删除的Watcher。

3  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 = []
    // ...
  }
  get(){
    window.target = this
    let value = this.getter.call(vm,vm)
    // 新增
    if(this.deep){
      traverse(value)
    }
    window.target = undefined
    return value
  }
  // ...
}

在上面的代码中,如果用户使用了deep参数,则在window.target = undefined之前带用traverse来处理deep的逻辑。

接下来,要递归value的所有子值来触发它们收集依赖的功能:


const seenObjects = new Set()
export function traverse(val){
  _tranverse(val,seenObjects)
  seenObjects.clear()
}
function _tranverse(val,seen){
  /**
   * 而_tranverse函数其实是一个递归操作,所以这个value的子值也会触发同样的逻辑,
   * 这样就可以实现通过deep参数来监听所有子值的变化
   */
  let i,keys
  const isA = Array.isArray(val)
  if((!isA && !isObject(val)) || Object.isFrozen(val)){
    // 先判断val的类型,如果它不是Array和Object,或者已经被冻结,那什么也不干
    return
  }
  if(val.__ob__){
    // 拿到val的dep.id,用这个id来保证不会重复收集依赖
    const depId = val.__ob__.dep.id
    if(seen.has(depId)){
      return
    }
    seen.add(depId)
  }
  // 重点!!!如果是Object类型,则循环Object中所有的key,然后执行一次读取操作,再递归子值
  if(isA){
    i = val.length
    while(i--) _tranverse(val[i],seen)
  }else{
    /**
     * val[keys[i]]会触发getter,也就是说会触发收集依赖的操作,这时window.target还没被清空,
     * 会将当前的Watcher收集进去。
     */
    keys = Object.keys(val)
    i = keys.length
    while(i--) _tranverse(val(val[keys[i]],seen))
  }
}

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值