Vue 3源码极客之:`Vue`的`computed`:其内部如何实现`dirty`标记和惰性求值。

各位观众,老铁们,晚上好!今天咱们来聊聊 Vue 3 源码里 computed 的那些事儿,特别是它内部的 dirty 标记和惰性求值机制。保证让大家听完之后,下次面试再遇到这个问题,直接把面试官问到怀疑人生。

咱们先从一个最简单的 computed 例子开始,热热身:

<template>
  <div>
    <p>原始数据: {{ message }}</p>
    <p>计算属性: {{ reversedMessage }}</p>
  </div>
</template>

<script>
import { ref, computed } from 'vue';

export default {
  setup() {
    const message = ref('Hello, Vue!');
    const reversedMessage = computed(() => {
      console.log('计算属性执行了!'); // 观察计算属性是否执行
      return message.value.split('').reverse().join('');
    });

    // 模拟数据改变
    setTimeout(() => {
      message.value = 'Goodbye, Vue!';
    }, 2000);

    return {
      message,
      reversedMessage,
    };
  },
};
</script>

在这个例子里,reversedMessage 是一个 computed 属性,它依赖于 message。大家运行一下这段代码,会发现:

  1. 页面首次渲染时,reversedMessage 的计算函数执行了一次。
  2. 2秒后,message 的值改变了,reversedMessage 的计算函数又执行了一次。

但是,如果没有使用 reversedMessage, 即使 message 改变了, reversedMessage 的计算函数也不会执行。 这就是 computed 的惰性求值。

dirty 标记:computed 背后的“懒人”机制

computed 之所以能实现惰性求值,核心就在于它的 dirty 标记。 简单来说,dirty 标记就是一个布尔值,用来指示计算属性是否“脏了”,需要重新计算。

  • dirty = true: 表示计算属性依赖的数据发生了改变,需要重新计算。
  • dirty = false: 表示计算属性的值是最新的,可以直接返回,不需要重新计算。

让我们用更通俗的语言来描述一下这个过程:

  • computed 对象心里住着一个小人,这个小人负责记录计算属性的值和 dirty 标记。
  • 当页面首次访问 computed 属性时,小人发现 dirty 标记是 true(初始值为 true),于是开始计算属性的值,然后把 dirty 标记设置为 false,并将计算结果缓存起来。
  • 当依赖的数据发生改变时,小人会把 dirty 标记设置为 true
  • 当下一次访问 computed 属性时,小人发现 dirty 标记是 true,于是重新计算属性的值,然后把 dirty 标记设置为 false,并将计算结果缓存起来。如果 dirty 标记是 false,小人就直接把缓存的值返回,不再重新计算。

现在,让我们来深入 Vue 3 源码,看看 computed 内部是如何实现 dirty 标记和惰性求值的。

Vue 3 源码剖析:computed 的实现细节

以下代码是 Vue 3 中 computed 的简化版本,方便大家理解:

import { isRef, track, trigger } from './reactive'; // 假设的响应式系统

class ComputedRefImpl {
  private _value: any;
  private _dirty = true; // 初始值为 true
  private _effect: any;
  public readonly __v_isRef = true;

  constructor(getter, private readonly _setter) {
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true;
        trigger(this, "set", "value"); // 触发更新
      }
    });
  }

  get value() {
    if (this._dirty) {
      this._value = this._effect.run();
      this._dirty = false;
    }
    track(this, "get", "value"); // 追踪依赖
    return this._value;
  }

  set value(newValue) {
    this._setter(newValue);
  }
}

class ReactiveEffect {
    constructor(public fn, public scheduler) {}
    run() {
        return this.fn();
    }
}

function computed(getter, setter) {
  return new ComputedRefImpl(getter, setter);
}

// 假设的响应式系统
function track(target, type, key) {
  console.log(`追踪 ${target} 的 ${key} 属性`);
}

function trigger(target, type, key) {
  console.log(`触发 ${target} 的 ${key} 属性更新`);
}

// 示例用法
const raw = { count: 1 };
const count = {
    get value() {
        return raw.count;
    },
    set value(val) {
        raw.count = val;
    }
};
const doubleCount = computed(() => count.value * 2, (val) => {count.value = val / 2});

console.log(doubleCount.value); // 输出 2,并追踪依赖
count.value = 2;
console.log(doubleCount.value); // 输出 4,并追踪依赖
doubleCount.value = 6;
console.log(count.value) // 输出3

