关于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 相关文章推荐
JS处理VBArray的函数使用说明
May 11 Javascript
javascript jQuery插件练习
Dec 24 Javascript
JavaScript中判断函数是new还是()调用的区别说明
Apr 07 Javascript
Javascript中自动切换焦点实现代码
Dec 15 Javascript
jQuery动态显示和隐藏datagrid中的某一列的方法
Dec 11 Javascript
jQuery.holdReady()方法用法实例
Dec 27 Javascript
DOM基础教程之使用DOM控制表格
Jan 20 Javascript
利用jQuery实现漂亮的圆形进度条倒计时插件
Sep 30 Javascript
JavaScript之Canvas_动力节点Java学院整理
Jul 04 Javascript
纯js代码生成可搜索选择下拉列表的实例
Jan 11 Javascript
node puppeteer(headless chrome)实现网站登录
May 09 Javascript
es6中Promise 对象基本功能与用法实例分析
Feb 23 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下常用正则表达式整理
2010/10/26 PHP
PHP分页详细讲解(有实例)
2013/10/30 PHP
PHP变量内存分配问题记录整理
2013/11/27 PHP
php的crc32函数使用时需要注意的问题(不然就是坑)
2015/04/21 PHP
PHP对称加密算法(DES/AES)类的实现代码
2017/11/14 PHP
php时间戳转换代码详解
2019/08/04 PHP
js change,propertychange,input事件小议
2011/12/20 Javascript
可简单避免的三个JS发布错误的详细介绍
2013/08/02 Javascript
点击按钮自动加关注的代码(sina微博/QQ空间/人人网/腾讯微博)
2014/01/02 Javascript
js查找节点的方法小结
2015/01/13 Javascript
jQuery实现时尚漂亮的弹出式对话框实例
2015/08/07 Javascript
jQuery实现的简洁下拉菜单导航效果代码
2015/08/26 Javascript
JS实现黑色风格的网页TAB选项卡效果代码
2015/10/09 Javascript
canvas学习之API整理笔记(二)
2016/12/29 Javascript
AngularJS实现路由实例
2017/02/12 Javascript
js轮播图无缝滚动效果
2017/06/17 Javascript
vue+iview+less+echarts实战项目总结
2018/02/22 Javascript
vue-router路由懒加载的实现(解决vue项目首次加载慢)
2018/08/28 Javascript
Nodejs 识别图片类型的方法
2019/08/15 NodeJs
js实现3D粒子酷炫动态旋转特效
2020/09/13 Javascript
vue+elementUI实现简单日历功能
2020/09/24 Javascript
python中wx将图标显示在右下角的脚本代码
2013/03/08 Python
利用Python开发微信支付的注意事项
2016/08/19 Python
TensorFlow 模型载入方法汇总(小结)
2018/06/19 Python
Python Pandas数据结构简单介绍
2019/07/03 Python
详解程序意外中断自动重启shell脚本(以Python为例)
2019/07/26 Python
django2.2安装错误最全的解决方案(小结)
2019/09/24 Python
详解html5 postMessage解决跨域通信的问题
2018/08/17 HTML / CSS
银行求职信个人范文
2013/12/16 职场文书
运动会通讯稿100字
2014/01/31 职场文书
目标责任书范文
2014/04/14 职场文书
带病坚持工作事迹
2014/05/03 职场文书
研究生求职自荐书
2014/06/23 职场文书
六五普法心得体会2016
2016/01/21 职场文书
python 利用PyAutoGUI快速构建自动化操作脚本
2021/05/31 Python
Python 处理表格进行成绩排序的操作代码
2021/07/26 Python