详解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 相关文章推荐
javascript 利用Image对象实现的埋点(某处的点击数)统计
Dec 28 Javascript
js自定义事件及事件交互原理概述(二)
Feb 01 Javascript
JS预览图像将本地图片显示到浏览器上
Aug 25 Javascript
JQuery获取表格数据示例代码
May 26 Javascript
jQuery学习笔记之jQuery构建函数的7种方法
Jun 03 Javascript
JavaScript实现的伸展收缩型菜单代码
Oct 14 Javascript
JavaScript脚本库编写的方法
Dec 09 Javascript
JavaScript中匿名函数的用法及优缺点详解
Jun 01 Javascript
jQuery.Sumoselect插件实现下拉复选框效果
Nov 09 jQuery
vue.js+element-ui动态配置菜单的实例
Sep 07 Javascript
javascript实现异形滚动轮播
Nov 28 Javascript
node.js爬虫框架node-crawler初体验
Oct 29 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开发过程中关于继承的使用方法分享
2011/06/17 PHP
php数组去重实例及分析
2013/11/26 PHP
php实现登录tplink WR882N获取IP和重启的方法
2016/07/20 PHP
php中遍历二维数组并以表格的形式输出的方法
2017/01/03 PHP
使用Js让Html中特殊字符不被转义
2013/11/05 Javascript
js获取光标位置和设置文本框光标位置示例代码
2014/01/09 Javascript
bootstrap输入框组代码分享
2016/06/07 Javascript
基于JavaScript实现新增内容滚动播放效果附完整代码
2017/08/24 Javascript
jQuery实现checkbox的简单操作
2017/11/18 jQuery
jQuery插件simplePagination的使用方法示例
2020/04/28 jQuery
vue中实现弹出层动画效果的示例代码
2020/09/25 Javascript
[05:26]2014DOTA2西雅图国际邀请赛 iG战队巡礼
2014/07/07 DOTA
python数据结构之二叉树的遍历实例
2014/04/29 Python
使用python开发vim插件及心得分享
2014/11/04 Python
详解Python中的Numpy、SciPy、MatPlotLib安装与配置
2017/11/17 Python
Python 实现异步调用函数的示例讲解
2018/10/14 Python
浅谈ROC曲线的最佳阈值如何选取
2020/02/28 Python
numpy矩阵数值太多不能全部显示的解决
2020/05/14 Python
详解Python之Scrapy爬虫教程NBA球员数据存放到Mysql数据库
2021/01/24 Python
伦敦高级内衣品牌:Agent Provocateur(大内密探)
2016/08/23 全球购物
老海军美国官网:Old Navy
2016/09/05 全球购物
美国最大的骑马用品零售商:HorseLoverZ
2017/01/12 全球购物
海滩咖啡馆:Beach Cafe
2018/02/02 全球购物
英国网上超市:Ocado
2020/03/05 全球购物
如何打开WebSphere远程debug
2014/10/10 面试题
大学校庆策划书
2014/01/31 职场文书
保险公司年会主持词
2014/03/22 职场文书
道德与公民自我评价
2015/03/09 职场文书
通知函格式范文
2015/04/27 职场文书
惹女朋友生气检讨书
2015/05/06 职场文书
2015年学校心理健康教育工作总结
2015/05/11 职场文书
主持人大赛开场白
2015/05/29 职场文书
《窃读记》教学反思
2016/02/18 职场文书
创业计划书之淘宝网店
2019/10/08 职场文书
javascript拖曳互换div的位置实现示例
2021/06/28 Javascript
Java异常处理try catch的基本用法
2021/12/06 Java/Android