Vue3进阶主题: 从Ref、Reactive到Reactivity

920 阅读7分钟

什么是Reactivity

Vue3的响应式系统基于ES6的Proxy实现,与Vue2的Object.defineProperty的方式相比,具有更高的性能和更好的扩展性。

Reactivity的特点

Vue3中的Reactivity具有以下特点:

  1. 基于ES6的Proxy实现:Vue3的响应式系统基于ES6的Proxy实现,与Vue2的Object.defineProperty的方式相比,具有更高的性能和更好的扩展性。
  2. 支持多种JavaScript数据类型:Vue3中的响应式系统不仅可以处理普通的JavaScript对象,还可以处理数组、Map、Set等JavaScript数据类型,使得数据的状态管理更加高效和灵活。
  3. 懒执行:Vue3中的响应式系统采用了懒执行的方式,只有在数据被访问时才会进行依赖收集,从而避免了不必要的依赖收集和触发视图更新的性能损耗。
  4. 自定义依赖追踪:Vue3中的响应式系统提供了自定义依赖追踪的能力,开发者可以通过tracktrigger API来手动追踪和触发响应式数据的依赖关系。
  5. 批量更新:Vue3中的响应式系统在更新视图时采用了批量更新的方式,即将所有需要更新的操作放在一个队列中,一次性地更新视图,从而减少不必要的DOM操作,提高性能。

第一点和第二点我们已经在上一篇中进行介绍和解析了, 本篇我们来逐个分析剩下的特点:

懒执行

Vue3中的响应式系统采用了懒执行的方式,只有在数据被访问时才会进行依赖收集,从而避免了不必要的依赖收集和触发视图更新的性能损耗。下面是一个示例代码:

import { reactive } from 'vue';

const state = reactive({
  count: 0,
  message: 'Hello Vue3'
})

console.log(state.count); // 不会触发视图更新

而Vue2 的响应式系统是基于 Object.defineProperty 实现的,它会在数据对象被访问时立即执行 getter 函数,而当数据对象发生变化时,会立即执行所有相关的 watcher 回调函数。

自定义依赖追踪(划重点)

Vue3中存在自动依赖追踪和自定义依赖追踪,区别主要在于依赖追踪的范围和方式不同。自动依赖追踪是 Vue3 中默认的依赖追踪方式,它会自动追踪响应式数据的依赖,并在数据变化时更新依赖的数据。而自定义依赖追踪需要手动指定依赖,可以用于特定场景下对性能进行优化。

自动依赖追踪

<template>
  <div>
    <p>count: {{ count }}</p>
    <button @click="increment">increment</button>
  </div>
</template>

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

export default {
  setup() {
    const count = ref(0);
    function increment() {
      count.value++;
    }
    return {
      count,
      increment,
    };
  },
};
</script>

在上面的组件中,我们使用了 ref() 函数将 count 变量转化为响应式数据,并在 increment() 函数中更新了 count 变量的值。此时,Vue3 会自动追踪 count 的依赖,并在 count 变量发生变化时重新渲染视图。

自定义依赖追踪

<template>
  <div>
    <p>count: {{ count }}</p>
    <p>double: {{ double }}</p>
    <button @click="increment">increment</button>
  </div>
</template>

<script>
import { reactive, effect } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: 0,
    });
    effect(() => {
      state.double = state.count * 2;
    });
    function increment() {
      state.count++;
    }
    return {
      count: state.count,
      double: state.double,
      increment,
    };
  },
};
</script>

在上面的组件中,我们使用了 reactive() 函数将 state 对象转化为响应式对象,并使用 effect() 函数手动定义了依赖关系,使得 double 变量依赖于 count 变量的值。在 increment() 函数中,我们更新了 count 变量的值,此时 Vue3 不会自动追踪 double 变量的依赖,而是通过 effect() 函数手动触发 double 变量的更新。

computed函数

在 Vue3 中,computed() 函数实际上就是利用自定义依赖追踪机制实现的。在上个section中我们通过手动调用effect()函数手动追踪double变量,计算属性的实现也是利用了自定义依赖追踪机制。下面是 computed() 函数的源码解析:

