详解Vue中的watch和computed


Posted in Javascript onNovember 09, 2020

前言

对于使用Vue的前端而言,watch、computed和methods三个属性相信是不陌生的,是日常开发中经常使用的属性。但是对于它们的区别及使用场景,又是否清楚,本文我将跟大家一起通过源码来分析这三者的背后实现原理,更进一步地理解它们所代表的含义。 在继续阅读本文之前,希望你已经具备了一定的Vue使用经验,如果想学习Vue相关知识,请移步至官网。

Watch

我们先来找到watch的初始化的代码,/src/core/instance/state.js

export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if (opts.props) initProps(vm, opts.props) // 初始化props
 if (opts.methods) initMethods(vm, opts.methods) // 初始化方法
 if (opts.data) {
 initData(vm) // 先初始化data 重点
 } else {
 observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
 if (opts.watch && opts.watch !== nativeWatch) {
 initWatch(vm, opts.watch) // 初始化watch
 }
}

接下来我们深入分析一下initWatch的作用,不过在接下去之前,这里有一点是data的初始化是在computed和watch初始化之前,这是为什么呢?大家可以停在这里想一下这个问题。想不通也没关系,继续接下来的源码分析,这个问题也会迎刃而解。

initWatch

function initWatch (vm: Component, watch: Object) {
 for (const key in watch) {
 const handler = watch[key]
 if (Array.isArray(handler)) { // 如果handler是一个数组
  for (let i = 0; i < handler.length; i++) { // 遍历watch的每一项,执行createWatcher
  createWatcher(vm, key, handler[i])
  }
 } else {
  createWatcher(vm, key, handler) 
 }
 }
}

createWatcher

function createWatcher (
 vm: Component,
 expOrFn: string | Function,
 handler: any,
 options?: Object
) {
 if (isPlainObject(handler)) { // 判断handler是否是纯对象,对options和handler重新赋值
 options = handler
 handler = handler.handler
 }
 if (typeof handler === 'string') { // handler用的是methods上面的方法,具体用法请查看官网文档
 handler = vm[handler]
 }
 // expOrnFn: watch的key值, handler: 回调函数 options: 可选配置
 return vm.$watch(expOrFn, handler, options) // 调用原型上的$watch
}

Vue.prototype.$watch

Vue.prototype.$watch = function (
 expOrFn: string | Function,
 cb: any,
 options?: Object
 ): Function {
 const vm: Component = this
 if (isPlainObject(cb)) { // 判断cb是否是对象,如果是则继续调用createWatcher
  return createWatcher(vm, expOrFn, cb, options)
 }
 options = options || {}
 options.user = true // user Watcher的标示 options = { user: true, ...options }
 const watcher = new Watcher(vm, expOrFn, cb, options) // new Watcher 生成一个user Watcher
 if (options.immediate) { // 如果传入了immediate 则直接执行回调cb
  try {
  cb.call(vm, watcher.value)
  } catch (error) {
  handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
  }
 }
 return function unwatchFn () {
  watcher.teardown()
 }
 }
}

上面几个函数调用的逻辑都比较简单,所以就在代码上写了注释。我们重点关注一下这个userWatcher生成的时候做了什么。

Watcher

又来到了我们比较常见的Watcher类的阶段了,这次我们重点关注生成userWatch的过程。

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
 ) {
 this.vm = vm
 if (isRenderWatcher) {
  vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) { // 在 new UserWatcher的时候传入了options,并且options.user = true
  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
 }
 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()
  : ''
 // parse expression for getter
 if (typeof expOrFn === 'function') { 
  this.getter = expOrFn
 } else {
  this.getter = parsePath(expOrFn) // 进入这个逻辑,调用parsePath方法,对getter进行赋值
  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()
 }
}

首先会对这个watcher的属性进行一系列的初始化配置,接着判断expOrFn这个值,对于我们watch的key而言,不是函数所以会执行parsePath函数,该函数定义如下:

