vue-watch 原理

379 阅读4分钟

vue-watch 原理

首先和数据双向绑定有关系 不懂得可以看一下我之前写得文章,稍作了解

咱们可以首先说一下它得流程

1. initWatch

首先在初始化组件时,会执行这个函数来初始化watch 这个函数的作用就是遍历 watch 的所有属性,执行createWatcher

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

2. createWatcher

createWatcher 的主要功能就是确定回调函数和options, 然后通过 $watch 实现监听

function createWatcher(
 vm,
 expOrFn, 
 handler,
 options
) {
 if (isPlainObject(handler)) {
   options = handler
   handler = handler.handler
 }
 if (typeof handler === 'string') {
   handler = vm[handler] 
 }
 return vm.$watch(expOrFn, handler, options)
}

参数介绍

  • vm: 组件实例
  • expOrFn: watch的 key 它可能是 根对象 也可能是深度的一个对象属性
  • handler: 用于 watch 监听的回调函数,可能是对象 也可能是函数,也可能是字符串,如果是对象就可能会配置下个参数options 如果是函数或者字符串 那 options 参数就是 undefined
  • options: 如果有,就是一个对象 是这样{deep:false,immediate:false}的 值不一定是false 看开发者的配置

例子

data(){
    return {
        obj:{
            name: 'watch'
        }
    }
}

// --------------------------------------------------------------------

// expOrFn 是根对象或者 深度的一个对象属性
watch:{
   obj:function(new,old){}, // 根对象
   'obj.name':function(new,old){}, // 深度监听name
}
// handler 是函数
watch:{
    obj:function(new,old){}, // handler 是函数
}
// handler 是对象
watch:{
    obj:{ // handler 是对象
        hanlder:function(new,old){},
        {
            deep:true,
            immediate:true
        }
    }
}
// handler 是字符串
methods:{
    watchCallback(new,old){}
},
watch:{
    obj: watchCallback // handler 是字符串
}

$watch

核心就是这个函数,先来看一下它的源码

Vue.prototype.$watch = function (
    expOrFn, // 监听的key
    cb, // 监听的回调
    options 
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }

因为 $watch 在运行时也可以添加监听属性,所以会在开头先判断一下回调函数是否已经处理过,没有处理过,就调用上一步的函数进行处理。

所以主要函数看后面的代码,用过watch 的都指定 options 中的属性是干什么用的 这里不多介绍! 看**const watcher = new Watcher(vm, expOrFn, cb, options)** 代码

大家先看一下 Wathcer 的源码 ,可以了解到 在 new Watcher 的时候,就会执行 watcherget 方法

先说明一些内容

  • this.getter = parsePath(expOrFn) 这段代码主要就是返回一个函数,用于获取 vm上的属性,比如 要获取 obj 或者 obj.name
function (){
    return vm['obj']
}
function (){
    return vm['obj']['name']
}
  • 所以在执行 get 时 就是 获取一下 data 中的属性
  • get 时 第一步就把当前的 Watcher 实例 推到了全局的 target
  • 然后执行value = this.getter.call(vm, vm) 获取 data 中的属性,让data 中的属性的 Dep 实例 收集到 全局的 target
  • 所以当 访问的 data 属性 更改时,就会拿到 $watch 创建的 Watcher 实例
  • 拿到 Watcher 实例 而且Watcher 实例 还保存了 $watch 的回调函数,那不就可以实现 data 属性更改后执行 $watch 的回调函数了吗?
class Watcher implements DepTarget {
    vm
    expression
    cb
    id
    deep
    user
    lazy
    sync
    dirty
    active
    deps
    newDeps
    depIds
    newDepIds
    before
    onStop
    noRecurse
    getter
    value
  
    constructor(
      vm,
      expOrFn,
      cb,
      options,
      isRenderWatcher
    ) {
      recordEffectScope(this, activeEffectScope || (vm ? vm._scope : undefined)) // 记录的影响范围
      if ((this.vm = vm)) {
        if (isRenderWatcher) {
          vm._watcher = this
        }
      }
      if (options) {
        this.deep = !!options.deep // watch
        this.user = !!options.user // watch
        this.lazy = !!options.lazy // computed 是true
        this.sync = !!options.sync 
        this.before = options.before
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      this.cb = cb
      this.id = ++uid 
      this.active = true
      this.dirty = this.lazy 
      this.deps = []
      this.newDeps = []
      this.depIds = new Set()
      this.newDepIds = new Set()
      this.expression = __DEV__ ? expOrFn.toString() : ''
      if (isFunction(expOrFn)) {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn) // watch 可以监听 'obj.xx.xx' 解析这样的路径
        if (!this.getter) {
          this.getter = noop
        }
      }
      this.value = this.lazy ? undefined : this.get()
    }
  
    get() {
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
          throw e
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
      }
      return value
    }
}

这行代码主要就是 创建了一个 Watcher 实例, 那在数据双向绑定原理中有提到,new Watcher 后 会执行 watcher.get() 这时就会把 Watcher 实例 推向全局的 target 中,全局的 target 就会将 data 属性中的 dep 收集起来 , data 属性中的 dep 也将全局的 target (也就是 $wather 执行时,new Watcher(vm, expOrFn, cb, options)) 收集起来, 那 data 中可以访问到 Watcher ,Watcher也可以访问 data 形成了相互引用的关系

现在他俩都相互引用到了,就有所关联了,那当data属性修改时,就通过data属性中可以访问到的Watcher让它执行自己的回调就可以了。

最后后面的代码就更简单了, 如果options.immediate == true 那就在初始化的时候先执行一次回调函数,最后返回一个函数用于移除监听

附图 刚刚画的一个图

1724582582749.jpg

parsePath 的实现

这个函数很简单,就是通过split 来分割 . 递归的获取到想要的属性

export function parsePath(path) {
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

小小结

不知道大家明白没有,不明白的点可以留言,大家可以结合源码看,效率会高一点

如果不知道如何看源码,我说一下我的方法

  • 首先就是把代码从 github 先下载下来,然后先了解一下结构目录,确认一下入口文件
  • 源码下载下来会有一个 examples 文件夹 就是例子 可以以例子来执行
  • 通过断点的方式 找不到上下文就先断点到附近,之后再一点一点的调试
  • 实在没啥耐心,就问一下 AI ,多问 AI 先从简单的问起,AI 也懒 刚开始就会说个概念,当你一次有一次的提问,它就会尽可能把它知道的告诉你 然后你再结合之前的方法 再学
  • 看源码本来就是很耗时的一件事情 但是提升也是较大的

没看懂的小伙伴不好意思了,耽误大家时间了,如果想看 Vue 其他相关知识 留言~~

Vue 双向绑定原理

Vue Computed 原理