Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

    • UI 框架

      • Angular

      • React

      • Solid

      • Svelte

      • Vue

        • Vue 项目中的 data-v-xxx 是怎么生成的
        • Vue之从零编写一个ContextMenu(右键菜单)插件
        • Vue 第一个组件,浏览器后退无法触发beforeRouteLeave的问题与解决
        • Vue问题记录
        • vue 中 updated 的执行时机
        • 源码解析

          • Vue 源码解析-0.前置准备
          • Vue实例化过程
          • Vue响应式原理
          • Vue响应式之data
          • Vue响应式之props
          • Vue响应式之computed
            • 前言
            • computed 设计
            • 源码分析
            • 总结
          • Vue响应式之watch
          • Vue响应式之异步更新与nextTick
          • Vue响应式之组件更新
      • 框架本质

    • 开发框架

    • 组件库

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 应用框架
  • UI 框架
  • Vue
  • 源码解析
gahing
2019-11-14
目录

Vue响应式之computed

# 前言

以前使用 computed 的时候一直觉得很神奇,怎么做到其中有个值变更, computed 会重新求值的

<div id="el">
</div>

<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
  <div>
    {{a}}
    {{aDouble}}
  </div>
</script>

<script>
  var vm = new Vue({
    el: '#el',
    template: '#demo-template',
    data: { a: 1 },
    computed: {
      // 仅读取
      aDouble: function () {
        return this.a * 2
      },
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当执行 vm.a = 2 时, aDouble 的取值也会变更,此时页面上显示为 2 4

先不看源码,根据前几篇文章的积累,我们自己尝试着设计 computed

# computed 设计

computed 属性可能是一个对象或者方法

aDouble: function () {
  return this.a * 2
},
// or
aDouble: {
  get:function(){
    return this.a * 2
  },
  set:function(val){
    console.log(val)
  }
},
1
2
3
4
5
6
7
8
9
10
11
12

通过 Object.defineProperty 定义 aDouble 的 get/set ,如果传入的是方法,那么该方法就是 get

Object.defineProperty(vm.$computed,'aDouble',{
  get:function(){
    return this.a * 2
  }
})
// or
Object.defineProperty(vm.$computed,'aDouble',{
  get:function(){
    return this.a * 2
  },
  set:function(val){
    console.log(val)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

接着需要对该对象进行响应式处理

defineReactive(vm.$computed,'aDouble')
1

后面调用 aDouble 的 getter 时就会调用原本的 get 方法

不过这里 set 倒是没必要,因为计算属性的变更是根据依赖属性的变更的,调用 set 并不会触发计算属性的变更,这个到时候看源码怎么做

aDouble 自身也是一个 Watcher , 当其中的属性变更,其需要得到通知

new Watcher(
  vm,
  getter,
  noop
)
1
2
3
4
5

实例化 Watcher 时,调用 computed 属性的 getter . 当 getter 触发时,会对里面用到的响应式属性进行访问,由于此时 Dep.target 为该 watcher ,会被这些响应式属性的依赖添加进订阅者列表

也就是说,当 a 更新时,会触发 aDouble 的 watcher 进行更新

最后 computed 属性被作为观察目标,处理同 data 类似,不再赘述

考虑这个场景

d: function () {
  if(this.c > 0){
    return this.a
  } else {
    return this.b
  }
},
1
2
3
4
5
6
7

初始化 Watcher 的时候, c 大于 0,a 的依赖会添加 watcher, 但是 b 没有访问到, b 的依赖也就不会添加 watcher

当 c 值变更 小于 0 时,此时访问到 b ,b 的依赖怎么添加 watcher 另外, a 的依赖也应该移除 watcher

所以实现上,当里面有依赖触发变更时,需要保证 Dep.target 有值,并重新收集依赖

这个留作问题,这个我们稍后看源码是怎么处理的

# 源码分析

· 从 initComputed 入手

// src\core\instance\state.js

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    // 必须要提供 getter
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    // ssr 不进行监听
    if (!isSSR) {
      // 每个计算属性都会创建一个内部 watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        // { lazy: true }
        computedWatcherOptions
      )
    }

    // 组件定义的计算属性已经在组件原型上定义。
    // 我们只需要处理在实例化时定义的计算属性。
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 重名属性警告
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

主要是做了两件事:

  1. 创建内部 watcher 。 由于 lazy = true 因此不会马上调用 get 去收集依赖
  2. 执行 defineComputed(vm, key, userDef)

这里提出了一点,组件的计算属性有挂在原型和实例上的,这个后面再看

我们继续分析 defineComputed

// src\core\instance\state.js

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  // 定义计算属性的 get/set
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // ... 省略:调用未定义 setter 的警告
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

可以看到,这里定义 get/set 不是简单的将原本定义传入 Object.defineProperty

对于 set ,直接调用原本的定义;对于 get 会调用 createComputedGetter (非ssr)

我们看下 createComputedGetter 的处理

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

刚刚说到,创建的 watcher 是 lazy 的,同时 dirty 值与 lazy 一致

dirty 用来判断 watcher 是否需要添加/更新依赖

由于 dirty 为 true,会执行 watcher.evaluate()

evaluate () {
  this.value = this.get()
  this.dirty = false
}
1
2
3
4

触发 get ; get 我们很熟悉了,就是 pushTarget 并执行 getter 后 popTarget

比如我们的例子, getter 为 function (){ return this.a * 2 }

执行 getter 时, watcher 会添加/更新计算属性上使用到的响应式属性( 本例为 a )的依赖,并且 dirty 赋值为 false

watcher 的 value 值即 getter 的执行结果,下次访问该计算属性,只要 dirty 为 false ,就不会重新收集依赖,会返回 watcher 的 value

当依赖变更( 比如 a 的值发生变化)时,会调用 watcher 的 update 方法,

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
1
2
3
4
5
6
7
8
9
10

由于 lazy 为 true ,会将 dirty 值重新赋值 true ,当下次访问计算属性时,就会重新收集依赖

那么计算属性的访问是在哪进行的,为何计算属性的结果马上得到更新?

上面有段代码被我们忽略了

if (Dep.target) {
  watcher.depend()
}
1
2
3

当初始化 Vue 的时候,创建了一个 Watcher w1, w1 调用了 getter 对页面进行解析,此时扫描到了 aDouble ,然后执行了 computedGetter ,所以一开始访问计算属性的时候,Dep.target 栈底为 w1,

此时执行 watcher.depend() ,会将 watcher 的所有依赖(比如这里有 a 的依赖)执行 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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

其结果为 w1 添加了 a 的依赖

所以当 a 变更时,会先后触发 watcher 和 w1 的变更,前者设置了 dirty 为 true,后者调用了 get 导致重新 render (此时 Dep.target 栈底为 w1),render 解析到 aDouble 导致再次执行了 computedGetter ,于是会更新依赖并返回计算结果

整个 computed 的流程如下:

  1. 初始化 computed 属性 aDouble ,该属性带了一个 getter 方法,创建内部 Watcher watcher 并调用 defineComputed
  2. Vue 实例调用 mount 绑定模板,此时会创建一个 Watcher w1,并进行页面模板解析
  3. 页面模板解析,访问到 aDouble,执行 computedGetter,初次执行会调用 Watcher.get 进而调用计算属性的 getter ,于是 watcher 会添加里面用的响应式属性(本例为 a)的依赖,最后设置并返回计算值 watcher.value
  4. 页面模板解析的过程是在 w1 初始化的时候执行的,其实现在 targetStack 栈底为 w1,于是执行 watcher.depend 导致 w1 会添加 watcher 的所有依赖(本例为 a)
  5. ...多次访问 aDouble ,只要依赖不变, aDouble 会直接返回原来的计算值
  6. a 发生变更,由于添加顺序, watcher 和 w1 会被先后触发更新
  7. watcher 会将 dirty 置为 true
  8. w1 会进行页面模板解析(targetStack 栈底为 w1),解析到 aDouble 并触发 computedGetter ,会执行 watcher.evaluate() (重新收集依赖) 和 watcher.depend() (w1 添加 watcher 收集的依赖),最后返回新的计算值。页面模板解析完毕,页面会进行更新

最后,还记得前面遗留了一个问题:组件的计算属性有挂在原型和实例上的

组件的计算属性都会挂载到原型上(目前不知道怎样才能挂载到实例上),于是每次实例化组件只会创建内部 Watcher 不会再次进行 defineComputed ,可能是一种优化吧

# 总结

本来以为 computed 可能会复用 defineReactive ,但是部分逻辑 defineReactive 可能需要修改

yyx 单独使用 defineComputed 的目的应该也是为了职责明确,更加可控。另外,vue 为了做一些优化,导致部分源码跳来跳去实在是难以读懂,好在大框架的设计还是可以的

编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
Vue响应式之props
Vue响应式之watch

← Vue响应式之props Vue响应式之watch→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式