/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
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]] // 每次把当前的key值对应的值重新赋值obj
 }
 return obj
 }
}

首先会判断传入的path是否符合预期,如果不符合则直接return,接着讲path根据'.'字符串进行拆分,因为我们传入的watch可能有如下几种形式:

watch: {
	a: () {}
 'formData.a': () {}
}

所以需要对path进行拆分,接下来遍历拆分后的数组,这里返回的函数的参数obj其实就是vm实例,通过vm[segments[i]],就可以最终找到这个watch所对应的属性,最后将obj返回。

constructor () { // 初始化的最后一段逻辑
	this.value = this.lazy // 因为this.lazy为false,所以会执行this.get方法
  ? undefined
  : this.get()
}
  
get () {
 pushTarget(this) // 将当前的watcher实例赋值给 Dep.target
 let value
 const vm = this.vm
 try {
  value = this.getter.call(vm, vm) // 这里的getter就是上文所讲parsePath放回的函数,并将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) { // 如果deep为true,则执行深递归
  traverse(value)
  }
  popTarget() // 将当前watch出栈
  this.cleanupDeps() // 清空依赖收集 这个过程也是尤为重要的,后续我会单独写一篇文章分析。
 }
 return value
 }

对于UserWatcher的初始化过程,我们基本上就分析完了,traverse函数本质就是一个递归函数,逻辑并不复杂,大家可以自行查看。 初始化过程已经分析完,但现在我们好像并不知道watch到底是如何监听data的数据变化的。其实对于UserWatcher的依赖收集,就发生在watcher.get方法中,通过this.getter(parsePath)函数,我们就访问了vm实例上的属性。因为这个时候已经initData,所以会触发对应属性的getter函数,这也是为什么initData会放在initWatch和initComputed函数前面。所以当前的UserWatcher就会被存放进对应属性Dep实例下的subs数组中,如下:

Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
  dep.depend()
  if (childOb) {
   childOb.dep.depend()
   if (Array.isArray(value)) {
   dependArray(value)
   }
  }
  }
  return value
 },
}

前几个篇章我们都提到renderWatcher,就是视图的初始化渲染及更新所用。这个renderWathcer初始化的时机是在我们执行$mount方法的时候,这个时候又会对data上的数据进行了一遍依赖收集,每一个data的key的Dep实例都会将renderWathcer放到自己的subs数组中。如图:

详解Vue中的watch和computed

, 当我们对data上的数据进行修改时,就会触发对应属性的setter函数,进而触发dep.notify(),遍历subs中的每一个watcher,执行watcher.update()函数->watcher.run,renderWathcer的update方法我们就不深究了,不清楚的同学可以参考下我写的Vue数据驱动。 对于我们分析的UserWatcher而言,相关代码如下:

class Watcher {
 constructor () {} //..
 run () {
 if (this.active) { // 用于标示watcher实例有没有注销
  const value = this.get() // 执行get方法
  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) { // UserWatcher
   try {
   this.cb.call(this.vm, value, oldValue) // 执行回调cb,并传入新值和旧值作为参数
   } catch (e) {
   handleError(e, this.vm, `callback for watcher "${this.expression}"`)
   }
  } else {
   this.cb.call(this.vm, value, oldValue)
  }
  }
 }
 }
}

首先会判断这个watcher是否已经注销,如果没有则执行this.get方法,重新获取一次新值,接着比较新值和旧值,如果相同则不继续执行,若不同则执行在初始化时传入的cb回调函数,这里其实就是handler函数。至此,UserWatcher的工作原理就分析完了。接下来我们来继续分析ComputedWatcher,同样的我们找到初始代码

Computed

