Vue.js: Composition API 深入解析与实战

Vue.js: Composition API 深入解析与实战

概念深度:

Options API

首先我们介绍 Vue 3 提出的 Composition API ,我们需要先知道为什么会出现这个?

既然是Vue 3 提出的那么它肯定是针对之前版本出现的可能需要优化的地方。

那么我们不得不提一下 Vue 2 中的 Options API。首先我来介绍一下什么叫 Options API

我们在 Vue 2 中,我们在编写组件的方式就是Options API

Options API的一大特点就是在对应的属性中编写对应的功能模块导致会出现一些问题:

  1. 组件逻辑分散

    • 只要用过 Vue 2 的都知道,在 Options API 中,组件的逻辑被分割成多个选项,如 datacomputedmethodswatch 也包括生命周期钩子。它要求开发者必须把对应的逻辑写入对应的结构内,当然我们也发现如果简单的组件,代码结构还是比较清晰的。

    • 但是对于复杂的组件来说,这种分散的结构有时会使组件变得难以阅读和理解。

  2. 调试困难

    • 由于 Options API的逻辑分布在多个地方,调试时需要跳转到不同的部分查看代码,这增加了调试的难度。

    • 特别是在大型项目中,这可能会导致调试效率降低。

Composition API:

由此 Vue 3 引入了 Composition API,以解决这些问题,并提供了一种更现代、更易于维护的方式来组织和复用组件逻辑。

Vue.js 3 中引入的一种新的编程模型,旨在改善组件逻辑的组织和复用。它允许开发者使用函数式的编程方式来编写组件逻辑,从而提高代码的可读性和可维护性。Composition API 的核心思想是将组件逻辑分解成可复用的小型函数,这些函数可以被多个组件共享。

Composition API的使用方式方法是什么?

Composition API 主要通过在 <script> 标签中定义一个名为 setup() 的函数来实现。这个函数是 Composition API 的入口点,它允许开发者使用 Composition API 提供的一系列功能。

示例:

下面是一个简单的示例

import { ref, onMounted } from 'vue';

export default {
  setup() {
    // 定义一个响应式的 ref
    const count = ref(0);

    // 定义一个方法
    function increment() {
      count.value++;
    }

    // 在组件挂载后执行的操作
    onMounted(() => {
      console.log('组件已挂载!');
    });

    // 返回值会被添加到组件的 props 中
    return {
      count,
      increment
    };
  }
};

Composition API的关键模块有什么?

Composition API 包括以下几个关键模块:

Ref

ref: 创建一个响应式的引用。

ref.value 属性用于获取或设置值。

例如:const count = ref(0);

Reactive

reactive: 创建一个响应式的对象。

这个对象可以像普通对象一样使用,但它是响应式的。

例如:const state = reactive({ count: 0 });

选择使用 reactive 还是 ref:

  • reactive: 当你需要处理复杂的数据结构,比如嵌套对象或数组时,使用 reactive 更合适。
  • ref: 当你处理的是基本类型(如字符串、数字等)时,使用 ref 更加简洁明了。

Effect

watchEffect: 创建一个副作用函数,用于监听数据变化并执行相应的操作。

watch: 监听单个或多个响应式数据的变化,并执行回调函数。

watchEffectwatch有什么区别呢?

  • watchEffect:这个副作用函数会自动收集依赖。这意味着当您在一个 watchEffect 函数内访问任何响应式数据时,这些数据都会被自动添加到该函数的依赖列表中。当你对应的响应式变量发生变化的时候,它会自动去重新执行watchEffect

  • watch

    • 它会要求你显式地声明要观察的依赖。这意味着您需要明确指定哪些响应式数据需要被观察,并提供一个回调函数来处理这些数据的变化。

    • watch函数中提供了对旧值和新值的访问,可以将两个作为参数获取。

    • 这里你还需要注意一点就是watch函数可以加入配置项{ immediate: true },这个配置项让你可以在观察器创建后立即执行一次回调函数,无论这个依赖是否已经发生变化。这种情况适用于组件初始化时就进行某些操作的场景。

选择使用 watchEffect 还是 watch:

  • 如果只需要响应数据的变化而不关心具体的旧值和新值,那么 watchEffect 是一个更简洁的选择。
  • 如果需要在数据变化时执行一些逻辑,并且需要了解旧值和新值之间的差异,那么 watch 更合适。
  • 如果需要在组件初始化时就执行某些基于初始状态的操作,那么可以使用 watch 并设置 { immediate: true }

生命周期钩子

  1. onMounted()
    • 生命周期钩子,在组件挂载到 DOM 后调用。常用于初始化数据或设置事件监听器。
  2. onBeforeMount()
    • 在组件挂载前调用的生命周期钩子,可用于组件挂载前的准备工作。
  3. onBeforeUpdate()
    • 在组件更新之前调用,可用于在组件重新渲染前执行逻辑。
  4. onUpdated()
    • 在组件更新并重新渲染后调用,可用于执行更新后的操作,如滚动到某个元素。
  5. onUnmounted()
    • 在组件卸载前调用,可用于清理定时器、取消异步请求或移除事件监听器。
  6. onBeforeUnmount()
    • 在组件卸载前调用,与 onUnmounted 类似,但发生在实际卸载操作之前。

计算属性

computed: 创建一个基于其他响应式数据的计算属性。

响应式系统

Composition API 的关系?

响应式系统是 Vue 3中核心特性之一,那在这里讲述这个,跟我们要讲述的Composition API 有什么关系呢?

Vue 3 中,Composition API 本身不是响应式系统的一部分,但它通过 reactiveref API 与响应式系统紧密相连。开发者可以在 Composition API 的函数如 setup() 中使用 reactiveref 来创建响应式数据,所以我们深入理解一下响应式系统是很有必要的。

响应式系统的工作原理:

  1. 创建响应式对象:使用 reactiveref 创建响应式对象或引用。
  2. 收集依赖:当访问或修改响应式对象的属性时,响应式系统会通过 Proxy 的 getset 方法拦截这些操作,并记录下哪些副作用函数(通常是渲染函数)依赖于这些数据。
  3. 触发更新:当响应式对象的属性发生更改时,响应式系统会通过 set 方法触发更新,通知所有相关的副作用函数重新执行,从而更新视图。

如何实现?

Vue 3 的响应式系统是基于 Proxy 对象构建的,它能够自动追踪数据变化并触发视图更新。这使得数据和组件之间的交互更加流畅和高效。

然后我就在思考,为什么要使用 Proxy对象来构建呢?不能直接使用源对象吗?

