Vue中computed和watch的区别
作者:Govi 时间:2024-05-29 22:22:50
前言??
在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中执行
来源:https://juejin.cn/post/7234063541450899513