深入浅出vm.$watch和watch初始化

2020/3/20 11:01:52

本文主要是介绍深入浅出vm.$watch和watch初始化,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

1.业务场景
2.用法
3.vm.$watch内部原理
4.deep参数实现原理
5.初始化watch
6.总结

1.业务场景

watch是用来监听某个数据发生变化,之后调用什么函数处理。一个数据可以去影响多个数据,比如说浏览器自适应、监控路由对象、监控自身属性变化等等。

2.用法

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

定义:官方文档是这么写的:观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
实例

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
 // 做点什么
})

// 函数
vm.$watch(
 function () {
   // 表达式 `this.a + this.b` 每次得出一个不同的结果时
   // 处理函数都会被调用。
   // 这就像监听一个未被定义的计算属性
   return this.a + this.b
 },
 function (newVal, oldVal) {
   // 做点什么
 }
)
复制代码

选项deep:为了发现对象内部值的变化,可以在选项参数中指定 deep: true 。注意监听数组的变动不需要这么做。

vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
复制代码

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

vm.$watch('a', callback, {
  immediate: true
})
// 立即以 `a` 的当前值触发回调
复制代码

返回值:vm.$watch会返回一个取消此监听的函数。

var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true }
)
复制代码

3.vm.$watch内部原理

实际上,vm.$watch其实是对watcher的一种封装,watcher是什么,不太懂的可以参考我这篇文章:深入浅出vue变化侦测

我们通过watcher完全可以实现watcher的功能,但同时,vm.$watch中的参数deep和immediate是没有的。下面我们看看vm.$watch是如何实现的:

Vue.prototype.$watch=function(exp,cb,options){
  const vm=this
  options=options||{}
  const watcher=new Watcher(vm,exp,cb,options)
  if(options.immediate){
    cb.call(vm,watcher.value)
  }
  return function unwatchFn(){
    watcher.teardown()
  }
}
复制代码

逻辑也很简单,执行 new Watcher来实现vm.$watch基本功能的。
但是有一个细节,exp是可以接受函数的,所以watcher构造函数就要改一改。

export default class watcher{
  constructor(vm,exp,cb){
    this.vm=vm
    //exp参数支持函数
    if(typeof exp==='function'){
      this.getter=exp
    }else{
      this.getter=parsePath(exp)
    }
    this.cb=cb
    this.value=this.get()
  }
  ......
}
复制代码

当exp为函数时,它不止可以动态返回数据,其中读取的所有数据都会被watcher观察到。任何一个发生变化时,wacther都会得到通知。

返回值:执行完vm.$watch后,返回一个函数unwatchFn,作用取消观察数据。既然要取消观察,我们就要知道watcher它自己都订阅了谁,也就是说watcher实例收集了哪些Dep。循环自己收集的Dep将自己从Dep中的依赖移除即可。

在watcher中添加addDep方法,作用是记录自己都订阅了哪些Dep:

export default class watcher{
  constructor(vm,exp,cb){
    this.vm=vm
    //新增
    this.deps=[]
    // 新增
    this.depIds=new Set()
    if(typeof exp==='function'){
      this.getter=exp
    }else{
      this.getter=parsePath(exp)
    }
    this.cb=cb
    this.value=this.get()
  }
  //新增
  addDep(dep){
    const id=dep.id
    // dep watcher产生互相关联
    if(!this.depIds.has[id]){
      this.depIds.add(id)
      //watcher添加dep
      this.deps.push(dep)
      //dep添加watcher
      dep.addSub(this)
    }
  }
  ......
}
复制代码

代码分析:我们使用了depIds来判断当前watcher已经订阅Dep,就不会重新订阅。当watcher读取value时,会触发收集依赖逻辑。通过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){
      this.addSub(window.target)  //废弃
      window.target.addDep(this)  //新增
    }
  }
}
复制代码

代码分析:此时,Dep会记录数据发生变化时,需要通知哪些watcher,而watcher也记录了自己被哪些Dep通知。所以Dep和Watcher是多对多的关系

