如何理解vue的computed?

简介: 这道考察computed属性的题蛮有意思的。不仅仅考察了computed,而且还考察了vue的依赖收集以及脏检查。

image.png


computed : {
    foo() {
        if(this.a>0){ return this.a}
        else { return this.b + this.c }
    }
}
data() {
    a: 1,
    b: 1,
    c: 1,
}


众所周知,首次a,b,c均为1时,foo()返回值为1。

以foo()返回值为1作为起始态,独立的执行下面以下3个操作,vue会如何计算foo呢?

  • 如果此时this.a = 0,foo()如何计算?
  • 如果此时this.b = 2,foo()如何计算?
  • 如果a的初始值为-1,执行this.a = 1,foo()如何计算?

目录

  • 执行表现
  • 源码分析
  • createComputedGetter
  • 关键的watcher.js
  • 关键的dep.js
  • 基于源码分析拆解执行表现
  • 初始化a,b,c均为1时,foo()如何计算?
  • 如果此时this.a = 0,foo()如何计算?
  • 如果此时this.b = 2,foo()如何计算?
  • 如果a的初始值为-1,执行this.a = 1,foo()如何计算?
  • 一句话总结


执行表现


  • 如果此时this.a = 0,foo()如何计算?
  • 如果此时this.b = 2,foo()如何计算?
  • 如果a的初始值为-1,执行this.a = 1,foo()如何计算?


如果此时this.a = 0,foo()如何计算?


foo()的返回值会从this.a变为this.b+this.c,2。

vue会重新执行一遍evaluate,得到返回值this.b+this.c。


如果此时this.b = 2,foo()如何计算?


foo()的返回值仍旧为this.a,1。

vue会跳过evaluate的阶段,直接得到返回值this.a。


如果a的初始值为-1,执行this.a = 1,foo()如何计算?


foo()的返回值会从this.b+this.c变为this.a。

vue会重新执行一遍evaluate,得到返回值this.a。

为什么会是这样的呢?是否执行evaluate的条件是什么?

为什么a的初始值为-1了也可以重新evaluate?


源码分析


对于this.b = 2,vue跳过evaluate阶段,直接得到返回值this.a,是如何优化的呢?

下面我们来看源码:


源码地址:state.js

computed相关的有三个非常重要的函数:

  • createComputedGetter(脏检查,依赖收集)
  • 关键的watcher.js(Watcher)
  • 关键的dep.js(依赖)

先来看看最最核心的代码

// 脏检查, 执行计算
 if (watcher.dirty) {
    watcher.evaluate()
  }
 // Dep更新依赖
 if (Dep.target) {
    watcher.depend()
 }

下面再看具体的源码


createComputedGetter


  • 脏检查, 重新计算 对__computedWatcher中的具体的属性做检查
  • Dep更新依赖


function createComputedGetter (key) {
return function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
   // 脏检查, 执行计算
    if (watcher.dirty) {
      watcher.evaluate()
    }
   // Dep更新依赖
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}
}

关键的watcher.js


  • get 收集依赖
  • udpate 更新dirty为true,从而可以重新evaluate
  • evaluate 重新获得computed属性的值
  • depend 通知dep收集依赖
  • addDep watcher订阅依赖
export default class Watcher {
  lazy: boolean;
  dirty: boolean;
  constructor (
  ) {
    this.dirty = this.lazy // for lazy watchers,dirty用于懒监听
    this.value = this.lazy? undefined: this.get() // Dep的target设置为foo watcher
  }
  // Evaluate the getter, and re-collect dependencies.
  get () {
    pushTarget(this)
    value = this.getter.call(vm, vm)
    return value;
  }
  update () {
    if (this.lazy) {
      this.dirty = true
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  addDep (dep: Dep) {
    const id = dep.id
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
  }
}

关键的dep.js

  • addSub:添加订阅
  • depend: 添加依赖
  • notify:通知更新
export default class Dep {
constructor () {
  this.subs = []
}
addSub (sub: Watcher) {
  this.subs.push(sub)
}
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
notify () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
}
Dep.target = null


基于源码分析拆解执行表现


  • 初始化a,b,c均为1时,foo()如何计算?
  • 如果此时this.a = 0,foo()如何计算?
  • 如果此时this.b = 2,foo()如何计算?
  • 如果a的初始值为-1,执行this.a = 1,foo()如何计算?
computed : {
  foo() {
      if(this.a>0){ return this.a}
      else { return this.b + this.c }
  }
}
data() {
  a: 1,
  b: 1,
  c: 1,
}
created(){ this.b = 2; }


初始化a,b,c均为1时,foo()如何计算?


  • 初始化watcher
  • 创建getter得到value并将dirty置为false
  • watcher帮助dep收集依赖,收集的是this.a
  • 依赖收集图


初始化watcher


_computedWatchers:{
    foo: Watcher {(vm, getter, null, { lazy: true })}
}
// watcher
Watcher: { lazy: true, dirty: true, value: undefined, deps:[] }
创建getter得到value并将dirty置为false


// Watcher: { lazy: true, dirty: true, value: undefined }
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
     // 脏检查, 执行计算
      if (watcher.dirty) {
        watcher.evaluate() // 得到value,dirty置为false
      }
     // 返回this.a 1
      return watcher.value
}
// watcher.evaluate() 拆解
evaluate () {
    // 从foo的getter get()得到value:this.a 1
    this.value = this.get()
   // 将dirty变为false
    this.dirty = false
}

 

执行完毕后,结果为Watcher { lazy: true, dirty: false, value: this.a, deps: [Dep a]}


watcher帮助dep收集依赖
if (watcher) {
     // Dep更新依赖
      if (Dep.target) {
        watcher.depend()
      }
}
// watcher.depend() 拆解
depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
}
// dep.depend()拆解
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}
// watcher.addDep拆解
  addDep (dep: Dep) {
    const id = dep.id
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
  }