initComputed

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
 // $flow-disable-line
 const watchers = vm._computedWatchers = Object.create(null) // 用来存放computedWatcher的map
 // computed properties are just getters during SSR
 const isSSR = isServerRendering()

 for (const key in computed) {
 const userDef = computed[key]
 const getter = typeof userDef === 'function' ? userDef : userDef.get
 if (process.env.NODE_ENV !== 'production' && getter == null) {
  warn(
  `Getter is missing for computed property "${key}".`,
  vm
  )
 }

 if (!isSSR) { // 不是服务端渲染
  // create internal watcher for the computed property.
  watchers[key] = new Watcher( // 执行new Watcher
  vm,
  getter || noop,
  noop,
  computedWatcherOptions { lazy: true }
  )
 }

 // component-defined computed properties are already defined on the
 // component prototype. We only need to define computed properties defined
 // at instantiation here.
 if (!(key in vm)) { 
 // 会在vm的原型上去查找computed对应的key值存不存在,如果不存在则执行defineComputed,存在的话则退出,
 // 这个地方其实是Vue精心设计的
 // 比如说一个组件在好几个文件中都引用了,如果不将computed
  defineComputed(vm, key, userDef)
 } else if (process.env.NODE_ENV !== 'production') {
  if (key in vm.$data) {
  warn(`The computed property "${key}" is already defined in data.`, vm)
  } else if (vm.$options.props && key in vm.$options.props) {
  warn(`The computed property "${key}" is already defined as a prop.`, vm)
  }
 }
 }
}

defineComputed

new Watcher的逻辑我们先放一边,我们先关注一下defineComputed这个函数到底做了什么

export function defineComputed (
 target: any,
 key: string,
 userDef: Object | Function
) {
 const shouldCache = !isServerRendering()
 if (typeof userDef === 'function') { // 分支1
 sharedPropertyDefinition.get = shouldCache
  ? createComputedGetter(key)
  : createGetterInvoker(userDef)
 sharedPropertyDefinition.set = noop
 } else {
 sharedPropertyDefinition.get = userDef.get
  ? shouldCache && userDef.cache !== false
  ? createComputedGetter(key)
  : createGetterInvoker(userDef.get)
  : noop
 sharedPropertyDefinition.set = userDef.set || noop
 }
 if (process.env.NODE_ENV !== 'production' &&
  sharedPropertyDefinition.set === noop) {
 sharedPropertyDefinition.set = function () {
  warn(
  `Computed property "${key}" was assigned to but it has no setter.`,
  this
  )
 }
 }
 Object.defineProperty(target, key, sharedPropertyDefinition)
}

这个函数本质也是调用Object.defineProperty来改写computed的key值对应的getter函数和setter函数,当访问到key的时候,就会触发其对应的getter函数,对于大部分情况下,我们会走到分支1,对于不是服务端渲染而言,sharedPropertyDefinition.get会被createComputedGetter(key)赋值,set会被赋值为一个空函数。

createComputedGetter

function createComputedGetter (key) {
 return function computedGetter () {
 const watcher = this._computedWatchers && this._computedWatchers[key] // 就是上文中new Watcher()
 if (watcher) {
  if (watcher.dirty) {
  watcher.evaluate()
  }
  if (Dep.target) {
  watcher.depend()
  }
  return watcher.value
 }
 }
}

可以看到createComputedGetter(key)其实会返回一个computedGetter函数,也就是说在执行render函数时,访问到这个vm[key]对应的computed的时候会触发getter函数,而这个getter函数就是computedGetter。

<template>
	<div>{{ message }}</div>
</template>
export default {
	data () {
 	return {
  	a: 1,
   b: 2
  }
 },
 computed: {
 	message () { // 这里的函数名message就是所谓的key
  	return this.a + this.b
  }
 }
}

以上代码为例子,来一步步解析computedGetter函数。 首先我们需要先获取到key对应的watcher.

const watcher = this._computedWatchers && this._computedWatchers[key]

而这里的watcher就是在initComputed函数中所生成的。

if (!isSSR) { // 不是服务端渲染
  // create internal watcher for the computed property.
  watchers[key] = new Watcher( // 执行new Watcher
  vm,
  getter || noop,
  noop,
  computedWatcherOptions { lazy: true }
  )
 }