good question!在我们使用 Proxy对象的时候我们可以直接对整个对象进行拦截,而不需要像 Vue 2那样为每个属性手动添加 gettersetter。在 Vue 2 中,响应式系统主要依靠在数据对象上手动设置 getter 和 setter 来实现数据的响应式特性。Vue 2 使用了数据劫持的方式,在数据初始化的时候遍历对象的所有属性,并为每个属性添加 getter 和 setter,以便在属性被访问或修改时触发相应的操作。

框架初始化
  • 当你引入 Vue.js 库时,响应式系统的各个组成部分已经准备好。
  • 这些组成部分包括 reactiverefeffect 等函数,它们是响应式系统的核心。
创建响应式数据
  • 创建响应式数据是在在 Vue 组件的 setup 函数中,你使用 reactiveref 来创建响应式数据。

  • 这些数据随后会被响应式系统追踪和管理。

初始化响应式数据的具体内容:
  • 准备 reactiveref
    • Vue 框架内部已经实现了 reactiveref 函数,它们可以用于创建响应式数据。
    • 这些函数内部使用了 Proxy API 来创建响应式代理对象。
  • 准备 effect
    • Vue 框架内部也实现了 effect 函数,用于追踪副作用函数(通常是组件的渲染函数)的依赖,并在数据变化时触发视图更新。
区分创建响应式数据的过程与响应式系统本身的职责:

当我们要提及创建响应式数据时,我们的目的是区分创建响应式数据的过程与响应式系统本身的职责。具体而言:

  • 创建响应式数据:当你执行 setup 函数时,你使用 reactiveref 来创建响应式数据。这是你显式创建响应式数据的操作点。
  • 响应式系统的职责:响应式系统内部实现了 reactiveref 函数,并准备了 effect 函数。这些工具和方法允许你创建响应式数据,并追踪数据变化以更新视图。

那具体响应式系统做了什么事情呢?请看下面的响应式系统的工作流程:

响应式系统的工作流程:

  • 初始化响应式变量:通过refreactive函数创建响应式引用和对象。
  • 依赖追踪track函数在访问响应式属性时被调用,收集依赖;effect函数则代表了需要在依赖变化时执行的副作用。
  • 渲染更新:依赖变化时,通过trigger函数通知相关effect函数重新执行,触发组件的重新渲染。

首先,我们来看一下 Vue 3 的源码是如何初始化响应式数据的。

reactivereadonly 函数:

Vue 3 中的 reactivereadonly 函数是创建响应式对象和只读对象(就是允许你可以像正常使用对象一样访问它的属性,但是你不能通过这个代理直接修改对象的属性或添加新的属性)的入口。

export function reactive<T extends object>(target: T): T {
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}

export function readonly<T extends object>(target: T): T {
  return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers);
}

createReactiveObject 函数:

