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进行初始化,往下看

Vue中computed和watch的区别

  • initComputed函数中,有两个参数,vm为vue实例,computed就是我们所定义的computed

Vue中computed和watch的区别

  • 具体实现逻辑就不具体解析了,从上面源码中可以发现,initComputed函数会遍历我们定义的computed对象,然后给每一个值绑定一个watcher实例

Vue中computed和watch的区别

  • 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函数

Vue中computed和watch的区别

  • 可以看出这里截取了两个函数,defineComputedcreateComputedGetter两个函数

首先说说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.dirtyfalse时就不会执行这个函数。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方法就是遍历一下它的数组,然后执行数组里每个watcherupdate方法

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的流程:

一开始dirtytrue,一旦执行了一次计算,就会设置为false,然后当它定义的函数内部依赖的值发生了变化,则这个值就会重新变为true。怎么理解?就拿上面的this.num1this.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方法. 也就是执行上面这部分代码. 这时候drityture, 就能如期执行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函数&lowast;&lowast;是Vue实例原型上的一个方法,那么我们就可以通过&lowast;&lowast;this&lowast;&lowast;的形式去调用它。而&lowast;&lowast;watch函数**是Vue实例原型上的一个方法,那么我们就可以通过**this**的形式去调用它。而**watch函数&lowast;&lowast;是Vue实例原型上的一个方法,那么我们就可以通过&lowast;&lowast;this&lowast;&lowast;的形式去调用它。而&lowast;&lowast;watch属性就实例化了一个watcher对象,然后通过这个watcher实现了监听,这就是为什么watchcomputed本质上都是一个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**

  • 最后, 因为**lazyfalse. 这个值只有计算属性的时候才会被传true.所以首次会执行this.get()**. get里面则是执行一次getter()触发响应式

  • 到这里监听属性的初始化逻辑就算是完成了, 但是在数据更新的时候, 监听属性的触发还有与计算属性不一样的地方.

  • 监听属性是异步触发的,为什么呢?因为监听属性的执行逻辑和组件的渲染是一样的,他们都会放到一个nextTick函数中,放到下一次Tick中执行

来源:https://juejin.cn/post/7234063541450899513

标签:Vue,computed,watch,区别
0
投稿

猜你喜欢

  • vue axios拦截器常用之重复请求取消

    2023-07-02 17:03:21
  • python重试装饰器示例

    2022-07-28 10:03:12
  • Python用20行代码实现完整邮件功能

    2023-04-06 12:20:49
  • Python中模拟enum枚举类型的5种方法分享

    2022-05-04 01:00:10
  • python使用matplotlib绘制折线图的示例代码

    2021-06-25 22:51:18
  • 详解pandas数据合并与重塑(pd.concat篇)

    2023-06-02 00:05:37
  • Python从入门到实战之数据结构篇

    2023-10-16 21:32:30
  • 详解Python3的TFTP文件传输

    2023-06-01 22:29:17
  • Tkinter组件Entry的具体使用

    2023-03-21 00:41:40
  • 提高JavaScript执行效率的23个实用技巧

    2023-08-15 18:38:12
  • SQL Server三种锁定模式的知识讲解

    2024-01-17 19:30:11
  • JavaScript创始人Brendan Eich访谈录

    2008-09-16 12:01:00
  • Python流程控制语句的深入讲解

    2023-12-01 00:06:31
  • PyTorch数据读取的实现示例

    2022-01-31 04:15:48
  • Python和Excel的完美结合的常用操作案例汇总

    2021-12-30 05:15:37
  • Matlab实现新冠病毒传播模拟效果

    2022-01-23 00:01:28
  • 详解在Python程序中使用Cookie的教程

    2021-10-25 17:58:43
  • Vue+Vux实现登录功能

    2024-04-30 10:39:45
  • asp,php,.net使用301重定向方法

    2007-09-26 14:05:00
  • Python中tkinter+MySQL实现增删改查

    2024-01-20 06:49:59
  • asp之家 网络编程 m.aspxhome.com