vue2源码学习--08监听属性watch

68 阅读2分钟

本篇实现watch,watch用到的仍然是watcher类,依赖收集我们已经实现了,只要数据更新的时候我们调一下传进来的回调函数即可,我们此次主要要解决的是 watch的回调要获取到旧值和新值的问题。


写watch之前我们要知道watch是一个对象,里边是以key:walue的形式存在的,以下是常用的几种写法:

  • 1.value是函数
  • 2.value是字符串,这个字符串会从methods里去找
  • 3.value是数组,当数据改变会依次执行数组里的回调函数
  • 4.value是对象,主要是有handler和immediate
  • 5.函数式调用,this.$watch(()=>key,回调)
export function initState(vm) {
  const opts = vm.$options; // 获取所有选项
  if(opts.data) {
      initData(vm)
  }
  if(opts.computed) {
      initComputed(vm)
  }
  // 初始化watch
  if(opts.watch) {
      initWatch(vm)
  }
}
function initWatch(vm) {
  let watch =vm.$options.watch
  for (let 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)
     }
  }
}
function createWatcher(vm, key, handler) {
  let options = {}
  if(typeof handler === 'string') {
  // 如果回调是字符串 会从methods里找,我们现在还没实现methods
      handler = vm[handler]
  }
  // 兼容一下对象
  else if (Object.prototype.toString.call(handler) === '[object Object]') {
    options = handler
    handler = handler.handler
  }
  // 无论哪种调用方式最后都是通过$watch
  return vm.$watch(key, handler, options)
}

以上不过都是在处理watch的不同写法,最后统一下调用方式,下一步就是挂上$watch,$watch也很简单就是创建一个watcher,不过我们又需要改造watcher类,需要给传个唯一标识和回调函数。

export function initStateMixin(Vue) {
  Vue.prototype.$nextTick = nextTick

  Vue.prototype.$watch = function(experOrFn, cb, options={}) {
      options.user = true
      const watcher = new Watcher(this, experOrFn, options, cb)
      // 立即执行
      if(options.immediate) {
        cb.call(this, watcher.value, undefined)
      }
  }
}

watcher改动思路:为什么要通过watcher实现?是因为watch是个典型的发布订阅,我们watcher类已经实现了基本的功能,watch只要数据变化就会去读取一次新值,在$watch这种调用方式时第一个传的是函数(不是函数要处理成函数),也就是会执行一次这个函数,同时也要执行一下传进来的回调函数,由于我们写computed的时候已经会把getter的值记录到value上(不过是lazy的,所以我们要改下,在watch的时候第一次执行就要记录到value),所以我们把key的函数(不是函数处理成函数)当做第二个参数传进来挂到value上,这样可以通过实例获取到value的旧值,当数据更新,再次执行getter并重新对value赋值拿到新值,并执行下传进来的回调即可


完整watcher代码:

class Watcher {
    constructor(vm, expreOrFn, options, cb) {
        this.id = id++
        this.renderWatcher = options

        if(typeof expreOrFn === 'string') {
          this.getter = function() {
              return vm[expreOrFn]
          }
        } else {
            this.getter = expreOrFn // getter 意味着调用函数发生取值操作
        }
        
        this.deps = [] // 后续实现计算属性 和清理工作
        this.depsId = new Set()
        this.vm = vm
        // 计算属性标示
        this.lazy = options.lazy
        this.dirty = this.lazy // 缓存值
        this.cb = cb
        this.user = options.user // 标示是否是用户自己的user
        // 执行user watcher 直接把值记录下来
        this.value = this.lazy? undefined: this.get()
    }
    get() {
        pushTarget(this) // 调用的时候把当前watcher赋值给Dep.target
        let value = this.getter.call(this.vm) // 会去vm上取值
        popTarget(this) // 渲染完成置空
        return value
    } 
    addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录 
        let id = dep.id
        if(!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
        }
    }
    evaluate() {
      this.value = this.get() // 获取用户的返回值 并且标记为脏
      this.dirty = false  
    }
    depend() {
      let i = this.deps.length
      while(i--) {
          this.deps[i].depend() //  
      }
    }
    update() { 
      if(this.lazy) {
          this.dirty = true
      } else {
          queueWatcher(this)
      }
    }
    run() {
      // 更新的时候通过value获取旧值 重新执行get获取新值 同时执行下回调即可
      let oldValue = this.value
      let newValue = this.value = this.get() 
      if(this.user) {
          this.cb.call(this.vm ,newValue, oldValue)
      }
    }

}

大功告成,检验下效果叭

image.png

image.png