// 创建 Proxy 对象
function createReactiveObject(
  target: Target, // 这是将要被代理化的原始对象。它可以是任何类型的数据结构,比如普通的 JavaScript 对象或数组。
  isReadonly: boolean, // 这是一个布尔值标志,用来指示生成的 Proxy 是否应该是只读的。
  baseHandlers: ProxyHandler<any>, // 这是一个包含一组 Proxy 操作处理器的对象。这些处理器定义了当特定操作发生时的行为,例如读取、写入属性等。mutableHandlers 和 collectionHandlers 可能是针对不同类型的 target 使用的不同处理器集。
  collectionHandlers: ProxyHandler<any>, //  类似于 baseHandlers,但是这里指特别为集合类型(如 Array, Set, Map 等)提供定制的行为。
  proxyMap: WeakMap<Target, any> // 这是一个 WeakMap,它存储了已经创建的 Proxy 对象。通常用于缓存机制,避免对同一个目标对象重复创建多个 Proxy 实例。
) {
  // 非对象类型:如果 target 不是一个对象,函数会返回原始值,并发出警告。
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
          target
        )}`
      )
    }
    return target
  }
  // 已经代理: 如果 target 已经是一个 Proxy 对象,并且不是将只读 Proxy 传递给 reactive 函数的情况,
  // 那么直接返回这个已有的 Proxy 对象。
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 缓存检查:如果 proxyMap 中已经有 target 的 Proxy 对象,直接返回已存在的 Proxy。
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // getTargetType(target) 函数用于确定 target 的类型。如果目标类型无效(例如 null 或不可观测的类型),则直接返回 target。
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 创建代理:根据目标类型选择适当的 ProxyHandler。对于普通对象使用 baseHandlers,对于集合类型(如 Map 和 Set)使用 collectionHandlers。
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers // 定义了可变对象的代理操作
  )
  // 创建一个新的 Proxy 对象,并将它存储在 proxyMap 中,以便下次可以快速查找已创建的 Proxy。
  proxyMap.set(target, proxy)
  return proxy
}

MutableReactiveHandler 类:

在我们分析这段源码之前我们先引入一下需要准备的知识:

浅响应

浅响应(Shallow Reactive)是指在 Vue 3 的响应式系统中,只对最外层的对象进行响应式处理,而不深入处理其内部的嵌套对象或数组。

  • 概念解释:

    当我们在使用 reactive 函数创建响应式对象时,默认情况下,不仅该对象本身是响应性的,而且其中包含的任何对象或数组也是响应式的。这就表明如果一个对象的某个属性被更新时,不论这个属性是直接的还是嵌套的,都会触发依赖该属性的视图的更新。

    所以引入了浅响应的概念,它不会让对象内部的每一个层级都变为响应式的。

  • 实现方式

    Vue 3API中提供了一个单独的函数 shallowReactive

    import { reactive, isReactive, isShallowReactive } from 'vue';
    
    const state = reactive({ a: { b: { c: 1 } } }, true); // 使用浅响应
    
  • 工作原理

    当你使用 shallowReactive 创建一个对象时,该对象自身是响应式的,但是它内部的属性如果是对象或数组,这些嵌套的对象或数组不会自动变成响应式的。

    例如,如果我们有一个对象 state 包含另一个对象 nested

    const state = shallowReactive({
      nested: { prop: 1 }
    });
    

    当我们修改 state.nested.prop 时,由于 nested 对象本身并不是响应式的,所以不会触发依赖 state.nested 的视图更新。

    虽然浅响应减少了创建响应式对象的数量,从而提高了初始化性能,但是它需要开发者手动去使用 reactiveshallowReactive对嵌套对象进行处理。并且如果不小心,可能会导致视图无法正确更新。

  • 示例

    import { shallowReactive } from 'vue';
    
    const state = shallowReactive({
      user: {
        name: 'John Doe',
        address: {
          city: 'New York',
          country: 'USA'
        }
      }
    });
    
    // 修改最外层对象
    state.user.name = 'Jane Doe'; // 这会导致依赖 `state.user.name` 的视图更新
    
    // 修改内部对象
    state.user.address.city = 'Los Angeles'; // 这不会触发视图更新
    

好了,我们接着分析MutableReactiveHandler源码了:

MutableReactiveHandler 类定义了响应式对象的代理操作,包括设置、删除、检查存在性和获取所有键的操作。这些操作都涉及依赖追踪和触发更新的功能,以确保响应式系统正确地工作。

  // 类定义
  class MutableReactiveHandler extends BaseReactiveHandler {
    // ...
  }
set 方法
  • 检查并处理旧值。
  • 修改属性。
  • 触发更新。
  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 使用 (target as any)[key] 获取 target 对象上的 key 属性值。
    let oldValue = (target as any)[key]
    // 非浅层模式:
    if (!this._isShallow) {
      // 检查旧值是否只读: isReadonly(oldValue): 检查旧值是否为只读。
      const isOldValueReadonly = isReadonly(oldValue)
      // 处理非只读的新值: 如果新值不是只读且旧值不是只读,则将两者都转为原始值。
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 处理 Ref 类型旧值:如果旧值是 Ref 类型且新值不是 Ref 类型,且旧值不是只读,则直接修改 Ref 的 value 属性。
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
          return false
        } else {
          oldValue.value = value
          return true
        }
      }
    } else {
      // 浅层模式:如果处于浅层模式,则直接设置值,不管值是否为响应式的。
    }
    // 检查键是否存在:
    // 对于数组: 如果 target 是数组且 key 是有效的整数索引,则检查索引是否小于数组长度。
    // 对于普通对象: 使用 hasOwn 检查 key 是否存在于 target 上。
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 执行设置操作: 使用 Reflect.set 方法设置 target 上的 key 属性为 value。
    const result = Reflect.set(target, key, value, receiver)
    // 触发更新:
    // 检查目标和接收器: 确保 target 是原始 receiver。
    if (target === toRaw(receiver)) {
      // 如果键不存在,则触发添加操作。如果键存在且新旧值不同,则触发设置操作。
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    // 返回 Reflect.set 的结果。
    return result
  }

上面这个代码的解释会让你觉得有点空洞,让我们来举一些例子:

  • 首先在这里我想说一下使用(target as any)[key]获取 target 对象上的 key 属性值。是什么意思?就是在开发者调用set方法的时候,set 方法会去 目标对象即target中获取对应的 key属性值,它才会谈得上去修改,即 target[key]

    示例

    const obj = reactive({ a: 1, b: { c: 2 } });
    

    当我们调用 set(obj, 'a', 2) 时,key 的属性值就是 obj['a'],也就是 1

  • 这里还有一个问题就是:为什么如果处于浅层模式,则直接设置值,不管值是否为响应式的?

    • 浅响应和非浅响应的最外层在执行set方法的时候都是响应的。
    • 当修改浅层响应对象的外层的时候,并不在乎它里面的内容是否为响应的,浅响应只有最外层对象是响应的,所以直接赋值给它可以降低性能开销。
    • 当修改非浅层响应对象的时候,需要对该对象内部进行访问控制所有的内容都是响应的,如果存在有未被封装的响应的内容需要让它变为响应的状态。
  • 我们确实

    对于数组: 如果 target 是数组且 key 是有效的整数索引,则检查索引是否小于数组长度。
    对于普通对象: 使用 hasOwn 检查 key 是否存在于 target 上。

示例1修改普通对象的属性
const obj = reactive({ a: 1 });

// 修改前
console.log(obj); // { a: 1 }

// 修改后
set(obj, 'a', 2);

// 修改后
console.log(obj); // { a: 2 }

根据上面的源代码,分析流程

  • 获取旧值
    • oldValue1
  • 处理旧值
    • 由于 obj 不是数组,且 1 不是 Ref 类型,所以不需要特殊处理。
  • 执行设置操作
    • 使用 Reflect.set 设置 obj.a2
  • 触发更新
    • 由于 a 已经存在且新旧值不同,触发 SET 操作。
示例2修改 Ref 类型的属性
const data = ref(1);
const obj = reactive({ a: data });

// 修改前
console.log(obj); // { a: ref(1) }

// 修改后
set(obj, 'a', 2);

// 修改后
console.log(obj); // { a: 2 }

根据上面的源代码,分析流程

  • 获取旧值
    • oldValuedata,即 ref(1)
  • 处理旧值
    • 由于 dataRef 类型,而新值 2 不是 Ref 类型,且 data 不是只读的 Ref,所以直接修改 data.value2
  • 执行设置操作
    • 无需执行 Reflect.set,因为已经修改了 data.value。(原因是因为当我们去更新Ref对应的对象时,会自动去更新视图,所以并不需要去通过Reflect.set
  • 触发更新
    • 由于 a 已经存在且新旧值不同,触发 SET 操作。
示例3添加新属性
const obj = reactive({});

// 修改前
console.log(obj); // {}

// 修改后
set(obj, 'a', 1);

// 修改后
console.log(obj); // { a: 1 }

根据上面的源代码,分析流程

  • 获取旧值:
    • obj 中不存在 a,因此没有旧值。
  • 处理旧值:
    • 无需处理旧值。
  • 执行设置操作:
    • 使用 Reflect.set 设置 obj.a1
  • 触发更新
    • 由于 a 不存在,触发 ADD 操作。
示例 4修改数组的元素
const arr = reactive([1, 2]);

// 修改前
console.log(arr); // [1, 2]

// 修改后
set(arr, 1, 3);

// 修改后
console.log(arr); // [1, 3]

根据上面的源代码,分析流程

  • 取旧值:
    • oldValue2
  • 处理旧值:
    • 由于 arr 是数组,且 2 不是 Ref 类型,所以不需要特殊处理。
  • 执行设置操作:
    • 使用 Reflect.set 设置 arr[1]3
  • 触发更新:
    • 由于 1 已经存在且新旧值不同,触发 SET 操作。
示例5出现不符合条件的调用
const arr = reactive([1, 2, 3]);

// 检查键是否存在
const hadKey = Number('4') < arr.length; // false

// 执行设置操作
set(arr, '4', 5); // 触发添加操作
问题

对于这里hadKeyfalse,因为 '4' 不是有效的数组索引。我刚开始以为这不是索引都溢出了吗?那它执行添加操作是干些什么?

后面发现当使用 set 方法设置数组的索引时,如果索引超出当前数组的长度,这将被视为添加了一个新的元素到数组末尾。

示例

const arr = reactive([1, 2, 3]);

// 设置 arr[4] 为 5
set(arr, 4, 5);

// 输出数组
console.log(arr); // 输出: [1, 2, 3, undefined, 5]

注意

这上面还有一点需要注意的是:

在处理非只读的新值时,如果新值不是只读且旧值不是只读,则将两者都转为原始值。

示例

const obj = reactive({ prop: oldObj });

// 修改前
console.log(obj.prop); // { a: 1 }

// 修改后
set(obj, 'prop', newObj);

// 修改后
console.log(obj.prop); // { a: 2 }

原因1:重复包装

假设我们不转换新值 newObj,那么在 set 方法中直接设置 obj.propnewObj 时,可能会发生以下情况:

  • 旧值 (oldObj) 是响应式的。
  • 新值 (newObj) 也是响应式的。

如果直接设置 obj.propnewObj,那么 newObj 可能会被再次包装成响应式对象,导致重复包装。这不仅增加了不必要的开销,还可能导致响应式系统的行为不一致。

原因2:依赖追踪问题

假设我们不转换旧值 oldObj,那么在 set 方法中直接设置 obj.propnewObj 时,可能会发生以下情况:

  • 旧值 (oldObj) 是响应式的。
  • 新值 (newObj) 不是响应式的。

如果不转换旧值,那么旧值仍然是响应式的,而新值不是响应式的。这可能会导致响应式系统无法正确地追踪新值的依赖关系,从而在新值变化时无法正确地更新视图。

希望通过上面的这些例子可以让你很好的理解set方法中根据不同情况设置它对应的值。

触发更新
  • 如果键不存在,则触发添加操作。
  • 如果键存在且新旧值不同,则触发设置操作。
trigger函数
trigger 函数的作用

trigger 函数的主要作用是通知依赖该数据的观察者(effect functions)数据已发生变化,需要重新执行以更新视图。它基于依赖追踪机制工作,确保只有真正依赖数据变化的部分才会被更新。

trigger 函数的工作流程
  1. 追踪依赖:
    • 在数据被访问时,track 函数会记录哪些副作用函数依赖于该数据。
    • 依赖追踪是通过 track 函数完成的,它记录了哪些副作用函数依赖于哪个响应式数据。
  2. 触发更新:
    • 当数据发生变化时,trigger 函数会被调用。
    • trigger 函数根据之前记录的依赖关系来确定哪些副作用函数需要重新执行。
    • 重新执行副作用函数,从而更新视图。
deleteProperty 方法

deleteProperty 方法用于处理删除响应式对象的属性。当从响应式对象中删除一个属性时,此方法会被调用。

根据上面的源代码,分析流程

  • 检查键是否存在:
    • 使用 hasOwn 方法检查 key 是否存在于 target 上。
  • 执行删除操作:
    • 使用 Reflect.deleteProperty 方法删除 target 上的 key 属性。
  • 触发删除操作:
    • 如果键存在且删除成功,则触发删除操作。
has方法

has 方法用于检查响应式对象中是否存在某个键。当访问响应式对象的属性时,此方法会被调用以确定键是否存在。

根据上面的源代码,分析流程

  • 执行存在性检查:
    • 使用 Reflect.has 方法检查 key 是否存在于 target 上。
  • 追踪依赖:
    • 如果 key 不是内置符号,则追踪 has 操作。
问题

在看到这个方法,发现一个问题:为什么在set方法中,对于普通对象使用 hasOwn 检查 key 是否存在于 target 上,而不是使用的has方法呢?

hasOwnJavaScript自带的一种原生的方法,用于检测一个对象是否具有指定的属性并且这个属性不能由继承得来意思就是不是从原型链继承来的。

has 方法在 Vue 3 的响应式系统中通常指的是 WeakMapMap 对象中的 has 方法,用于检查给定的键是否存在于集合中。

关于has的用法:在 Vue 3 的响应式系统中,WeakMapMap 类型被用来存储关于对象的元数据,比如哪些属性是响应式的,存储映射关系:将原始对象和响应式对象的映射关系存储在 WeakMap 中接着通过has 方法可以用于检查特定的键是否已经存在于这些集合中。

ownKeys方法

ownKeys 方法用于获取响应式对象的所有键。当遍历响应式对象的属性时,此方法会被调用以获取所有属性键。

根据上面的源代码,分析流程

  • 追踪迭代操作:
    • 对于数组,追踪 length 属性。
    • 对于其他对象,追踪 ITERATE_KEY
  • 获取所有键:
    • 使用 Reflect.ownKeys 获取 target 上的所有键。
问题

hasownKeys使用的是track方法,deletePropertyset方法使用的是trigger方法,这两者有什么区别呢?为什么要这么使用呢?

track方法用于记录依赖关系。当访问一个响应式对象的属性时 ,Vue 3会调用track函数去收集当前活动的副作用函数,也就是任何读取这个属性的地方都会被标记为依赖于该属性。这样就可以在属性变化时知道哪些效果函数需要重新执行。

trigger 方法则是在数据发生变化时触发依赖更新。数据发生改变(如设置新值、删除属性等),Vue 3 会调用 trigger 方法来通知所有依赖这个属性的副作用函数进行重新计算。

所以也就清晰了,当去调用hasownKeys方法的时候,是去获取某一个特定的属性或者是响应对象所有的键的动作,不会涉及数据发生后变化,所以只需要去调用 track 方法来记录这个操作依赖于对应的响应式对象即可。但是当去调用deletePropertyset方法的时候,都是会对响应式对象进行改变的操作的,所以只需要去调用 trigger 方法在数据发生变化时触发依赖更新。

Ref:

上面的 hasownKeys方法适用的场景是使用reactive创建的响应式对象,而对于Ref来说,它在Vue 3 中的处理方式是不同的。ref主要用于创建一个响应式的引用类型,而reactive则用于使对象响应式。

Ref的工作原理:

ref创建了一个具有.value属性的对象,该属性指向实际的值。当你访问或修改ref.value时,Vue 会跟踪这些依赖并在值发生变化时进行更新。

ref 中的 tracktrigger

ref中这两个方法肯定是会使用的,使用的方式:

  • 当你读取ref的值的时候,Vue会调用track函数记录依赖关系。

  • 当你修改 ref.value 时,Vue 会调用 trigger 方法来触发依赖更新。

ref当中我们一般也不会去调用deletePropertyset方法来删除或添加属性,因为 ref 的目标是管理一个单一的值,而不是一个对象。如果你想删除或添加属性,你应该直接操作 ref.value

通过这些方法,Vue 3 的响应式系统能够在数据发生变化时自动更新视图,同时保证操作的效率和准确性。并且你也能够对响应式系统能够有一个全面的认识。

副作用函数

我们讲了很多次副作用函数,但是这个概念我们还没有对它有清晰地认识。所以接下来让我们来玩一下:

  • 副作用函数通常指的是那些依赖于响应式数据并在响应式数据发生变化时会需要重新执行的函数。所以适用副作用函数能够避免不必要的更新,提高性能。

  • Composition API中常见的副作用函数就是effect函数,当然在Vue中副作用函数还存在其它的作用,比如说执行异步操作,发送网络请求、定时任务,副作用函数还可以作为组件的生命周期钩子等等。

effect函数
import { ref, effect } from 'vue';

const count = ref(0);

// 创建一个副作用函数
const incrementCounter = effect(() => {
  document.getElementById('counter').textContent = count.value;
});

// 更新 count 的值
count.value++;

// incrementCounter 会自动重新执行,更新 DOM
关于effect函数在响应式系统中的分析

响应式系统中的 effect 函数是用于创建副作用函数的关键部分。

首先对于effect函数的设计思路

  1. 创建effect函数并执行

  2. 跟踪依赖

    当副作用函数去访问某一个响应式变量时,需要记录副作用函数与数据之间的依赖关系。

  3. 触发更新

    当某一个响应式变量发生改变时,就需要去更新依赖它的副作用函数。

  4. 执行环境管理

    当一个在执行某一个副作用函数里面可能会涉及调用另外一个副作用函数,需要正确管理副作用函数的执行环境。

    所以这里引入了effectStack保存正在执行的副作用函数序列。

简化版的effect函数实现:
import { activeEffect, effectStack } from './effect'

function effect(fn, options: EffectOptions = {}) {
  const _effect = function effectFn() {
    // 清理副作用函数的依赖
    cleanup(effectFn)

    // 将当前执行的 effect 设置为 activeEffect
    // 将当前 effectFn 添加到 effectStack 中
    try {
      effectStack.push(effectFn)
      activeEffect = effectFn

      // 执行副作用函数
      return fn()
    } finally {
      // 从 effectStack 中移除当前 effectFn
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]
    }
  }

  // 标记副作用函数
  _effect.isEffect = true
  _effect.raw = fn

  // 如果指定了调度函数,则设置
  if (options.scheduler) {
    _effect.scheduler = options.scheduler
  }

  // 如果设置了立即执行,则执行副作用函数
  if (!options.lazy) {
    _effect()
  }

  return _effect
}
/**
 * 遍历副作用函数 effectFn 的依赖集合 deps。
 * 从每个依赖集合中删除 effectFn 的引用。
 * 清空 effectFn 的依赖集合 deps。
 */
function cleanup(effectFn) {
  const { deps } = effectFn
  if (deps) {
    // 清理 effectFn 的依赖
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effectFn)
    }

    // 清空依赖
    effectFn.deps.length = 0
  }
}

export { effect }

接下来我们开始分析代码:

好的,开始提问:

问题
为什么需要清理副作用函数的依赖?

因为假如我们不清除之前的依赖,会出现一些问题比如可能会导致上一次的依赖在改变的时候,副作用函数会被重新调用,但实际上现在对应的副作用函数已经依赖于另外一个响应对象或者变量了。这么说你可能还是不能理解,让我来给你举一个例子

假设我们有一个副作用函数 effectFn,它最初依赖于 count 数据。之后,我们修改了 effectFn,使其不再依赖于 count,而是依赖于 anotherCount 数据。如果我们不清除旧的依赖关系,effectFn 仍然会被 count 数据的变化所触发,即使它不再访问 count

import { effect, ref } from './effect'

const count = ref(0)
const anotherCount = ref(0)

// 创建一个副作用函数,最初依赖于 count
const effectFn = effect(() => {
  document.getElementById('counter').textContent = count.value
})

// 修改副作用函数,使其依赖于 anotherCount
effectFn.raw = () => {
  document.getElementById('counter').textContent = anotherCount.value
}

count.value++
anotherCount.value++
为什么要将当前执行的 effect 设置为 activeEffect?
  1. 通过设置activeEffect收集依赖。举例:

    const count = reactive({ value: 0 });
    
    effect(() => {
      console.log('Count : ', count.value);
    });
    

    在执行effect函数的时候,当读取到countvalue值时,框架会检查当前是否有活跃的 effect(即activeEffect),如果有就通过track函数记录这个依赖关系,这样在count.value修改的时候,框架就知道这个effect依赖于这个响应式变量了

  2. 避免了循环引用:当在执行一个effect函数的时候又触发了其它的effect函数时,通过activeEffecteffectStack来控制执行的顺序,防止无限循环或重复执行相同的效果。

为什么需要原始的副作用函数 raw还有设置isEffect 标志?

在我们创建effect函数的时候,我们接受一个fn函数作为参数,为了记录副作用函数的依赖关系和执行逻辑,需要创建一个新的函数 _effect,但是我们希望在_effect中随时都能访问到原始副作用函数,所以设置了一个 raw属性来记录原始副作用函数。

isEffect 标志主要的作用就是标记了副作用函数的身份。这样防止了普通函数也会被执行。

让我来给你举一个例子

import { effect, ref } from './effect'

const count = ref(0)

const incrementCounter = effect(() => {
  document.getElementById('counter').textContent = count.value
})

// 假设我们需要在某个地方调用原始的副作用函数
function callOriginalEffect() {
  // 使用 effectFn.raw 来调用原始副作用函数
  incrementCounter.raw()
}

// 假设我们需要检查一个函数是否是副作用函数
function isSideEffectFunction(fn) {
  return fn.isEffect === true
}

console.log(isSideEffectFunction(incrementCounter)); // 输出: true
为什么还需要指定调度函数和设置立即执行?

指定调度函数:我们可以控制它在下一个任务队列中被调用,不需要当执行副作用函数的时候它马上就被调用。比如说如果副作用函数执行时间较长,我们可能希望将它放入一个异步任务中执行,以避免阻塞主线程。或者当多个数据变化时,我们可以延迟执行副作用函数,合并多次更新为一次,从而减少不必要的渲染次数。这些都是它的适用场景。

设置立即执行:通过设置 lazytrue,我们可以延迟副作用函数的执行,直到显式调用它。当我们不确定副作用函数是否需要执行时,可以通过设置 lazytrue 来延迟执行,直到真正需要时再调用它。

相信通过上面的几个问题以及对简易版的effect函数的代码理解,你已经能够清晰的认识到副作用函数了,也明白了副作用函数的作用

渲染器

渲染器是Vue 3 的运行时的核心组件,它定义了如何将Vue 组件的虚拟DOM转换成实际的DOM结构或者其他的输出形式。例如,我们在浏览器环境下,会将Vue 组件转换成HTML元素;但在服务器端(SSR)场景下,渲染器会生成静态的HTML字符串。关于SSR到时候我们专门去玩一下。

DOM 渲染器实现:

RendererOptions 接口:

在渲染器中,RendererOptions 接口是重要的组成部分,它提供了一些列的操作包括

patchProp(用于更新或设置元素的属性,包括 classstyle 和事件监听器等。)

insert(用于将一个节点插入DOM中。)

remove(用于从 DOM移除一个节点。)

createElement(用于创建一个新的 DOM 元素。)

createText(用于创建文本节点

createComment(用于创建注释节点

setText(用于设置文本节点

setElementText(用于设置元素的文本内容

parentNode(用于获取节点的父节点

nextSibling(用于获取节点的下一个同级节点

等等。

createRenderer 函数:

这个函数的作用就是创建渲染器实例。这个实例包含了 h 方法用于创建虚拟节点,以及 render 方法用于将虚拟节点转换为实际 DOM 节点并插入到 DOM 中。

// 创建渲染器实例
const renderer = createRenderer(rendererOptions);

// 导出 `h` 函数和 `render` 方法供外部使用
export const h = renderer.h;
export const render = renderer.render;

使用渲染器:

  • 创建虚拟节点 (使用 h 函数):第一个参数为对应的节点标签,第二个参数为属性对象

  • 渲染虚拟节点(render):第一个参数为虚拟节点实例,第二个参数为对应的body

  • 案例:

    import { h, render } from './renderer'; // 假设上面的代码已经导出了 `h` 和 `render`
    
    const app = h('div', { id: 'app' }, [
      h('h1', {}, '你最喜欢的美食:'),
      h('button', { onClick: () => alert('点击按钮') }, '点击'),
      h('p', {}, '我爱吃肉夹馍'),
    ]);
    
    render(app, document.body);
    

对于渲染器来说它虽然不像响应式系统那样,是Composition API的基石,但是也是跟Composition API有所联系。它负责将Vue 组件渲染到目标环境中。

Teleport 和 Suspense API

Vue 3 还引入了两个非常有用的 API,分别是 TeleportSuspense,它们分别解决了不同的UI设计和异步加载的问题。这里作为课外地内容了解,希望能够对你在面对某些场景能够有用。

Teleport:

用途:

Teleport允许你将一个子树”传送“到DOM中的另一个位置它并且允许你在不改变组件内部结构的情况下,将元素放置在任何地方。它会很适用某些场景,比如模态框(modal)、弹出提示框(tooltips)或者拖拽元素等,这些元素通常被附加到文档的 <body> 或其他特定容器中,而不是它们在当前组件内的位置。

问题:
  • 在传统情况下,我想要在一个div中加入一个拖动元素的话,我直接在div中加不就好了,为什么还要用Teleport?它的优势在哪里?
    1. 避免样式冲突:
      • 如果你的拖拽元素需要具有全局定位(例如,绝对定位或固定定位),它可能会与其他元素的样式冲突。将拖拽元素使用 Teleport 放置在 <body> 或其他容器中可以避免这种冲突。
    2. 灵活的布局管理:
      • 使用 Teleport 可以让你在不改变组件内部结构的情况下,将元素放置在任何地方。这对于保持组件的可复用性和简洁性非常有用。
案例:
<template>
  <button @click="isOpen = true">Open Modal</button>
  <Teleport to="body">
    <div v-if="isOpen" class="modal">
      <h1>Modal Title</h1>
      <p>This is a modal dialog.</p>
      <button @click="isOpen = false">Close Modal</button>
    </div>
  </Teleport>
</template>

<script lang='ts' setup>
import { ref } from "vue";


const isOpen = ref(false);

</script>
<style lang='less' scoped>
.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  padding: 20px;
  border: 1px solid #ccc;
}
</style>

Suspense:

用途

Suspense组件用于处理异步组件的加载过程中的状态,类似于el-table实现loading

解决的问题:
  • 改善用户体验:在异步组件加载完成之前显示一个加载指示器,避免页面出现空白或闪烁的情况。
  • 统一加载逻辑:为所有异步组件提供一个一致的加载机制,简化了组件的实现逻辑。
  • 性能优化:异步组件可以按需加载,有助于减少初始加载时间。
案例:
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
<script lang='ts' setup>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));


</script>
<style lang='less' scoped></style>

实战示例

实现一个简易版的setup():

setup()函数的底层设计

自己设计一个简易版本的setup()函数

Vue 3Composition API 中,setup() 函数的主要作用确实是基于已有的响应式系统来实现组件的状态管理和逻辑处理。为了实现这一点,我们需要构建一系列的功能,包括:

  1. 创建响应式对象或变量

    • ref:创建一个响应式的引用。
    • reactive:创建一个响应式的对象。
  2. 计算属性

    • computed:创建一个基于其他响应式依赖的计算属性。
  3. 观察者

    • watch:观察一个或多个响应式依赖的变化,并执行副作用。
    • watchEffect:与 effect 类似,但自动追踪依赖。
  4. 依赖追踪和触发

    • track:记录哪些副作用依赖了特定的响应式依赖。
    • trigger:当响应式依赖改变时,触发相关副作用的执行。
  5. 副作用函数

    • effect:创建一个副作用函数,它可以自动追踪依赖并在依赖变化时重新执行。
如何实现呢?
  • 对于reactive来说,我们前面也介绍过,它的目的就是创建响应式对象,它是基于Proxy来实现的,所以我们在创建reactive函数的时候直接就是创建一个新的Proxy对象就可以,因为简易版就不需要考虑那么多情况。这个代理对象能够拦截对原对象的访问和修改,当我们进行访问和修改操作的时候,首先会访问这个Proxy对象。

  • 对于track来说它是记录响应式数据和对应的副作用函数的依赖关系,当一个副作用函数读取响应式数据时,我们需要知道这个副作用函数依赖于哪些数据。这样,当这些数据发生变化时,我们可以通知相关的副作用函数重新执行,从而更新视图或其他依赖于这些数据的部分(当然这也是trigger函数的工作)。

    举个例子:

    假设我们有两个副作用函数 effect1effect2,它们分别依赖于响应式对象 state 的不同属性 state.astate.b。当我们第一次读取这些属性时,会发生以下情况:

    • effect1 执行并读取 state.a 时,track 函数被调用,将 effect1 添加到 state.a 的依赖列表中。
    • effect2 执行并读取 state.b 时,track 函数再次被调用,这次将 effect2 添加到 state.b 的依赖列表中。

    这样,当 state.astate.b 发生变化时,我们可以通过 trigger 函数找到并执行相应的副作用函数,从而更新依赖于这些属性的视图或其他部分。

  • 我们在set的时候去调用trigger函数是有道理的因为我们修改了响应式数据所以对于依赖于这个数据的副作用函数都是需要去执行的。

    • 但是为什么我们要在get的时候去执行track方法呢?

    • reactive不是创建响应式对象吗?它又不是在执行副作用函数,那它get请求调用这个track函数有什么意义呢?反正activeEffectfalse

    • 在创建响应式对象时,确实不会执行 track 函数内部的逻辑,因为此时没有副作用函数在运行,activeEffect 也未被设置。然而,track 函数的存在是为了在副作用函数运行时记录依赖关系

  • 对于computed函数的构建,我们直接使用reactive来实现,computed 则是在这个基础上构建的一种高级抽象,用于处理依赖其他响应式数据的复杂计算场景。

    • computed 函数通过使用 reactive 来创建一个具有响应式特性的计算属性。这个计算属性的值依赖于其他响应式数据,并且在这些数据变化时自动重新计算。

    • 在这个函数里面有一个关键的一点是会设置一个dirty属性来控制这个数据是否是最新的。

    • 利用getset 方法,它们是用于定义对象属性存取器的特殊方法。它们允许你定义对象的属性,这些属性的读取和写入操作可以触发自定义的函数。

代码实现
// 响应式数据结构
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      track(target, key);
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      trigger(target, key);
      return true;
    }
  });
}

// ref 实现
function ref(initialValue) {
  return reactive({ value: initialValue });
}

function track(target, key) {
  if (activeEffect) {
    let depsMap = target.__dep__ || (target.__dep__ = new Map());
    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }
    dep.add(activeEffect);
  }
}

// 依赖触发
function trigger(target, key) {
  const depsMap = target.__dep__;
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effectFn => effectFn());
  }
}

// 依赖收集和触发函数
let activeEffect = null;
const effectStack = [];

function effect(fn, options = {}) {
  const effectFn = () => {
    activeEffect = effectFn;
    effectStack.push(effectFn);

    const result = fn();

    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1] || null; // 确保当栈为空时,activeEffect 为 null  
    return result;
  };

  if (options.lazy) {
    return () => {
      if (!effectStack.includes(effectFn)) {
        effectFn();
      }
    };
  }

  effectFn(); // 立即执行效果,除非指定为 lazy  
  return effectFn;
}

// computed 实现  
function computed(getter) {
  let value;
  let isDirty = true;
  const dep = new Set();

  const evaluate = () => {
    if (isDirty) {
      effect(() => {
        value = getter();
        isDirty = false;
      }, { lazy: true })();
    }
  };

  const obj = {
    get value() {
      evaluate();
      return value;
    },
    // 通常 computed 不允许设置值  
    set value(newVal) {
      console.warn('Computed value is readonly');
    }
  };

  // 初始化时计算一次  
  evaluate();

  return obj;
}

// setup 函数
function setup(props) {
  // 使用 Composition API
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);

  // 一个副作用函数,用于监听 count 的变化并打印
  effect(() => {
    console.log('count发生变化:', count.value);
  });

  // 返回模板渲染时使用的属性
  return {
    count,
    doubleCount
  };
}

// 测试
function renderTemplate({ count, doubleCount }) {
  console.log(`Current count is ${count.value}`);
  console.log(`Double count is ${doubleCount.value}`);
}

// 调用 setup 函数并传入 props(这里props不使用)
const { count, doubleCount } = setup({});

// 初始化渲染
renderTemplate({ count, doubleCount });

console.log("``````(开始发生变化)````````");
// 改变 count 的值
count.value = 5;

// 再次渲染,这次应该能看到 count 和 doubleCount 的值都发生了变化
renderTemplate({ count, doubleCount });
问题
我们在执行副作用函数的时候,发现在effect函数中也没有去调用track函数或者trigger函数?那怎么去做依赖更新的呢?

当执行副作用函数时,我们不会直接调用 tracktrigger 函数。这就体现了响应式系统创建响应式变量使用Proxy对象的强大之处了,因为它会拦截原对象的访问和修改。拦截之后实现了对于 tracktrigger 函数的调用。

设置 activeEffect
  • 在副作用函数执行前,框架会将当前执行的副作用函数设置为 activeEffect,这样当副作用函数访问响应式对象的属性时,track 函数就能记录下依赖关系。

  • 调用副作用函数:副作用函数被执行,它会访问响应式对象的属性,触发 get 方法,进而调用 track 函数。

  • 清理 activeEffect:在副作用函数执行结束后,框架通常会清理 activeEffect,这样在后续的非副作用函数代码中访问响应式对象的属性时,不会错误地记录依赖。

在track函数中为什么要嵌套两层呢?我们只去维护一层map不就行了吗?

track 函数中,我们使用了两层结构:一个 Map 和一个 Set。这里是一个简化版的例子来帮助你理解:

 // 假设有一个响应式对象
 const obj = { a: 1, b: 2 };
 
 // 使 obj 成为响应式的
 const reactiveObj = makeReactive(obj);
 
 // 假设我们有两个副作用函数
 function effect1() {
   console.log(reactiveObj.a);
 }
 
 function effect2() {
   console.log(reactiveObj.b);
 }
 
 // 执行副作用函数
 effect1();
 effect2();
 
 // 当属性 a 或 b 发生变化时,我们需要重新执行相关的副作用函数
 reactiveObj.a = 3;
 reactiveObj.b = 4;

在这个例子中,我们需要记录哪些副作用函数依赖于哪些属性。

  • 第一层 Map 用于存储对象中的每个属性及其对应的副作用函数集合。
  • 第二层 Set 用于存储依赖于特定属性的所有副作用函数。

现在,如果我们更新 reactiveObj.areactiveObj.b,我们需要触发相关副作用函数的重新执行。

  • reactiveObj.a 更新时,trigger 函数查找 reactiveObj.__dep__.get('a'),并执行其中的所有副作用函数(在这个例子中只有 effect1)。
  • reactiveObj.b 更新时,trigger 函数查找 reactiveObj.__dep__.get('b'),并执行其中的所有副作用函数(在这个例子中只有 effect2)。

这样的设计允许我们精确地知道哪些副作用函数依赖于哪些数据属性,并且只在相关数据改变时重新执行这些副作用函数。

computed 函数的工作流程:

例子

 const state = reactive({ a: 1, b: 2 });
 
 const computedValue = computed(() => {
   return state.a + state.b;
 });
 
 console.log(computedValue.value); // 第一次访问
 
 state.a = 5;
 console.log(computedValue.value); // 重新计算后访问

初始化阶段:

  • computed函数被调用,传入一个箭头函数() => state.a + state.b作为getter

  • computed函数内部创建了一个evaluate函数,该函数将调用effect函数,以lazy模式执行getter函数。

    lazy模式的作用

    • 当依赖的响应式属性(如 state.astate.b)发生变化时,计算属性的 getter 函数不会立即执行,而是等待下次访问计算属性的值时才执行。

    • 这样,如果在依赖属性变化后,应用程序的当前逻辑不需要立即访问计算属性的值,就不会触发不必要的计算。

  • evaluate函数在computed对象的value属性的get方法中被调用,用于在需要时计算并返回getter的值。

  • isDirty标志被初始化为true,表示getter的值尚未被计算或需要重新计算。

第一次访问阶段:

  • 用户首次访问computedValue.value
  • 由于isDirtytrueevaluate函数被调用。
  • evaluate函数内部,effectlazy模式执行getter函数,计算state.a + state.b的结果。
  • getter函数在执行过程中会通过track函数追踪到对state.astate.b的依赖。
  • 计算完成后,isDirty被设置为false,表示value现在是干净的(已计算)。
  • 最终的计算结果被存储在value变量中,并通过value属性的get方法返回给用户。

依赖更新阶段:

  • 用户更新了state.a的值,从1变为5。

  • 由于state.a的值发生了变化,trigger函数被调用,触发所有依赖于state.a的副作用函数(即effect函数)。

  • 在本例中,evaluate函数的effect副作用函数被触发,但由于它以lazy模式执行,实际上不会立即重新计算getter函数。

  • 相反,isDirty标志被设置为true关于这一点

    • 在标准的Vue 3响应式系统中,computed函数的effect副作用函数会接收一个scheduler函数,这个函数会在依赖项发生变化时被调用,而不是立即执行副作用函数本身。
    • scheduler函数中,我们通常会把isDirty标志设置为true,这样下一次访问computed属性时,就会触发重新计算。

第二次访问阶段:

  • 用户再次访问computedValue.value
  • isDirty再次检查,发现为true,所以evaluate函数被调用。
  • evaluate函数内的effect函数重新执行getter,此时getter中的state.astate.b的值都是最新的。
  • 新的计算结果(5 + 2 = 7)被存储在value变量中,isDirty被设置为false
  • 新的计算结果通过value属性的get方法返回给用户。
运行结果:

在这里插入图片描述
希望通过上面这个例子,让你对整个响应式系统有一个详细的理解,踩了很多坑,终于实现了这个setup函数。

自定义 Hook

import { ref, watch } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => (count.value = initialValue);

  // 可选的:监听 count 的变化并执行一些操作
  watch(count, newValue => {
    console.log(`监听: ${newValue}`);
  });

  return {
    count,
    increment,
    decrement,
    reset
  };
}
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="reset">Reset</button>
  </div>
</template>

<script setup lang="ts">
import { useCounter } from './hooks/useCounter';

const { count, increment, decrement, reset } = useCounter(10);
</script>

上面这种自定义hook的方式也是我们在使用Composition API时经常使用的,它使得组件的逻辑非常清晰并且便于维护,多个组件都可以使用。

总结:

对于Vue.js中的Composition API来说

首先要明确它是为什么被提出

为了解决在Vue 2中出现的问题,在我们Vue 2中使用传统的Option API会导致组件逻辑结构不清晰,代码难以扩展和阅读,调试困难等等问题。

所以Vue 3提出了Composition API,它提供了一种更现代、更易于维护的方式来组织和复用组件逻辑。使得开发者使用起来更加便捷,并且便于开发者分析组件的逻辑以及提高代码的可读性和可维护性。

Composition API的提供使用的方法以及常用的模块

它是以setup函数为切入口,使得我们开发的组件逻辑不会分散。它提供了一些列常用的方法包括ref、reactive、computed、watch、watchEffect、各种生命周期钩子函数等等。

Composition API的底层实现

基于Vue 3 的响应式系统和渲染器,响应式系统是实现响应式的基石。响应式系统在Vue 框架加载的时候自动创建了。所以我们可以非常方便的在setup函数中调用ref,reactive,computed等等方法实现响应式数据的创建,监控等等。

响应式系统的工作流程

  1. 初始化响应式变量
  2. 依赖追踪:依赖收集和副作用函数
  3. 渲染更新

reactive函数底层实现

响应式对象的创建在Vue 3中是非常重要的这也是初始化响应式变量的基础,并且它并不像Vue 2中需要循环遍历每一个对象然后使用gettersetter方法实现响应式,在Vue 3中响应式对象的创建是通过Proxy对象来实现的,它拦截了响应式对象的读取和修改操作,提高了性能。

依赖追踪底层实现

依赖追踪其实就是通过实现track函数和effect函数即副作用函数实现的

当你访问或修改响应式对象的属性时,Vue 会通过 Proxy 的 getset 方法追踪这些操作。在访问或修改时,Vue 通过track函数会记录哪些 Effect 函数依赖于这些数据,通过维护一个Map来实现依赖追踪。

Effect函数

对于Effect函数,通常指的是那些依赖于响应式数据并在响应式数据发生变化时会需要重新执行的函数。所以适用副作用函数能够避免不必要的更新,提高性能。我们需要知道它的应用场景以及实现它的一些细节点。包括每一创建一个新的_effect对象之前都要去清理副作用函数的依赖cleanup(effectFn)

渲染更新

当响应式数据发生变化时,Vue 调用trigger函数通知所有依赖于这些数据的 Effect 函数,触发它们重新执行,从而导致视图更新。所以在这里也有渲染器的参与。

渲染器

渲染器的作用就是将这些虚拟的DOM最后真真正正的转换为真实的DOM,它的源码最主要的提供了两个东西一个是RendererOptions 接口(提供了一些操作)和createRenderer 函数(创建渲染器实例)。

正是基于响应式系统和渲染器才能实现Composition API

TeleportSuspense API

当然我们Vue 3中还提供了一些其它的API 比如说分别是 TeleportSuspense,它们分别解决了不同的UI设计和异步加载的问题。

实战应用:

你提到的简易setup函数构建和自定义Hook的例子,很好地体现了Composition API的实际应用,鼓励开发者将逻辑分解为更小、更可复用的单元。

希望对您学习有帮助!Composition API里面还有很多内容比如说生命周期钩子等等,都非常值得学习的。如果有问题希望能够大家一起交流交流。

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值