Vue中computed和watch的区别
作者:Govi
前言🤞
在vue项目中我们常常需要用到computed和watch,那么我们究竟在什么场景下使用computed和watch呢?他们之间又有什么区别呢?记录一下!
computed和watch有什么区别?
相同点:(过目一下,下面还会更新)
- 本质上都是一个watcher实例,它们都通过响应式系统与数据,页面建立通信
- 它们都是以Vue的依赖追踪机制为基础的
computed
简而言之,它的作用就是自动计算我们定义在函数内的“公式”
data() { return { num1: 1, num2: 2 }; }, computed: { total() { return this.num1 * this.num2; } }
在这个场景下,当this.num1或者this.num2变化时,这个total的值也会随之变化,为什么呢?
## 计算属性实现:
由computed是一个函数可以看出,它应该也有一个初始化函数 initComputed来对它进行初始化。
- 从vue源码可以看出在initState函数中对computed进行初始化,往下看
- 在initComputed函数中,有两个参数,vm为vue实例,computed就是我们所定义的computed
具体实现逻辑就不具体解析了,从上面源码中可以发现,initComputed函数会遍历我们定义的computed对象,然后给每一个值绑定一个watcher实例
Watcher实例是响应式系统中负责监听数据变化的角色
计算属性执行的时候就会被访问到,this.num1和this.num2在Data初始化的时候就被定义成响应式数据了,它们内部会有一个Dep实例,Dep实例就会把这个计算属性watcher放到自己的sub数组内,往后如果子级更新了,就会通知数组内的watcher实例更新
再看回源码
const computedWatcherOptions = { lazy: true } // vm: 组件实例 computed 组件内的 计算属性对象 function initComputed (vm: Component, computed: Object) { // 遍历所有的计算属性 for (const key in computed) { // 用户定义的 computed const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) defineComputed(vm, key, userDef) }
可以看出在watcher实例在刚被创建时就往
ComputedWatcherOptions
, 传了{ lazy: true }
, 即意味着它不会立即执行我们定义的计算属性函数,这也意味着它是一个懒计算的功能(标记一下)说到这,就能基本了解了计算watcher实例在计算属性执行流程的作用了,即初始化的过程,那么计算属性是怎么执行的?
从上面的源码可以看出最下面还有一个defineComputed函数,它到底是干嘛的?其实它是vue中用来判断computed中的key是否已经在实例中定义过,如果未定义,则执行defineComputed函数
来看一下defineComputed函数
- 可以看出这里截取了两个函数,defineComputed和createComputedGetter两个函数
首先说说defineComputed函数
- 它会判断是否为服务器渲染,如果为服务器渲染则将计算属性的get、set定义为用户定义get、set;怎么理解?如果非服务器渲染的话则在定义get属性的时候并没有直接赋值用户函数,而是返回一个新的函数computedGetter
- 这里会判断userDef也就是用户定义计算属性key对应的value值是否为函数,如果为函数的话,则将get定义为用户函数,set赋值为一个空函数noop;如果不为函数(对象)则分别取get、set字段赋值
- 在非服务端渲染中计算属性的get属性为computedGetter函数,在每次计算属性触发get属性时,都会从实例的_computedWatchers(在initComputed已初始化)计算属性的watcher对象中获取get函数(用户定义函数)
- 至此,计算属性的初始化就结束了,最终会把当前key定义到vue实例上,也就是可以this.computedKey可以获取到的原因
细心的同学可能发现了,在上述源码中还有一行代码 :Object.defineProperty(target, key, sharedPropertyDefinition),它就是我接下来要说的defineComputed函数做的第二件事(第一件事就是上面的操作)。当访问一次计算属性的key 就会触发一次 sharedPropertyDefinition(我们自定义的函数),对computed做了一次劫持,Target可以理解为this,从上面源码可以看出,每次使用计算属性,都会执行一次computedGetter,跟我们一开始的DEMO一样,它就会执行我们定义的函数,具体怎么实现?
function computedGetter () { // 拿到 上述 创建的 watcher 实例 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 首次执行的时候 dirty 基于 lazy 所以是true if (watcher.dirty) { // 这个方法会执行一次计算 // dirty 设置为 false // 这个函数执行完毕后, 当前 计算watcher就会推出 watcher.evaluate() } // 如果当前激活的渲染watcher存在 if (Dep.target) { /** * evaluate后求值的同时, 如果当前 渲染watcher 存在, * 则通知当前的收集了 计算watcher 的 dep 收集当前的 渲染watcher * * 为什么要这么做? * 假设这个计算属性是在模板中被使用的, 并且渲染watcher没有被对应的dep收集 * 那派发更新的时候, 计算属性依赖的值发生改变, 而当前渲染watcher不被更新 * 就会出现, 页面中的计算属性值没有发生改变的情况. * * 本质上计算属性所依赖的dep, 也可以看做这个属性值本身的dep实例. */ watcher.depend() } return watcher.value } }
综上所述,更加证实了文章开头所说的计算属性带有“懒计算”的功能,为什么呢?回看上面的代码中的watcher.dirty,在**
计算watcher
实例化的时候,一开始watcher.dirty会被设置为true**,这样一说,上面所说的逻辑好像能走通了。走到这里会执行watcher的evaluate(),即求值,this.get()简单理解为执行我们定义的计算属性函数就可以了。
evaluate () { this.value = this.get() this.dirty = false }
this.dirty
这时候就被变成
false既然这样,我们是不是可以理解为当this.dirty为false时就不会执行这个函数。Vue为什么这样做? 当然是觉得, 它依赖的值没有变化, 就没有计算的必要啦
那么问题来了,说了这么久,我们只看到了将this.dirty设为false,什么时候设为true呢?来看一下响应式系统的set部分代码
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } // 通知它的订阅者更新 dep.notify() }
这段代码只做两件事:
1.如果新值和旧值一致,则无需做任何事。
2.如果新值和旧值不一致,则通知这个数据下的订阅者,也就是watcher实例更新
Notity方法就是遍历一下它的数组,然后执行数组里每个watcher的update方法
update () { /* istanbul ignore else */ if (this.lazy) { // 假设当前 发布者 通知 值被重新 set // 则把 dirty 设置为 true 当computed 被使用的时候 就可以重新调用计算 // 渲染wacher 执行完毕 堆出后, 会轮到当前的渲染watcher执行update // 此时就会去执行queueWatcher(this), 再重新执行 组件渲染时候 // 会用到计算属性, 在这时因为 dirty 为 true 所以能重新求值 // dirty就像一个阀门, 用于判断是否应该重新计算 this.dirty = true } }
- 就在这里,
**dirty**
被重新设置为了**true
**. - 总结一下dirty的流程:
一开始dirty为true,一旦执行了一次计算,就会设置为false,然后当它定义的函数内部依赖的值发生了变化,则这个值就会重新变为true。怎么理解?就拿上面的this.num1和this.num2来说,当二者其中一个变化了,dirty的值就变为true。
- 说了这么久dirty,那它到底有什么作用?简而言之,它就是用来记录我们依赖的值有没有变,如果变了就重新计算一下值,如果没变,那就返回以前的值。就像一个懒加载的理念,这也是计算属性缓存的一种方式。有聪明的同学又会问了,我们好像一直在让dirty变成true |false,好像实现逻辑完全跟缓存搭不着边,也完全没有涉及到计算属性函数的执行呀?那我们回头看看computedGetter函数
function computedGetter () { // 拿到 上述 创建的 watcher 实例 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 首次执行的时候 dirty 基于 lazy 所以是true if (watcher.dirty) { // 这个方法会执行一次计算 // dirty 设置为 false // 这个函数执行完毕后, 当前 计算watcher就会推出 watcher.evaluate() } // 如果当前激活的渲染watcher存在 if (Dep.target) { /** * evaluate后求值的同时, 如果当前 渲染watcher 存在, * 则通知当前的收集了 计算watcher 的 dep 收集当前的 渲染watcher * * 为什么要这么做? * 假设这个计算属性是在模板中被使用的, 并且渲染watcher没有被对应的dep收集 * 那派发更新的时候, 计算属性依赖的值发生改变, 而当前渲染watcher不被更新 * 就会出现, 页面中的计算属性值没有发生改变的情况. * * 本质上计算属性所依赖的dep, 也可以看做这个属性值本身的dep实例. */ watcher.depend() } return watcher.value } }
这里有一段
Dep.target
的判断逻辑. 这是什么意思呢.Dep.target
是当前正在渲染组件
. 它代指的是你定义的组件, 它也是一个**watcher
**, 我们一般称之为**渲染watcher**
.计算属性watcher
, 被通知更新的时候, 会改变**dirty
的值. 而渲染watcher
**被通知更新的时候, 它就会更新一次页面.显然我们现在的问题是, 计算属性的**
dirty
重新变为ture
了, 怎么让页面知道现在要重新刷新**了呢?通过**
watcher.depend()
** 这个方法会通知当前数据的**Dep实例
去收集我们的渲染watcher
. 将其收集起来.当数据发生变化的时候, 首先通知计算watcher
更改drity
值, 然后通知渲染watcher
更新页面.渲染watcher更新
页面的时候, 如果在页面的HTML结果中我们用到了total
这个属性. 就会触发它对应的computedGetter
方法. 也就是执行上面这部分代码. 这时候drity
为ture
, 就能如期执行watcher.evaluate()
**方法了。至此,computed属性的逻辑已经完毕,总结来说就是:computed属性的缓存功能,实际上是通过一个dirty字段作为节流阀实现的,如果需要重新求值,阀门就打开,否则就一直返回原先的值,而无需重新计算。
watch
watch更多充当监控者的角色
- 先看例子,当total发生变化时,handler函数就会被执行。
data() { return { total:99 } }, watch: { count: { hanlder(){ console.log('total改变了') } } }
- 相同道理,在watch初始化的时候,肯定有一个initWatch函数,来初始化我们的监听属性,来到源码
// src/core/instance/state.js function initWatch (vm: Component, watch: Object) { // 遍历我们定义的wathcer for (const key in watch) { const handler = watch[key] if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
- 不难看出,当这个函数拿到我们所定义的watch对象的total对象,然后拿到handler值,当然handler也可以是一个数组,然后传进createWatcher函数中,那么在这个过程中又做了什么呢?接着看
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
- 看得出来,它会解析我们传进来的handler对象,最后调用**$watch**实现监听,当然我们也可以直接通过这个方法实现监听。为什么呢?接着看
Vue.prototype.$watch = function ( expOrFn: string | Function, // 这个可以是 key cb: any, // 待执行的函数 options?: Object // 一些配置 ): Function { const vm: Component = this // 创建一个 watcher 此时的 expOrFn 是监听对象 const watcher = new Watcher(vm, expOrFn, cb, options) return function unwatchFn () { watcher.teardown() } }
- 从代码看的出来,watch函数∗∗是Vue实例原型上的一个方法,那么我们就可以通过∗∗this∗∗的形式去调用它。而∗∗watch函数**是Vue实例原型上的一个方法,那么我们就可以通过**this**的形式去调用它。而**watch函数∗∗是Vue实例原型上的一个方法,那么我们就可以通过∗∗this∗∗的形式去调用它。而∗∗watch属性就实例化了一个watcher对象,然后通过这个watcher实现了监听,这就是为什么watch和computed本质上都是一个watcher对象的原因。那既然它跟computed都是watcher实例,那么本质上都是通过Vue响应式系统实现的监听,那是不容置疑的。好,到这里我们就要想一个问题,total的Dep实例,是什么时候收集这个watcher实例的?回看实例化时的代码
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object )
vm
是组件实例, 也就是我们常用的this
expOrFn
是在我们的Demo
中就是total
, 也就是被监听的属性cb
就是我们的handler函数
if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 如果是一个字符则转为一个 一个 getter 函数 // 这里这么做是为了通过 this.[watcherKey] 的形式 // 能够触发 被监听属性的 依赖收集 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()
这是**
watcher实例化
的时候, 会默认执行的一串代码, 回想一下我们在computed实例化的时候传入的函数, 也是expOrFn
.** 如果是一个函数会被直接赋予. 如果是一个字符串. 则**parsePath
通过创建为一个函数. 大家不需要关注这个函数的行为, 它内部就是执行一次this.[expOrFn]
. 也就是this.total
**最后, 因为**
lazy
是false
. 这个值只有计算属性的时候才会被传true
.所以首次会执行this.get()
**.get
里面则是执行一次getter()
触发响应式到这里监听属性的初始化逻辑就算是完成了, 但是在数据更新的时候, 监听属性的触发还有与计算属性不一样的地方.
监听属性是异步触发的,为什么呢?因为监听属性的执行逻辑和组件的渲染是一样的,他们都会放到一个nextTick函数中,放到下一次Tick中执行
总结
说了这么多关于这两座大山的相关内容,也该来总结一下了。
相同点:
- 本质上都是一个watcher实例,它们都通过响应式系统与数据,页面建立通信,只是行为不同
- 计算属性和监听属性对于新值和旧值一样的赋值操作,都不会做任何变化,不过这一点的实现是在响应式系统完成的。
- 它们都是以Vue的依赖追踪机制为基础的
不同点:
- 计算属性具有“懒计算”功能,只有依赖的值变化了,才允许重新计算,成为"缓存",感觉不够准确。
- 在数据更新时,计算属性的dirty状态会立即改变,而监听属性与组件重新渲染,至少会在下一个"Tick"执行。
#感谢
至此,本篇有关computed和watch属性的相关内容到此就结束啦,有什么补充的可以联系我哦!
以上就是Vue中computed和watch的区别的详细内容,更多关于Vue computed和watch的资料请关注脚本之家其它相关文章!