为什么是多对多的关系:我们知道当视图多次使用一个数据时,此时一个Dep对应多个watcher.同时,当一个watcher观察的参数是函数时,此时该函数使用了多个数据时,那么这个watcher就要收集多个Dep了。

unwatcher:我们已经知道watcher订阅了哪些Dep,就可以在Watcher中新增teardown方法来通知这些订阅,把他们从依赖列表中移除掉:

//从dep列表移除自己
teardown(){
  let i=this.deps.length
  while(i--){
    this.deps[i].removeSub(this)
  }
}
复制代码

dep中添加removeSub方法:

removeSub(sub){
    const index=this.subs.indexOf(sub)
    if(index>-1){
      return this.subs.splice(index,1)
    }
  }
复制代码

以上就是unwatch的原理,当数据改变时,也不会通知已经删去的watcher了。

4deep参数原理实现

deep的功能就是除了要触发当前这个被监听数据的收集依赖逻辑之外,还要把这个值在内的所有子值都要触发一遍收集依赖,watcher更改如下:

export default class watcher{
  constructor(vm,exp,cb){
    this.vm=vm
    //新增
   if(options){
     this.deep=!!options.deep
   }else{
     this.deep=false
   }
    this.deps=[]
    this.depIds=new Set()
    if(typeof exp==='function'){
      this.getter=exp
    }else{
      this.getter=parsePath(exp)
    }
    this.cb=cb
    this.value=this.get()
  }
  get(){
    window.target=this
    let value=this.getter.call(this.vm,this.vm)
    //新增
    if(this.deep){
      traverse(value)
    }
    window.target=undefined
    return value
  }
}
复制代码

代码分析:如果用户使用了deep参数,会在window.target=undefined之前调用traverse来处理deep逻辑。否则,watcher就不会收集到子值的依赖列表中了。

traverse函数:其实这个函数很简单。就是采用递归的方式,要递归value所有子值来触发他们收集依赖功能。

5.初始化watch

类型:{ [key: string]: string | Function | Object | Array }

官网解释:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。

示例

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // 方法名
    b: 'someMethod',
    // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true
    },
    e: [
      'handle1',
      function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
        /* ... */
      }
    ],
    // watch vm.e.f's value: {g: 5}
    'e.f': function (val, oldVal) { /* ... */ }
  }
})
vm.a = 2 // => new: 2, old: 1
复制代码

初始化分析: 初始化watch其实不难,watch和vm.$watch功能是相同的,所以只要循环watch选项,将对象每一项调用vm.$watch方法来实现。

但是watch选项的值同时支持字符串,函数,数组,对象。不同的类型有不同的用法,所以在调用vm.$watch时我们需要做一些适配。

function initWatch(vm,watch){
  for(const key in watch){
    const handler=watch[key]
    if(Array.isArray(handler)){
      for(let i=0;i<handler.length;i++){
        createWatcher(vm,key,handler[i])
      }
    }else{
      createWatcher(vm,key,handler)
    }
  }
}
复制代码

代码分析:先把watch选项值分为两类,数组和其他。接着在调用createWatcher函数处理其他类型并调用vm.$watch创建观察表达式。

function createWatcher(vm,exp,handler,options){
  if(isPlainObject(handler)){
    options=handler
    handler=handler.handler
  }
  if(typeof handler==='string'){
    handler=vm[handler]
  }
  return vm.$watch(exp,handler,options)
}
复制代码

代码分析:处理了三种类型:

  • 函数:不用特殊处理,直接传递给vm.$watch
  • 字符串:从vm取出方法,将它赋值给handler
  • 对象:options的值设置为handler,并且将变量handler设置为handler对象handler方法。

watch初始化原理就已经说完了。

6.总结

关于watch这一方面其实不是很难,主要是灵活运用了Watcher类和Dep类.

一天较真一个api,一天比一天进步。



这篇关于深入浅出vm.$watch和watch初始化的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程