关于Vue源码vm.$watch()内部原理详解

 更新时间:2019年04月26日 09:09:16   作者:小诺哥  
这篇文章主要介绍了关于Vue源码vm.$watch()内部原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

关于vm.$watch()详细用法可以见官网

大致用法如下:

<script>
  const app = new Vue({
    el: "#app",
    data: {
      a: {
        b: {
          c: 'c'
        }
      }
    },
    mounted () {
      this.$watch(function () {
        return this.a.b.c
      }, this.handle, {
        deep: true,
        immediate: true // 默认会初始化执行一次handle
      })
    },
    methods: {
      handle (newVal, oldVal) {
        console.log(this.a)
        console.log(newVal, oldVal)
      },
      changeValue () {
        this.a.b.c = 'change'
      }
    }
  })
</script>

可以看到data属性整个a对象被Observe, 只要被Observe就会有一个__ob__标示(即Observe实例), 可以看到__ob__里面有dep,前面讲过依赖(dep)都是存在Observe实例里面, subs存储的就是对应属性的依赖(Watcher)。 好了回到正文, vm.$watch()在源码内部如果实现的。

内部实现原理

// 判断是否是对象
export function isPlainObject (obj: any): boolean {
 return _toString.call(obj) === '[object Object]'
}

源码位置: vue/src/core/instance/state.js

// $watch 方法允许我们观察数据对象的某个属性,当属性变化时执行回调
// 接受三个参数: expOrFn(要观测的属性), cb, options(可选的配置对象)
// cb即可以是一个回调函数, 也可以是一个纯对象(这个对象要包含handle属性。)
// options: {deep, immediate}, deep指的是深度观测, immediate立即执行回掉
// $watch()本质还是创建一个Watcher实例对象。

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
 ): Function {
  // vm指向当前Vue实例对象
  const vm: Component = this
  if (isPlainObject(cb)) {
   // 如果cb是一个纯对象
   return createWatcher(vm, expOrFn, cb, options)
  }
  // 获取options
  options = options || {}
  // 设置user: true, 标示这个是由用户自己创建的。
  options.user = true
  // 创建一个Watcher实例
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
   // 如果immediate为真, 马上执行一次回调。
   try {
    // 此时只有新值, 没有旧值, 在上面截图可以看到undefined。
    // 至于这个新值为什么通过watcher.value, 看下面我贴的代码
    cb.call(vm, watcher.value)
   } catch (error) {
    handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
   }
  }
  // 返回一个函数,这个函数的执行会解除当前观察者对属性的观察
  return function unwatchFn () {
   // 执行teardown()
   watcher.teardown()
  }
 }

关于watcher.js。

源码路径: vue/src/core/observer/watcher.js

export default class Watcher {
 vm: Component;
 expression: string;
 cb: Function;
 id: number;
 deep: boolean;
 user: boolean;
 lazy: boolean;
 sync: boolean;
 dirty: boolean;
 active: boolean;
 deps: Array<Dep>;
 newDeps: Array<Dep>;
 depIds: SimpleSet;
 newDepIds: SimpleSet;
 before: ?Function;
 getter: Function;
 value: any;

