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


Posted in Javascript onApril 26, 2019

关于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>

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

可以看到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
 }
}

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

Javascript 相关文章推荐
jquery插件冲突(jquery.noconflict)解决方法分享
Mar 20 Javascript
深入理解JavaScript系列(46):代码复用模式(推荐篇)详解
Mar 04 Javascript
在easyUI开发中,出现jquery.easyui.min.js函数库问题的解决办法
Sep 11 Javascript
Node.js下自定义错误类型详解
Oct 17 Javascript
JavaScript中的toString()和toLocaleString()方法的区别
Feb 15 Javascript
详解Vue中过度动画效果应用
May 25 Javascript
详解在AngularJS的controller外部直接获取$scope
Jun 02 Javascript
基于jQuery实现Ajax验证用户名是否可用实例
Mar 25 jQuery
详解React中setState回调函数
Jun 14 Javascript
如何使用JavaScript实现栈与队列
Jun 24 Javascript
6种JavaScript继承方式及优缺点(小结)
Feb 06 Javascript
vue实现点击按钮“查看详情”弹窗展示详情列表操作
Sep 09 Javascript
JS异步错误捕获的一些事小结
Apr 26 #Javascript
原生JS实现图片懒加载之页面性能优化
Apr 26 #Javascript
vue请求本地自己编写的json文件的方法
Apr 25 #Javascript
vue中img src 动态加载本地json的图片路径写法
Apr 25 #Javascript
详解如何给React-Router添加路由页面切换时的过渡动画
Apr 25 #Javascript
vue项目中使用fetch的实现方法
Apr 25 #Javascript
详解vuejs2.0 select 动态绑定下拉框支持多选
Apr 25 #Javascript
You might like
php打造属于自己的MVC框架
2012/03/07 PHP
PHP集成百度Ueditor 1.4.3
2014/11/23 PHP
PHP如何通过传引用的思想实现无限分类(代码简单)
2015/10/13 PHP
thinkPHP5使用Rabc实现权限管理
2019/08/28 PHP
Hutia 的 JS 代码集
2006/10/24 Javascript
js+html5实现可在手机上玩的拼图游戏
2015/07/17 Javascript
ionic进入多级目录后隐藏底部导航栏(tabs)的完美解决方案
2016/11/23 Javascript
超简单的Vue.js环境搭建教程
2017/03/17 Javascript
关于Angular2 + node接口调试的解决方案
2017/05/28 Javascript
浅谈mint-ui loadmore组件注意的问题
2017/11/08 Javascript
angularJs-$http实现百度搜索时的动态下拉框示例
2018/02/27 Javascript
对layui初始化列表的CheckBox属性详解
2019/09/13 Javascript
Javascript实现鼠标移入方向感知
2020/06/24 Javascript
vue中解决拖拽改变存在iframe的div大小时卡顿问题
2020/07/22 Javascript
JS实现百度搜索框
2021/02/25 Javascript
[15:56]Heroes18_暗影萨满(完美)
2014/10/31 DOTA
[01:36:19]Secret vs NB 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
[01:01:14]完美世界DOTA2联赛PWL S2 SZ vs Rebirth 第一场 11.21
2020/11/23 DOTA
python判断给定的字符串是否是有效日期的方法
2015/05/13 Python
Python实现Linux的find命令实例分享
2017/06/04 Python
Python3实现的简单验证码识别功能示例
2018/05/02 Python
Python Dataframe 指定多列去重、求差集的方法
2018/07/10 Python
Python爬虫常用小技巧之设置代理IP
2018/09/13 Python
对Python2与Python3中__bool__方法的差异详解
2018/11/01 Python
python 从文件夹抽取图片另存的方法
2018/12/04 Python
Python绘制热力图示例
2019/09/27 Python
PHP笔试题
2012/02/22 面试题
司机的工作范围及职责
2013/11/13 职场文书
求职简历中个人的自我评价
2013/12/25 职场文书
写演讲稿所需要注意的4个条件
2014/01/09 职场文书
小学语文课后反思精选
2014/04/25 职场文书
初中新生军训方案
2014/05/13 职场文书
装饰工程师岗位职责
2014/06/08 职场文书
离婚民事起诉状
2015/08/03 职场文书
初中英语教学随笔
2015/08/15 职场文书
Python find()、rfind()方法及作用
2022/12/24 Python