function computed<T>(getterOrOptions) {
  // 用于存储当前计算属性的值
  let getter
  let setter

  if (isFunction(getterOrOptions)) {
    // 如果 getterOrOptions 是一个函数,则认为它是一个计算属性的 getter
    getter = getterOrOptions
    setter = NOOP
  } else {
    // 否则认为它是一个对象,从对象中获取 getter 和 setter
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 构造一个响应式的 ref 对象
  const computedRef = computed({
    get: () => {
      // 使用传入的 getter 获取计算属性的值,并将计算属性依赖的响应式对象收集到依赖中
      track(computedRef, TrackOpTypes.GET, 'value')
      return getter()
    },
    set: isFunction(setter) ? (value) => {
      // 如果传入的 setter 是一个函数,则调用该函数更新计算属性的值
      setter(value)
    } : NOOP
  })

  // 返回一个只读的 computedRef 对象
  return computedRef
}

computed() 函数接受一个参数,可以是一个函数或一个包含 getset 方法的对象。如果传入的参数是一个函数,则函数被认为是计算属性的 getter;如果是一个对象,则从中获取 getset 方法。

接下来,computed()函数将传入的 getter 和 setter 包装成一个响应式的计算属性。它首先构造了一个 ref 对象,并在计算属性的 getter 中收集计算属性依赖的响应式对象(即 track)。这确保了当响应式对象发生变化时,计算属性会重新计算自己的值。

如果传入的 setter 是一个函数,则计算属性也具有可写的能力,因此可以使用 setter 方法更新计算属性的值。

最后,computed() 函数返回一个只读的计算属性对象,该对象本质上是一个 ref 对象,它的值是计算属性的计算结果。每当依赖的响应式对象发生变化时,计算属性的值也会重新计算。

批量更新

Vue3中的响应式系统在更新视图时采用了批量更新的方式,即将所有需要更新的操作放在一个队列中,一次性地更新视图,从而减少不必要的DOM操作,提高性能。下面是一个示例代码:

import { ref } from 'vue';

const count = ref(0);

const handleClick = () => {
  count.value++;
  count.value++;
  count.value++;
}

// 多次修改count.value,但只会触发一次视图更新

在这个示例中,我们多次修改了count的值,但只会触发一次视图更新,因为Vue3会将这些修改操作放在一个队列中,一次性地更新视图。

Batcher 函数

在 Vue3 中,批量更新的核心就是 Batcher 函数。Batcher 函数的作用是将多次数据变更操作合并成一次更新操作,以提高更新的效率。下面是 Batcher 函数的源码:

export function queueJob(job: ReactiveEffect): void {
  if (!queue.includes(job)) {
    queue.push(job);
    if (!queueFlushJob) {
      queueFlushJob = queuePostFlushCb(flushJobs);
    }
  }
}

export function queuePostFlushCb(cb: Function): Function {
  const currentFlushPromise = resolvedPromise;
  let resolved = false;
  return () => {
    const resolvedPromise = currentFlushPromise;
    if (!resolved) {
      resolved = true;
      resolvedPromise.then(() => {
        resolved = false;
        cb();
      });
    }
  };
}

let isFlushing = false;
let isFlushPending = false;

export function flushPostFlushCbs(seen?: CountMap): void {
  if (postFlushCbs.length) {
    const cbs = [...new Set(postFlushCbs)];
    postFlushCbs.length = 0;
    if (seen) {
      seen = new Map(seen);
      for (let i = 0; i < cbs.length; i++) {
        checkRecursiveUpdates(seen, cbs[i]);
      }
    }
    for (let i = 0; i < cbs.length; i++) {
      cbs[i]();
    }
  }
}

const getId = (job: ReactiveEffect) => (job.id == null ? Infinity : job.id);

export function flushJobs(seen?: CountMap): void {
  isFlushPending = false;
  isFlushing = true;
  const queueLength = queue.length;
  for (let i = 0; i < queueLength; i++) {
    const job = queue[i];
    if (job) {
      if (__DEV__) {
        checkRecursiveUpdates(seen, job);
      }
      callWithErrorHandling(job, null, ErrorCodes.SCHEDULER);
    }
  }
  flushPostFlushCbs(seen);
  queue.length = 0;
  isFlushing = false;
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen);
  }
  queueFlushJob = null;
}

let queue: ReactiveEffect[] = [];
let queueFlushJob: ReturnType<typeof queuePostFlushCb> | null = null;

export const postFlushCbs: Function[] = []

这段代码中,queueJob 函数用于将待执行的任务 job 推入队列 queue 中。如果当前队列中没有任务,则会通过 queuePostFlushCb 函数注册一个 flushJobs 函数的回调函数,以便在浏览器的下一个事件循环中执行 flushJobs 函数,处理队列中的所有任务。

flushJobs 函数会将队列 queue 中的所有任务逐一执行,并在执行完所有任务后调用 flushPostFlushCbs 函数执行所有注册在 postFlushCbs 数组中的回调函数。执行完任务和回调函数后,会检查队列中是否还有待执行的任务或者后置回调函数,如果有则继续执行 flushJobs 函数,直到队列中的所有任务和回调函数都被处理完毕。

在这个过程中,为了防止在执行任务或回调函数期间有新的任务或回调函数被推入队列,从而打乱了执行顺序,isFlushingisFlushPending 这两个变量用于标记当前是否正在执行任务和是否有任务待执行。

最后,postFlushCbs 数组用于存储需要在 flushJobs 执行完所有任务后执行的回调函数。

和Vue2的差异

  1. 源码实现不同:Vue3 中的批量更新是通过 Batcher 函数和 nextTick 函数实现的,而 Vue2 中则是通过异步更新队列和 Watcher 对象实现的。
  2. 更优秀的性能:Vue3 中的批量更新机制采用了新的 Batcher 函数和 nextTick 函数实现,可以更好地优化更新性能。相比之下,Vue2 中的异步更新队列和 Watcher 对象的实现效率相对较低。
  3. 支持了 sync 标记:在 Vue3 中,可以通过 sync 标记来强制同步执行更新操作,以便及时获取最新的数据。而在 Vue2 中,没有提供类似的功能,需要通过手动调用 $nextTick 函数来实现同步更新操作。

参考链接