我们来看看computedWatcher的初始化过程,我们还是接着来继续回顾一下Watcher类相关代码

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
 ) {
 this.vm = vm
 if (isRenderWatcher) {
  vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.lazy = !!options.lazy // lazy = 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 // uid for batching
 this.active = true
 this.dirty = this.lazy // for lazy watchers this.dirty = true 这里把this.dirty设置为true
 this.deps = []
 this.newDeps = []
 this.depIds = new Set()
 this.newDepIds = new Set()
 this.expression = process.env.NODE_ENV !== 'production'
  ? expOrFn.toString()
  : ''
 // parse expression for getter
 if (typeof expOrFn === 'function') { // 走到这一步
  this.getter = expOrFn
 } else {
  // ..
 }
 this.value = this.lazy // 一开始不执行this.get()函数 直接返回undefined
  ? undefined
  : this.get()
 }

紧接着回到computedGetter函数中,执行剩下的逻辑

if (watcher) {
 if (watcher.dirty) {
 watcher.evaluate()
 }
 if (Dep.target) {
 watcher.depend()
 }
 return watcher.value
}

首先判断watcher是否存在,如果存在则执行以下操作

  • 判断watcher.dirty是否为true,如果为true,则执行watcher.evaluate
  • 判断当前Dep.target是否存在,存在则执行watcher.depend
  • 最后返回watcher.value

在computedWatcher初始化的时候,由于传入的options.lazy为true,所以相应的watcher.diry也为true,当我们在执行render函数的时候,访问到message,触发了computedGetter,所以会执行watcher.evaluate。

evaluate () {
 this.value = this.get() // 这里的get() 就是vm['message'] 返回就是this.a + this.b的和
 this.dirty = false // 将dirty置为false
}

同时这个时候由于访问vm上的a属性和b属性,所以会触发a和b的getter函数,这样就会把当前这个computedWatcher加入到了a和b对应的Dpe实例下的subs数组中了。如图:

详解Vue中的watch和computed

接着当前的Dep.target毫无疑问就是renderWatcher了,并且也是存在的,所以就执行了watcher.depend()

depend () {
 let i = this.deps.length 
 while (i--) {
 this.deps[i].depend()
 }
}

对于当前的message computedWatcher而言,this.deps其实就是a和b两个属性对应的Dep实例,接着遍历整个deps,对每一个dep就进行depend()操作,也就是每一个Dep实例把当前的Dep.target(renderWatcher都加入到各自的subs中,如图:

详解Vue中的watch和computed

所以这个时候,一旦你修改了a和b的其中一个值,都会触发setter函数->dep.notify()->watcher.update,代码如下:

update () {
 /* istanbul ignore else */
 if (this.lazy) {
 this.dirty = true
 } else if (this.sync) {
 this.run()
 } else {
 queueWatcher(this)
 }
}

总结

其实不管是watch还是computed本质上都是通过watcher来实现,只不过它们的依赖收集的时机会有所不同。就使用场景而言,computed多用于一个值依赖于其他响应式数据,而watch主要用于监听响应式数据,在进行所需的逻辑操作!大家可以通过单步调试的方法,一步步调试,能更好地加深理解。

以上就是详解Vue中的watch和computed的详细内容,更多关于Vue watch和computed的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
ExtJS GridPanel 根据条件改变字体颜色
Mar 08 Javascript
动态加载图片路径 保持JavaScript控件的相对独立性
Sep 06 Javascript
Javascript base64编码实现代码
Dec 02 Javascript
JS小功能(onmouseover实现选择月份)实例代码
Nov 28 Javascript
javascript模拟post提交隐藏地址栏的参数
Sep 03 Javascript
Angular ng-repeat指令实例以及扩展部分
Dec 26 Javascript
js阻止默认右键的下拉菜单方法
Jan 02 Javascript
Vue SSR 组件加载问题
May 02 Javascript
Vue实现简易翻页效果源码分享
Nov 08 Javascript
JavaScript两种计时器的实例讲解
Jan 31 Javascript
django js 实现表格动态标序号的实例代码
Jul 12 Javascript
js判断一个对象是数组(函数)的方法实例
Dec 19 Javascript
vue-axios同时请求多个接口 等所有接口全部加载完成再处理操作
Nov 09 #Javascript
解决vue 使用axios.all()方法发起多个请求控制台报错的问题
Nov 09 #Javascript
Echarts在Taro微信小程序开发中的踩坑记录
Nov 09 #Javascript
Vue路由权限控制解析
Nov 09 #Javascript
在vue项目中promise解决回调地狱和并发请求的问题
Nov 09 #Javascript
vue 中的动态传参和query传参操作
Nov 09 #Javascript
你不知道的SpringBoot与Vue部署解决方案
Nov 09 #Javascript
You might like
php仿ZOL分页类代码
2008/10/02 PHP
PHP的范围解析操作符(::)的含义分析说明
2011/07/03 PHP
PHP+Mysql+jQuery文件下载次数统计实例讲解
2015/10/10 PHP
详解php中空字符串和0之间的关系
2016/10/23 PHP
深入解析PHP中SESSION反序列化机制
2017/03/01 PHP
通过jquery的$.getJSON做一个跨域ajax请求试验
2011/05/03 Javascript
Extjs407 getValue()和getRawValue()区别介绍
2013/05/21 Javascript
jquery对象和DOM对象的区别介绍
2013/08/09 Javascript
jquerydom对象的事件隐藏显示和对象数组示例
2013/12/10 Javascript
学习JavaScript设计模式之迭代器模式
2016/01/19 Javascript
JS中type=&quot;button&quot;和type=&quot;submit&quot;的区别
2017/07/04 Javascript
使用JavaScript实现链表的数据结构的代码
2017/08/02 Javascript
详解微信小程序Page中data数据操作和函数调用
2017/09/27 Javascript
使用Vue开发动态刷新Echarts组件的教程详解
2018/03/22 Javascript
JavaScript相等运算符的九条规则示例详解
2019/10/20 Javascript
[00:30]塑造者的传承礼包-戴泽“暗影之焰”套装展示视频
2014/04/04 DOTA
[00:37]DOTA2上海特级锦标赛 OG战队宣传片
2016/03/03 DOTA
在Python中测试访问同一数据的竞争条件的方法
2015/04/23 Python
python日志记录模块实例及改进
2017/02/12 Python
浅谈python 线程池threadpool之实现
2017/11/17 Python
详解Python 爬取13个旅游城市,告诉你五一大家最爱去哪玩?
2019/05/07 Python
python3中使用__slots__限定实例属性操作分析
2020/02/14 Python
HTML5 Canvas实现360度全景图的示例代码
2018/01/29 HTML / CSS
HTML5 input新增type属性color颜色拾取器的实例代码
2018/08/27 HTML / CSS
基本款天堂:Everlane
2017/05/13 全球购物
HQhair美国/加拿大:英国化妆品、美容及美发产品商城
2019/04/15 全球购物
会计毕业生自荐信
2013/11/21 职场文书
网络宣传方案
2014/03/15 职场文书
个人收入证明模板
2014/09/18 职场文书
2015年办公室文秘工作总结
2015/04/30 职场文书
《烈火英雄》观后感:致敬和平时代的英雄
2019/11/11 职场文书
CSS3 制作的悬停缩放特效
2021/04/13 HTML / CSS
WebWorker 封装 JavaScript 沙箱详情
2021/11/02 Javascript
《堡垒之夜》联动《刺客信条》 4月7日正式上线
2022/04/06 其他游戏
Nginx的gzip相关介绍
2022/05/11 Servers
服务器间如何实现文件共享
2022/05/20 Servers