// dep.addSub()拆解
addSub (sub: Watcher) {
    this.subs.push(sub)
}

最终结果为:


计算属性foo仅仅收集了this.a作为dep。没有收集b和c。

Watcher { lazy: true, dirty: false, value: this.a , deps: [Dep a]}

依赖收集图(dirty为false)

deps: [Dep a(1)]


image.png


如果此时this.a = 0,foo()如何计算?


  • 执行computedGetter触发watcher.evaluate()
  • 在收集了a的基础上,再次收集到b和c的依赖
  • 依赖收集图


当我们执行this.a = 0时,a的setter发出依赖更新,getter执行更新,dirty由false变为true。


由于dirty为true,所以执行evaluate,得到foo()的返回值this.b+this.c。

   // 发出依赖更新
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  // dirty由false变为true
  update () {
    if (this.lazy) {
      this.dirty = true
    }
  }
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
     // 脏检查, 执行计算
      if (watcher.dirty) {
        // 执行evaluate
        watcher.evaluate()
      }
     // Dep更新依赖
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  get () {
    pushTarget(this)
    value = this.getter.call(vm, vm)
    return value;
  }
  // 收集b和c的依赖
  if (Dep.target) {
        watcher.depend()
  }


最终收集到的依赖为Watcher: { lazy: true, dirty: true, value: this.b+this.c , deps: [Dep a, Dep b, Dep c]}

依赖收集图(dirty为true)

deps: [Dep a(1), Dep b(2), Dep c(2)]


image.png


如果此时this.b = 2,foo()如何计算?

  • 执行computedGetter不会触发watcher.evaluate()
  • 依赖收集图
执行computedGetter不会触发watcher.evaluate()

为什么执行computedGetter不会触发watcher.evaluate()?

因为仅收集了this.a的依赖

当我们执行this.b = 2时,b的setter发出依赖更新,getter执行更新。

但是,由于我们初始化的条件仅仅将this.a作为计算属性foo的依赖,所以不会有任何变化。


// Watcher { lazy: true, dirty: false, value: this.a,deps: [Dep a(1) }
return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
     // 此时watcher的dirty为false
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 返回this.a的值 1
      return watcher.value
    }
}


依赖收集图(dirty为false)

deps: [Dep a(1)]


image.png


如果a的初始值为-1,执行this.a = 1,foo()如何计算?


  • 每次get都会被收集到依赖中,收集a,b,c依赖到deps
  • 由于this.a的依赖被收集到,因此可以直接通过this.a = 0触发更新
  • 依赖收集图

源码get()的注释:

Evaluate the getter, and re-collect dependencies.


computed : {
    foo() {
       // a的get()触发,收集到deps
        if(this.a>0){ return this.a}
       // b和c的get()触发,收集到deps
        else { return this.b + this.c }
    }
}
data() {
    a: -1,
    b: 1,
    c: 1,
}


如何收集的?

get () {
    pushTarget(this)
    value = this.getter.call(vm, vm)
}

此时再触发this.a=1,由于this.a的依赖被收集到,因此可以直接触发更新。

最终返回1。


依赖收集图(dirty为true)


deps: [Dep a(1), Dep a(2), Dep a(2)]


image.png


一句话总结


一个computed属性中,每个类似this.foo的调用,都会在get()中重新收集依赖。当依赖收集大于一次(不是一个)时,视为脏(dirty)计算属性,需要重新 evaluate再取值。对于干净的计算属性,不需重新执行evaluate,vue直接取值即可。



相关文章
|
7天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
7天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
27 9
|
6天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
5天前
|
JavaScript
如何在 Vue 中使用具名插槽
【10月更文挑战第25天】通过使用具名插槽,你可以更好地组织和定制组件的模板结构,使组件更具灵活性和可复用性。同时,具名插槽也有助于提高代码的可读性和可维护性。
13 2
|
5天前
|
JavaScript
Vue 中的插槽
【10月更文挑战第25天】插槽的使用可以大大提高组件的复用性和灵活性,使你能够根据具体需求在组件中插入不同的内容,同时保持组件的结构和样式的一致性。
11 2
|
5天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
6天前
|
JavaScript 前端开发 UED
vue 提高 tree shaking 的效果
【10月更文挑战第23天】提高 Vue 中 Tree shaking 的效果需要综合考虑多个因素,包括模块的导出和引用方式、打包工具配置、代码结构等。通过不断地优化和调整,可以最大限度地发挥 Tree shaking 的优势,为 Vue 项目带来更好的性能和用户体验。
|
6天前
|
缓存 JavaScript UED
Vue 中异步加载模块的方式
【10月更文挑战第23天】这些异步加载模块的方式各有特点和适用场景,可以根据项目的需求和架构选择合适的方法来实现模块的异步加载,以提高应用的性能和用户体验
|
6天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。
|
7天前
|
JavaScript 搜索推荐 前端开发
Vue SSR 预渲染的广泛应用场景及其优势
【10月更文挑战第23天】Vue SSR 预渲染技术在众多领域都有着广泛的应用价值,可以显著提升网站的性能、用户体验和搜索引擎优化效果。随着技术的不断发展和完善,其应用场景还将不断拓展和深化
21 2