关于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 相关文章推荐
JavaScript中实现sprintf、printf函数
Jan 27 Javascript
jQuery获取URL请求参数的方法
Jul 18 Javascript
jQuery.Callbacks()回调函数队列用法详解
Jun 14 Javascript
深入浅析search 搜索框的写法
Aug 02 Javascript
js 判断附件后缀的简单实现方法
Oct 11 Javascript
jquery表单插件form使用方法详解
Jan 20 Javascript
抖音上用记事本编写爱心小程序教程
Apr 17 Javascript
vue实现前台列表数据过滤搜索、分页效果
May 28 Javascript
vue路由守卫及路由守卫无限循环问题详析
Sep 05 Javascript
JavaScript如何借用构造函数继承
Nov 06 Javascript
Vue2.0 ES6语法降级ES5的操作
Oct 30 Javascript
javascript实现下拉菜单效果
Feb 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 删除无限级目录与文件代码共享
2008/11/22 PHP
关于session在PHP5的配置文件中的详细设置参数说明
2011/04/20 PHP
PHP转换文本框内容为HTML格式的方法
2016/07/20 PHP
Javascript 面向对象 重载
2010/05/13 Javascript
JavaScript插件化开发教程 (一)
2015/01/27 Javascript
javascript实现当前页导航激活的方法
2015/02/27 Javascript
分享javascript实现的冒泡排序代码并优化
2016/06/05 Javascript
利用JS屏蔽页面中的Enter按键提交表单的方法
2016/11/25 Javascript
使用ng-packagr打包Angular的方法示例
2018/09/21 Javascript
vue 双向数据绑定的实现学习之监听器的实现方法
2018/11/30 Javascript
详解Vue基于vue-quill-editor富文本编辑器使用心得
2019/01/03 Javascript
关于js陀螺仪的理解分析
2019/04/11 Javascript
详解vue-cli3开发Chrome插件实践
2019/05/29 Javascript
DatePickerDialog 自定义样式及使用全解
2019/07/09 Javascript
详解JavaScript 作用域
2020/07/14 Javascript
[05:13]TI4 中国战队 机场出征!!
2014/07/07 DOTA
给Python初学者的一些编程技巧
2015/04/03 Python
python使用urllib2实现发送带cookie的请求
2015/04/28 Python
Python2.7基于笛卡尔积算法实现N个数组的排列组合运算示例
2017/11/23 Python
python通过ffmgep从视频中抽帧的方法
2018/12/05 Python
python执行精确的小数计算方法
2019/01/21 Python
深度辨析Python的eval()与exec()的方法
2019/03/26 Python
Python3.5文件读与写操作经典实例详解
2019/05/01 Python
Python创建一个元素都为0的列表实例
2019/11/28 Python
vue学习笔记之动态组件和v-once指令简单示例
2020/02/29 Python
python之pygame模块实现飞机大战完整代码
2020/11/29 Python
基于html和CSS3制作酷炫的导航栏
2015/09/23 HTML / CSS
css animation配合SVG制作能量流动效果
2021/03/24 HTML / CSS
校园新闻广播稿
2014/01/10 职场文书
高一生物教学反思
2014/01/17 职场文书
无传销社区工作方案
2014/05/13 职场文书
考生诚信考试承诺书
2014/05/23 职场文书
全国法院系统开展党的群众路线教育实践活动综述(全文)
2014/10/25 职场文书
2014年保洁员工作总结
2014/11/19 职场文书
创业计划书之蛋糕店
2019/08/29 职场文书
《战锤40K:暗潮》跳票至9月 公布新宣传片
2022/04/03 其他游戏