让我们逐行解读这段代码:

  1. ComputedRefImpl: 这是 computed 的核心实现类。

    • _value: 用来缓存计算属性的值。
    • _dirty: dirty 标记,初始值为 true,表示需要重新计算。
    • _effect: 一个 ReactiveEffect 实例,用来管理计算属性的计算函数。
    • __v_isRef: Vue 内部用来判断是否是 ref 对象的标识。
  2. constructor 构造函数:

    • 接收两个参数:getter (计算函数) 和 setter (可选的设置函数)。
    • 创建 ReactiveEffect 实例,并将 getter 作为其计算函数。
    • ReactiveEffect 的第二个参数是一个 scheduler 函数,这个函数会在依赖的数据发生改变时被调用。在这里,scheduler 函数的作用是将 _dirty 标记设置为 true,并触发更新。
  3. get value() 方法: 这是访问 computed 属性时调用的方法。

    • 首先检查 _dirty 标记是否为 true
    • 如果 _dirtytrue,则调用 this._effect.run() 重新计算属性的值,并将结果缓存到 _value 中,然后将 _dirty 设置为 false
    • 调用 track(this, "get", "value") 追踪依赖,以便在依赖的数据发生改变时,能够触发更新。
    • 最后返回缓存的 _value
  4. set value() 方法: 这是设置 computed 属性时调用的方法。

    • 调用 _setter 函数,并将新的值传递给它。
  5. ReactiveEffect: 一个简化的响应式effect类, 里面包含了 getter 函数和 scheduler 函数。

    • run 函数: 执行 getter 函数
  6. computed 函数: 创建并返回 ComputedRefImpl 实例。

流程总结

  1. 初始化: 创建 ComputedRefImpl 实例时,_dirty 标记被设置为 true
  2. 首次访问: 当首次访问 computed 属性时,get value() 方法发现 _dirtytrue,于是调用 _effect.run() 重新计算属性的值,并将 _dirty 设置为 false
  3. 依赖追踪: 在 _effect.run() 执行期间,Vue 的响应式系统会追踪计算属性依赖的数据。
  4. 数据改变: 当依赖的数据发生改变时,ReactiveEffectscheduler 函数会被调用,将 _dirty 标记设置为 true,并触发更新。
  5. 后续访问: 当后续访问 computed 属性时,get value() 方法会根据 _dirty 标记来判断是否需要重新计算属性的值。如果 _dirtyfalse,则直接返回缓存的值。

tracktrigger 的作用

在上面的代码中,tracktrigger 是两个非常重要的函数,它们是 Vue 响应式系统的核心组成部分。

  • track(target, type, key): 用来追踪依赖关系。当访问一个响应式对象的属性时,track 函数会被调用,将当前正在执行的 effect (在这里就是 ReactiveEffect 实例) 添加到该属性的依赖列表中。这样,当该属性的值发生改变时,Vue 就能找到所有依赖于它的 effect,并触发它们重新执行。
  • trigger(target, type, key): 用来触发更新。当一个响应式对象的属性的值发生改变时,trigger 函数会被调用,它会遍历该属性的依赖列表,并依次执行列表中的 effect

表格总结

概念 描述
dirty 一个布尔值,用来指示计算属性是否需要重新计算。true 表示需要重新计算,false 表示不需要重新计算。
惰性求值 计算属性只有在被访问时才会进行计算。如果计算属性的值没有被访问,即使它的依赖数据发生了改变,它也不会重新计算。
track Vue 响应式系统中的一个函数,用来追踪依赖关系。当访问一个响应式对象的属性时,track 函数会被调用,将当前正在执行的 effect 添加到该属性的依赖列表中。
trigger Vue 响应式系统中的一个函数,用来触发更新。当一个响应式对象的属性的值发生改变时,trigger 函数会被调用,它会遍历该属性的依赖列表,并依次执行列表中的 effect
ReactiveEffect 包含一个函数和一个调度器scheduler,当函数依赖的值发生改变时,执行调度器函数。 computed 的实现依赖于 ReactiveEffect

进阶思考

  1. computedsetter: 在上面的例子中,我们只使用了 computedgetter,实际上 computed 还可以接收一个 setter 函数,用来手动设置计算属性的值。如果提供了 setter 函数,那么 computed 就不再是只读的了。
  2. computed 的性能优化: Vue 3 对 computed 进行了很多性能优化,例如:

    • 缓存: 计算属性的值会被缓存起来,只有在依赖的数据发生改变时才会重新计算。
    • 避免不必要的更新: 只有当计算属性的值真正发生改变时,才会触发组件的重新渲染。
  3. watch的区别computed 一般用于依赖多个响应式状态,返回一个新的响应式状态;watch 用于监听一个或多个响应式状态,并在状态改变时执行副作用。

总结

computed 是 Vue 中一个非常重要的概念,它提供了一种声明式的方式来描述派生状态。通过 dirty 标记和惰性求值,computed 能够有效地提高应用的性能。理解 computed 的内部实现原理,能够帮助我们更好地使用 Vue,并写出更高效的代码。

希望今天的分享对大家有所帮助! 如果大家觉得讲得还行,记得点个赞,分享给你的朋友们。 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注