😭面试官, 我彻底搞懂computed和watch的区别了 您所在的位置:网站首页 Vue实例对象属性和方法 😭面试官, 我彻底搞懂computed和watch的区别了

😭面试官, 我彻底搞懂computed和watch的区别了

2023-04-20 16:59| 来源: 网络整理| 查看: 265

computed 和 watch 有什么异同

相同点:

计算属性和监听属性, 本质上都是一个watcher实例, 它们都通过响应式系统与数据,页面建立通信. 不同点

计算属性带有"懒计算"的功能, 为什么我不说是缓存呢? 后面你就知道了.

监听的逻辑有差异. 这一点从使用时就特别明显, 监听属性是目标值变了,它去执行函数.而计算属性是函数的值变了, 它重新求值.

页面刷新以后, 计算属性会默认立即执行, 而watch属性则需要我们自己配置

其实从表层来看, 计算属性和监听属性似乎就这么点差别了.但是你也明白, 这么回答面试官肯定是远远不够的. 所以我们要从源码层面, 去深究上述两点, 搞清楚他们的下一个不同点: "实现差异"

computed

简单的来说, 它的作用就是, 自动计算我们定义在函数内的"公式"

data() { return { a: 1, b: 1 } } computed: { total() { return this.a + this.b } } 复制代码

这样的场景, 想必你也非常熟, 只要 this.a 或者 this.b 的值发生变化, 这个total值就会变化. 我们这也是我们需要搞明白的.

实现

从这个函数名, 就可以看明白initComputed, 这是初始化计算属性的函数. 它的就是遍历下我们定义的computed对象, 然后从中给每一个值定义一个watcher实例.

watcher实例是响应式系统的中负责监听数据变化的角色. 如果你对Vue2的响应式系统不了解的话,建议你读一下这篇用大白话来聊聊Vue2响应式的原理.

计算属性执行的时候会访问到, this.a 和 this.b. 这时候这两个值因为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) } 复制代码

computedWatcherOptions, 传了{ lazy: true }, 它意味着 watcher实例 在刚被创建的时候, 不会立即执行我们定义的计算属性函数, 这也是和监听属性不一样的地方, 我们后面再来看

到这里我们就明白了计算watcher实例在计算属性执行流程的作用了, 也就是初始化的过程. 接下来我们来看计算属性是怎么执行的.

仔细看上方代码, 最下面有个defineComputed, 他用来定义一个计算属性, 内部做了两件事情, 拿到我们定义的函数. (为了不让流程复杂, 我们只看DEMO的这种定义方式), 然后塞到Object.defineProperty. 好让我们访问的时候, 去执行我们定义的函数.

export function defineComputed ( target: any, key: string, userDef: Object | Function ) { sharedPropertyDefinition.get = computedGetter // 当访问一次计算属性的key 就会触发一次 sharedPropertyDefinition // 对computed 做了一次劫持 Object.defineProperty(target, key, sharedPropertyDefinition) } 复制代码

target我们只需理解成this即可. 也就是说我们每次使用计算属性, 就会执行一次computedGetter 就像我们DEMO中的, 我们 this.total 它就会执行我们定义的函数. 怎么实现的呢?

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(), 也就是求值. 这里的get, 你只需要简单的理解为我们定义的计算属性函数就可以了.

evaluate () { this.value = this.get() this.dirty = false } 复制代码

this.dirty 这时候就被变成false

🤔也就是说, 因为它是false, 以后这个函数执行, 就不会再执行这个函数了. Vue为什么要这么做? 当然是觉得, 它依赖的值没有变化, 就没有计算的必要啦

那我们就需要思考下一个问题了, dirty 什么时候又恢复成 true, 显然就是需要重新计算的时候.

这里我们需要来看一下响应式系统的代码. 这里我们只需要看下set部分的逻辑

set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } // 通知它的订阅者更新 dep.notify() } 复制代码

这段代码会做两件事情:

如果新值和旧值一样, 我们就无需做任何事情. 通知这个数据下的订阅者, 也就是watcher实例更新.

notify方法就是遍历一下, 它的数组, 然后执行数组里每个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. 然后当它定义的函数内部依赖的值比如: this.a 和 this.b 发声了变化. 这个值就会重新变为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()方法了

那它重新收集这个渲染watcher吗? 不会的放心吧. 具体怎么处理就留给你自己去看了.

直到这里我们就看完了computed的逻辑了, 好像也不难对吧? 我们来总结一下:

computed属性的缓存功能, 实际上是通过一个dirty字段作为节流阀实现的, 如果需要重新求值, 阀门就打开, 否则就一直返回原先的值, 而无需重新计算.

computed属性和组件一样, 本质上都是一个watcher实例.

watch

watch相对要简单很多了, 在这里我们略过watch属性所有的配置, 仅去考虑他的基本功能

先来看看Demo, 这是一个最简单的例子, 只要count在任何时候下发生变化,handler函数就会被执行.这也是我们目前需要思考的问题.

data() { return { count: 0 } }, watch: { count: { hanlder(){ console.log('count changed') } } } 复制代码

我们直接看源码, 在初始化状态的时候, 有一个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对象中定义的count对象, 然后拿到handler值, 从上面的代码中我们也可以看出来, 实际上handler是一个数组也是ok的.

我们继续看createWatcher函数, 它会解析出我们的配置, 然后调用$watch实现监听. 实际上我们也可以通过这个方法, 函数式实现监听

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) } 复制代码

$watch函数是Vue实例原型上的一个方法, 这也是为什么我们可以通过this的形式去调用它的原因

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属性, 就实例化了一个watcher对象, 然后通过这个watcher实现了监听,这也是和计算属性一样的地方.

既然它也是watcherh实例, 那本质上都是通过Vue的响应式系统实现的监听. 那我们要考虑的就是count的Dep实例, 是什么时候收集了这个watcher实例的

我们先来看一下实例化的时候, 传给watcher构造函数的几个参数.

vm是组件实例, 也就是我们常用的this expOrFn是在我们的Demo中就是count, 也就是被监听的属性 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函数中, 没错就是我们熟悉的API.它可以让我们的同步逻辑, 放到下一个Tick在执行.

如果你不懂nextTick的逻辑, 没关系. 欢迎关注我, 我将会在下篇文章中着重介绍这个API

总结

🤭我相信你看到这里, 也知道面试应该怎么回答这个问题了.这里我们再来总结一下. (这里我就不提应用层面的差异了, 你比我还懂...)

相同点:

计算属性和监听属性以及组件实例, 本质上都是一个Watcher实例.只是行为不同.

计算属性和监听属性对于新值与旧值一样的赋值操作, 都不会做任何变化. 但这点的实现是由响应式系统完成的

不同点:

计算属性具有"懒计算"功能, 只有依赖的值变化了, 才允许重新计算. 称为"缓存", 个人觉得不准确

在数据更新时, 计算属性的dirty状态会立即改变, 而监听属性与组件重新渲染, 至少都会在下一个"tick"执行.

感谢😘

如果觉得文章内容对你有帮助:

❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章 个人公众号: 前端Link 联系作者: linkcyd 😁 往期:

关于正则表达式, 你要了解的都在这里了

React入门指南: 6张脑图带你入门React

面试官: 来, 手写一个promise

原型与原型链: 如何自己实现 call, bind, new

关于正则表达式, 你要了解的都在这里了



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有