 constructor (
  vm: Component, // 组件实例对象
  expOrFn: string | Function, // 要观察的表达式
  cb: Function, // 当观察的表达式值变化时候执行的回调
  options?: ?Object, // 给当前观察者对象的选项
  isRenderWatcher?: boolean // 标识该观察者实例是否是渲染函数的观察者
 ) {
  // 每一个观察者实例对象都有一个 vm 实例属性,该属性指明了这个观察者是属于哪一个组件的
  this.vm = vm
  if (isRenderWatcher) {
   // 只有在 mountComponent 函数中创建渲染函数观察者时这个参数为真
   // 组件实例的 _watcher 属性的值引用着该组件的渲染函数观察者
   vm._watcher = this
  }
  vm._watchers.push(this)
  // options
  // deep: 当前观察者实例对象是否是深度观测
  // 平时在使用 Vue 的 watch 选项或者 vm.$watch 函数去观测某个数据时,
  // 可以通过设置 deep 选项的值为 true 来深度观测该数据。
  // user: 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的
  // 无论是 Vue 的 watch 选项还是 vm.$watch 函数,他们的实现都是通过实例化 Watcher 类完成的
  // sync: 告诉观察者当数据变化时是否同步求值并执行回调
  // before: 可以理解为 Watcher 实例的钩子,当数据变化之后,触发更新之前,
  // 调用在创建渲染函数的观察者实例对象时传递的 before 选项。
  if (options) {
   this.deep = !!options.deep
   this.user = !!options.user
   this.lazy = !!options.lazy
   this.sync = !!options.sync
   this.before = options.before
  } else {
   this.deep = this.user = this.lazy = this.sync = false
  }
  // cb: 回调
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  // 避免收集重复依赖,且移除无用依赖
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  this.expression = process.env.NODE_ENV !== 'production'
   ? expOrFn.toString()
   : ''
  // 检测了 expOrFn 的类型
  // this.getter 函数终将会是一个函数
  if (typeof expOrFn === 'function') {
   this.getter = expOrFn
  } else {
   this.getter = parsePath(expOrFn)
   if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
     `Failed watching path: "${expOrFn}" ` +
     'Watcher only accepts simple dot-delimited paths. ' +
     'For full control, use a function instead.',
     vm
    )
   }
  }
  // 求值
  this.value = this.lazy
   ? undefined
   : this.get()
 }

 /**
  * 求值: 收集依赖
  * 求值的目的有两个
  * 第一个是能够触发访问器属性的 get 拦截器函数
  * 第二个是能够获得被观察目标的值
  */
 get () {
  // 推送当前Watcher实例到Dep.target
  pushTarget(this)
  let value
  // 缓存vm
  const vm = this.vm
  try {
   // 获取value
   value = this.getter.call(vm, vm)
  } catch (e) {
   if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
   } else {
    throw e
   }
  } finally {
   // "touch" every property so they are all tracked as
   // dependencies for deep watching
   if (this.deep) {
    // 递归地读取被观察属性的所有子属性的值
    // 这样被观察属性的所有子属性都将会收集到观察者,从而达到深度观测的目的。
    traverse(value)
   }
   popTarget()
   this.cleanupDeps()
  }
  return value
 }

 /**
  * 记录自己都订阅过哪些Dep
  */
 addDep (dep: Dep) {
  const id = dep.id
  // newDepIds: 避免在一次求值的过程中收集重复的依赖
  if (!this.newDepIds.has(id)) {
   this.newDepIds.add(id) // 记录当前watch订阅这个dep
   this.newDeps.push(dep) // 记录自己订阅了哪些dep
   if (!this.depIds.has(id)) {
    // 把自己订阅到dep
    dep.addSub(this)
   }
  }
 }

 /**
  * Clean up for dependency collection.
  */
 cleanupDeps () {
  let i = this.deps.length
  while (i--) {
   const dep = this.deps[i]
   if (!this.newDepIds.has(dep.id)) {
    dep.removeSub(this)
   }
  }
  //newDepIds 属性和 newDeps 属性被清空
  // 并且在被清空之前把值分别赋给了 depIds 属性和 deps 属性
  // 这两个属性将会用在下一次求值时避免依赖的重复收集。
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
 }

 /**
  * Subscriber interface.
  * Will be called when a dependency changes.
  */
 update () {
  /* istanbul ignore else */
  if (this.lazy) {
   this.dirty = true
  } else if (this.sync) {
   // 指定同步更新
   this.run()
  } else {
   // 异步更新队列
   queueWatcher(this)
  }
 }

 /**
  * Scheduler job interface.
  * Will be called by the scheduler.
  */
 run () {
  if (this.active) {
   const value = this.get()
   // 对比新值 value 和旧值 this.value 是否相等
   // 是对象的话即使值不变(引用不变)也需要执行回调
   // 深度观测也要执行
   if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
   ) {
    // set new value
    const oldValue = this.value
    this.value = value
    if (this.user) {
     // 意味着这个观察者是开发者定义的,所谓开发者定义的是指那些通过 watch 选项或 $watch 函数定义的观察者
     try {
      this.cb.call(this.vm, value, oldValue)
     } catch (e) {
      // 回调函数在执行的过程中其行为是不可预知, 出现错误给出提示
      handleError(e, this.vm, `callback for watcher "${this.expression}"`)
     }
    } else {
     this.cb.call(this.vm, value, oldValue)
    }
   }
  }
 }

 /**
  * Evaluate the value of the watcher.
  * This only gets called for lazy watchers.
  */
 evaluate () {
  this.value = this.get()
  this.dirty = false
 }

 /**
  * Depend on all deps collected by this watcher.
  */
 depend () {
  let i = this.deps.length
  while (i--) {
   this.deps[i].depend()
  }
 }

 /**
  * 把Watcher实例从从当前正在观测的状态的依赖列表中移除
  */
 teardown () {
  if (this.active) {
   // 该观察者是否激活状态 
   if (!this.vm._isBeingDestroyed) {
    // _isBeingDestroyed一个标识,为真说明该组件实例已经被销毁了,为假说明该组件还没有被销毁
    // 将当前观察者实例从组件实例对象的 vm._watchers 数组中移除
    remove(this.vm._watchers, this)
   }
   // 当一个属性与一个观察者建立联系之后,属性的 Dep 实例对象会收集到该观察者对象
   let i = this.deps.length
   while (i--) {
    this.deps[i].removeSub(this)
   }
   // 非激活状态
   this.active = false
  }
 }
}
export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
// path为keypath(属性路径) 处理'a.b.c'(即vm.a.b.c) => a[b[c]]
export function parsePath (path: string): any {
 if (bailRE.test(path)) {
  return
 }
 const segments = path.split('.')
 return function (obj) {
  for (let i = 0; i < segments.length; i++) {
   if (!obj) return
   obj = obj[segments[i]]
  }
  return obj
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • vue2.x select2 指令封装详解

    vue2.x select2 指令封装详解

    本篇文章主要介绍了vue2.x select2 指令封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Storybook 7.0 Beta Vue3踩坑解决记录

    Storybook 7.0 Beta Vue3踩坑解决记录

    这篇文章主要为大家介绍了Storybook 7.0 Beta Vue3踩坑解决记录详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • vue点击按钮实现简单页面的切换

    vue点击按钮实现简单页面的切换

    这篇文章主要为大家详细介绍了vue点击按钮实现简单页面的切换,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-09-09
  • Vue 动态路由的实现详情

    Vue 动态路由的实现详情

    这篇文章主要介绍了Vue 动态路由的实现,动态路由是一个常用的功能,根据后台返回的路由json表,前端动态显示可跳转的路由项,本文主要实现的是后台传递路由,前端拿到并生成侧边栏的一个形势,需要的朋友可以参考一下
    2022-06-06
  • 计算属性和侦听器详情

    计算属性和侦听器详情

    这篇文章主要介绍了计算属性和侦听器,文章以介绍计算属性、侦听器的相关资料展开详细内容,需要的朋友可以参考一下,希望对你有所帮助
    2021-11-11
  • 浅析vue3中组件的二次封装

    浅析vue3中组件的二次封装

    在实际开发中每个开发者应该都有经历过对组件进行二次封装,本文将从三个方面来基于 Element UI 的el-input组件简单实现一下组件的二次封装,有需要的可以参考下
    2023-09-09
  • vue3中路由传参query、params及动态路由传参详解

    vue3中路由传参query、params及动态路由传参详解

    vue3中的传参方式和vue2中一样,都可以用query和params传参,下面这篇文章主要给大家介绍了关于vue3中路由传参query、params及动态路由传参的相关资料,需要的朋友可以参考下
    2022-09-09
  • vue清空数组的几个方式(小结)

    vue清空数组的几个方式(小结)

    本文主要介绍了vue清空数组的几个方式,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • vue项目中实现缓存的最佳方案详解

    vue项目中实现缓存的最佳方案详解

    这篇文章主要给大家介绍了关于vue项目中实现缓存的最佳方案,文中通过示例代码介绍的非常详细,对大家学习或者使用vue具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • vue.js根据代码运行环境选择baseurl的方法

    vue.js根据代码运行环境选择baseurl的方法

    本篇文章主要介绍了vue.js根据代码运行环境选择baseurl的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02

最新评论