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
的一大特点就是在对应的属性中编写对应的功能模块导致会出现一些问题:
-
组件逻辑分散:
-
只要用过
Vue 2
的都知道,在Options API
中,组件的逻辑被分割成多个选项,如data
、computed
、methods
和watch
也包括生命周期钩子。它要求开发者必须把对应的逻辑写入对应的结构内,当然我们也发现如果简单的组件,代码结构还是比较清晰的。 -
但是对于复杂的组件来说,这种分散的结构有时会使组件变得难以阅读和理解。
-
-
调试困难:
-
由于
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
: 监听单个或多个响应式数据的变化,并执行回调函数。
那watchEffect
和watch
有什么区别呢?
-
watchEffect:这个副作用函数会自动收集依赖。这意味着当您在一个
watchEffect
函数内访问任何响应式数据时,这些数据都会被自动添加到该函数的依赖列表中。当你对应的响应式变量发生变化的时候,它会自动去重新执行watchEffect
。 -
watch:
-
它会要求你显式地声明要观察的依赖。这意味着您需要明确指定哪些响应式数据需要被观察,并提供一个回调函数来处理这些数据的变化。
-
watch函数中提供了对旧值和新值的访问,可以将两个作为参数获取。
-
这里你还需要注意一点就是
watch
函数可以加入配置项{ immediate: true }
,这个配置项让你可以在观察器创建后立即执行一次回调函数,无论这个依赖是否已经发生变化。这种情况适用于组件初始化时就进行某些操作的场景。
-
选择使用 watchEffect 还是 watch:
- 如果只需要响应数据的变化而不关心具体的旧值和新值,那么
watchEffect
是一个更简洁的选择。 - 如果需要在数据变化时执行一些逻辑,并且需要了解旧值和新值之间的差异,那么
watch
更合适。 - 如果需要在组件初始化时就执行某些基于初始状态的操作,那么可以使用
watch
并设置{ immediate: true }
。
生命周期钩子:
onMounted()
- 生命周期钩子,在组件挂载到 DOM 后调用。常用于初始化数据或设置事件监听器。
onBeforeMount()
- 在组件挂载前调用的生命周期钩子,可用于组件挂载前的准备工作。
onBeforeUpdate()
- 在组件更新之前调用,可用于在组件重新渲染前执行逻辑。
onUpdated()
- 在组件更新并重新渲染后调用,可用于执行更新后的操作,如滚动到某个元素。
onUnmounted()
- 在组件卸载前调用,可用于清理定时器、取消异步请求或移除事件监听器。
onBeforeUnmount()
- 在组件卸载前调用,与
onUnmounted
类似,但发生在实际卸载操作之前。
- 在组件卸载前调用,与
计算属性:
computed
: 创建一个基于其他响应式数据的计算属性。
响应式系统:
与Composition API
的关系?
响应式系统是 Vue 3中核心特性之一,那在这里讲述这个,跟我们要讲述的Composition API
有什么关系呢?
在 Vue 3
中,Composition API 本身不是响应式系统的一部分,但它通过 reactive
和 ref
API 与响应式系统紧密相连。开发者可以在 Composition API 的函数如 setup()
中使用 reactive
和 ref
来创建响应式数据,所以我们深入理解一下响应式系统是很有必要的。
响应式系统的工作原理:
- 创建响应式对象:使用
reactive
或ref
创建响应式对象或引用。 - 收集依赖:当访问或修改响应式对象的属性时,响应式系统会通过 Proxy 的
get
和set
方法拦截这些操作,并记录下哪些副作用函数(通常是渲染函数)依赖于这些数据。 - 触发更新:当响应式对象的属性发生更改时,响应式系统会通过
set
方法触发更新,通知所有相关的副作用函数重新执行,从而更新视图。
如何实现?
Vue 3
的响应式系统是基于 Proxy
对象构建的,它能够自动追踪数据变化并触发视图更新。这使得数据和组件之间的交互更加流畅和高效。
然后我就在思考,为什么要使用 Proxy
对象来构建呢?不能直接使用源对象吗?
good question!在我们使用 Proxy
对象的时候我们可以直接对整个对象进行拦截,而不需要像 Vue 2
那样为每个属性手动添加 getter
和 setter
。在 Vue 2 中,响应式系统主要依靠在数据对象上手动设置 getter 和 setter 来实现数据的响应式特性。Vue 2 使用了数据劫持的方式,在数据初始化的时候遍历对象的所有属性,并为每个属性添加 getter 和 setter,以便在属性被访问或修改时触发相应的操作。
框架初始化:
- 当你引入 Vue.js 库时,响应式系统的各个组成部分已经准备好。
- 这些组成部分包括
reactive
、ref
、effect
等函数,它们是响应式系统的核心。
创建响应式数据:
-
创建响应式数据是在在 Vue 组件的
setup
函数中,你使用reactive
或ref
来创建响应式数据。 -
这些数据随后会被响应式系统追踪和管理。
初始化响应式数据的具体内容:
- 准备
reactive
和ref
:- Vue 框架内部已经实现了
reactive
和ref
函数,它们可以用于创建响应式数据。 - 这些函数内部使用了
Proxy
API 来创建响应式代理对象。
- Vue 框架内部已经实现了
- 准备
effect
:- Vue 框架内部也实现了
effect
函数,用于追踪副作用函数(通常是组件的渲染函数)的依赖,并在数据变化时触发视图更新。
- Vue 框架内部也实现了
区分创建响应式数据的过程与响应式系统本身的职责:
当我们要提及创建响应式数据时,我们的目的是区分创建响应式数据的过程与响应式系统本身的职责。具体而言:
- 创建响应式数据:当你执行
setup
函数时,你使用reactive
或ref
来创建响应式数据。这是你显式创建响应式数据的操作点。 - 响应式系统的职责:响应式系统内部实现了
reactive
和ref
函数,并准备了effect
函数。这些工具和方法允许你创建响应式数据,并追踪数据变化以更新视图。
那具体响应式系统做了什么事情呢?请看下面的响应式系统的工作流程:
响应式系统的工作流程:
- 初始化响应式变量:通过
ref
和reactive
函数创建响应式引用和对象。 - 依赖追踪:
track
函数在访问响应式属性时被调用,收集依赖;effect
函数则代表了需要在依赖变化时执行的副作用。 - 渲染更新:依赖变化时,通过
trigger
函数通知相关effect
函数重新执行,触发组件的重新渲染。
首先,我们来看一下 Vue 3 的源码是如何初始化响应式数据的。
reactive
和 readonly
函数:
Vue 3 中的 reactive
和 readonly
函数是创建响应式对象和只读对象(就是允许你可以像正常使用对象一样访问它的属性,但是你不能通过这个代理直接修改对象的属性或添加新的属性)的入口。
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 3
的API
中提供了一个单独的函数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
的视图更新。虽然浅响应减少了创建响应式对象的数量,从而提高了初始化性能,但是它需要开发者手动去使用
reactive
或shallowReactive
对嵌套对象进行处理。并且如果不小心,可能会导致视图无法正确更新。 -
示例:
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 }
根据上面的源代码,分析流程:
- 获取旧值:
oldValue
是1
。
- 处理旧值:
- 由于
obj
不是数组,且1
不是Ref
类型,所以不需要特殊处理。
- 由于
- 执行设置操作:
- 使用
Reflect.set
设置obj.a
为2
。
- 使用
- 触发更新:
- 由于
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 }
根据上面的源代码,分析流程:
- 获取旧值:
oldValue
是data
,即ref(1)
。
- 处理旧值:
- 由于
data
是Ref
类型,而新值2
不是Ref
类型,且data
不是只读的Ref
,所以直接修改data.value
为2
。
- 由于
- 执行设置操作:
- 无需执行
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.a
为1
。
- 使用
- 触发更新
- 由于
a
不存在,触发ADD
操作。
- 由于
示例 4:修改数组的元素
const arr = reactive([1, 2]);
// 修改前
console.log(arr); // [1, 2]
// 修改后
set(arr, 1, 3);
// 修改后
console.log(arr); // [1, 3]
根据上面的源代码,分析流程:
- 取旧值:
oldValue
是2
。
- 处理旧值:
- 由于
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); // 触发添加操作
问题:
对于这里hadKey
为 false
,因为 '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.prop
为 newObj
时,可能会发生以下情况:
- 旧值 (
oldObj
) 是响应式的。 - 新值 (
newObj
) 也是响应式的。
如果直接设置 obj.prop
为 newObj
,那么 newObj
可能会被再次包装成响应式对象,导致重复包装。这不仅增加了不必要的开销,还可能导致响应式系统的行为不一致。
原因2:依赖追踪问题
假设我们不转换旧值 oldObj
,那么在 set
方法中直接设置 obj.prop
为 newObj
时,可能会发生以下情况:
- 旧值 (
oldObj
) 是响应式的。 - 新值 (
newObj
) 不是响应式的。
如果不转换旧值,那么旧值仍然是响应式的,而新值不是响应式的。这可能会导致响应式系统无法正确地追踪新值的依赖关系,从而在新值变化时无法正确地更新视图。
希望通过上面的这些例子可以让你很好的理解set
方法中根据不同情况设置它对应的值。
触发更新:
- 如果键不存在,则触发添加操作。
- 如果键存在且新旧值不同,则触发设置操作。
trigger函数:
trigger
函数的作用:
trigger
函数的主要作用是通知依赖该数据的观察者(effect functions
)数据已发生变化,需要重新执行以更新视图。它基于依赖追踪机制工作,确保只有真正依赖数据变化的部分才会被更新。
trigger
函数的工作流程:
- 追踪依赖:
- 在数据被访问时,
track
函数会记录哪些副作用函数依赖于该数据。 - 依赖追踪是通过
track
函数完成的,它记录了哪些副作用函数依赖于哪个响应式数据。
- 在数据被访问时,
- 触发更新:
- 当数据发生变化时,
trigger
函数会被调用。 trigger
函数根据之前记录的依赖关系来确定哪些副作用函数需要重新执行。- 重新执行副作用函数,从而更新视图。
- 当数据发生变化时,
deleteProperty 方法:
deleteProperty
方法用于处理删除响应式对象的属性。当从响应式对象中删除一个属性时,此方法会被调用。
根据上面的源代码,分析流程:
- 检查键是否存在:
- 使用
hasOwn
方法检查key
是否存在于target
上。
- 使用
- 执行删除操作:
- 使用
Reflect.deleteProperty
方法删除target
上的key
属性。
- 使用
- 触发删除操作:
- 如果键存在且删除成功,则触发删除操作。
has方法:
has
方法用于检查响应式对象中是否存在某个键。当访问响应式对象的属性时,此方法会被调用以确定键是否存在。
根据上面的源代码,分析流程:
- 执行存在性检查:
- 使用
Reflect.has
方法检查key
是否存在于target
上。
- 使用
- 追踪依赖:
- 如果
key
不是内置符号,则追踪has
操作。
- 如果
问题:
在看到这个方法,发现一个问题:为什么在set
方法中,对于普通对象使用 hasOwn
检查 key
是否存在于 target
上,而不是使用的has
方法呢?
hasOwn
是JavaScript
自带的一种原生的方法,用于检测一个对象是否具有指定的属性并且这个属性不能由继承得来意思就是不是从原型链继承来的。
has
方法在 Vue 3 的响应式系统中通常指的是 WeakMap
或 Map
对象中的 has
方法,用于检查给定的键是否存在于集合中。
关于has
的用法:在 Vue 3 的响应式系统中,WeakMap
和 Map
类型被用来存储关于对象的元数据,比如哪些属性是响应式的,存储映射关系:将原始对象和响应式对象的映射关系存储在 WeakMap
中接着通过has
方法可以用于检查特定的键是否已经存在于这些集合中。
ownKeys方法:
ownKeys
方法用于获取响应式对象的所有键。当遍历响应式对象的属性时,此方法会被调用以获取所有属性键。
根据上面的源代码,分析流程:
- 追踪迭代操作:
- 对于数组,追踪
length
属性。 - 对于其他对象,追踪
ITERATE_KEY
。
- 对于数组,追踪
- 获取所有键:
- 使用
Reflect.ownKeys
获取target
上的所有键。
- 使用
问题:
has
和ownKeys
使用的是track
方法,deleteProperty
和set
方法使用的是trigger
方法,这两者有什么区别呢?为什么要这么使用呢?
track
方法用于记录依赖关系。当访问一个响应式对象的属性时 ,Vue 3
会调用track
函数去收集当前活动的副作用函数,也就是任何读取这个属性的地方都会被标记为依赖于该属性。这样就可以在属性变化时知道哪些效果函数需要重新执行。
trigger
方法则是在数据发生变化时触发依赖更新。数据发生改变(如设置新值、删除属性等),Vue 3 会调用 trigger
方法来通知所有依赖这个属性的副作用函数进行重新计算。
所以也就清晰了,当去调用has
和ownKeys
方法的时候,是去获取某一个特定的属性或者是响应对象所有的键的动作,不会涉及数据发生后变化,所以只需要去调用 track
方法来记录这个操作依赖于对应的响应式对象即可。但是当去调用deleteProperty
和set
方法的时候,都是会对响应式对象进行改变的操作的,所以只需要去调用 trigger
方法在数据发生变化时触发依赖更新。
Ref:
上面的 has
和 ownKeys
方法适用的场景是使用reactive
创建的响应式对象,而对于Ref
来说,它在Vue 3
中的处理方式是不同的。ref
主要用于创建一个响应式的引用类型,而reactive
则用于使对象响应式。
Ref
的工作原理:
ref
创建了一个具有.value
属性的对象,该属性指向实际的值。当你访问或修改ref
的.value
时,Vue
会跟踪这些依赖并在值发生变化时进行更新。
ref
中的 track
和 trigger
:
在ref
中这两个方法肯定是会使用的,使用的方式:
-
当你读取
ref
的值的时候,Vue
会调用track
函数记录依赖关系。 -
当你修改
ref
的.value
时,Vue
会调用trigger
方法来触发依赖更新。
在ref
当中我们一般也不会去调用deleteProperty
和set
方法来删除或添加属性,因为 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
函数的设计思路:
-
创建
effect
函数并执行 -
跟踪依赖:
当副作用函数去访问某一个响应式变量时,需要记录副作用函数与数据之间的依赖关系。
-
触发更新:
当某一个响应式变量发生改变时,就需要去更新依赖它的副作用函数。
-
执行环境管理:
当一个在执行某一个副作用函数里面可能会涉及调用另外一个副作用函数,需要正确管理副作用函数的执行环境。
所以这里引入了
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?
-
通过设置
activeEffect
收集依赖。举例:const count = reactive({ value: 0 }); effect(() => { console.log('Count : ', count.value); });
在执行
effect
函数的时候,当读取到count
的value
值时,框架会检查当前是否有活跃的effect
(即activeEffect
),如果有就通过track
函数记录这个依赖关系,这样在count.value
修改的时候,框架就知道这个effect
依赖于这个响应式变量了 -
避免了循环引用:当在执行一个
effect
函数的时候又触发了其它的effect
函数时,通过activeEffect
和effectStack
来控制执行的顺序,防止无限循环或重复执行相同的效果。
为什么需要原始的副作用函数 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
为什么还需要指定调度函数和设置立即执行?
指定调度函数:我们可以控制它在下一个任务队列中被调用,不需要当执行副作用函数的时候它马上就被调用。比如说如果副作用函数执行时间较长,我们可能希望将它放入一个异步任务中执行,以避免阻塞主线程。或者当多个数据变化时,我们可以延迟执行副作用函数,合并多次更新为一次,从而减少不必要的渲染次数。这些都是它的适用场景。
设置立即执行:通过设置 lazy
为 true
,我们可以延迟副作用函数的执行,直到显式调用它。当我们不确定副作用函数是否需要执行时,可以通过设置 lazy
为 true
来延迟执行,直到真正需要时再调用它。
相信通过上面的几个问题以及对简易版的effect
函数的代码理解,你已经能够清晰的认识到副作用函数了,也明白了副作用函数的作用
渲染器:
渲染器是Vue 3
的运行时的核心组件,它定义了如何将Vue
组件的虚拟DOM
转换成实际的DOM
结构或者其他的输出形式。例如,我们在浏览器环境下,会将Vue
组件转换成HTML
元素;但在服务器端(SSR)场景下,渲染器会生成静态的HTML
字符串。关于SSR到时候我们专门去玩一下。
DOM 渲染器实现:
RendererOptions 接口:
在渲染器中,RendererOptions 接口是重要的组成部分,它提供了一些列的操作包括
patchProp
(用于更新或设置元素的属性,包括 class
、style
和事件监听器等。)
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,分别是 Teleport
和 Suspense
,它们分别解决了不同的UI
设计和异步加载的问题。这里作为课外地内容了解,希望能够对你在面对某些场景能够有用。
Teleport:
用途:
Teleport
允许你将一个子树”传送“到DOM
中的另一个位置它并且允许你在不改变组件内部结构的情况下,将元素放置在任何地方。它会很适用某些场景,比如模态框(modal
)、弹出提示框(tooltips
)或者拖拽元素等,这些元素通常被附加到文档的 <body>
或其他特定容器中,而不是它们在当前组件内的位置。
问题:
- 在传统情况下,我想要在一个div中加入一个拖动元素的话,我直接在div中加不就好了,为什么还要用
Teleport
?它的优势在哪里?- 避免样式冲突:
- 如果你的拖拽元素需要具有全局定位(例如,绝对定位或固定定位),它可能会与其他元素的样式冲突。将拖拽元素使用
Teleport
放置在<body>
或其他容器中可以避免这种冲突。
- 如果你的拖拽元素需要具有全局定位(例如,绝对定位或固定定位),它可能会与其他元素的样式冲突。将拖拽元素使用
- 灵活的布局管理:
- 使用
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 3
的 Composition API
中,setup()
函数的主要作用确实是基于已有的响应式系统来实现组件的状态管理和逻辑处理。为了实现这一点,我们需要构建一系列的功能,包括:
-
创建响应式对象或变量:
ref
:创建一个响应式的引用。reactive
:创建一个响应式的对象。
-
计算属性:
computed
:创建一个基于其他响应式依赖的计算属性。
-
观察者:
watch
:观察一个或多个响应式依赖的变化,并执行副作用。watchEffect
:与effect
类似,但自动追踪依赖。
-
依赖追踪和触发:
track
:记录哪些副作用依赖了特定的响应式依赖。trigger
:当响应式依赖改变时,触发相关副作用的执行。
-
副作用函数:
effect
:创建一个副作用函数,它可以自动追踪依赖并在依赖变化时重新执行。
如何实现呢?
-
对于
reactive
来说,我们前面也介绍过,它的目的就是创建响应式对象,它是基于Proxy
来实现的,所以我们在创建reactive
函数的时候直接就是创建一个新的Proxy
对象就可以,因为简易版就不需要考虑那么多情况。这个代理对象能够拦截对原对象的访问和修改,当我们进行访问和修改操作的时候,首先会访问这个Proxy
对象。 -
对于
track
来说它是记录响应式数据和对应的副作用函数的依赖关系,当一个副作用函数读取响应式数据时,我们需要知道这个副作用函数依赖于哪些数据。这样,当这些数据发生变化时,我们可以通知相关的副作用函数重新执行,从而更新视图或其他依赖于这些数据的部分(当然这也是trigger
函数的工作)。举个例子:
假设我们有两个副作用函数
effect1
和effect2
,它们分别依赖于响应式对象state
的不同属性state.a
和state.b
。当我们第一次读取这些属性时,会发生以下情况:- 当
effect1
执行并读取state.a
时,track
函数被调用,将effect1
添加到state.a
的依赖列表中。 - 当
effect2
执行并读取state.b
时,track
函数再次被调用,这次将effect2
添加到state.b
的依赖列表中。
这样,当
state.a
或state.b
发生变化时,我们可以通过trigger
函数找到并执行相应的副作用函数,从而更新依赖于这些属性的视图或其他部分。 - 当
-
我们在
set
的时候去调用trigge
r函数是有道理的因为我们修改了响应式数据所以对于依赖于这个数据的副作用函数都是需要去执行的。-
但是为什么我们要在
get
的时候去执行track
方法呢? -
reactive
不是创建响应式对象吗?它又不是在执行副作用函数,那它get请求调用这个track
函数有什么意义呢?反正activeEffect
为false
。 -
在创建响应式对象时,确实不会执行
track
函数内部的逻辑,因为此时没有副作用函数在运行,activeEffect
也未被设置。然而,track
函数的存在是为了在副作用函数运行时记录依赖关系。
-
-
对于
computed
函数的构建,我们直接使用reactive来实现,computed
则是在这个基础上构建的一种高级抽象,用于处理依赖其他响应式数据的复杂计算场景。-
computed
函数通过使用reactive
来创建一个具有响应式特性的计算属性。这个计算属性的值依赖于其他响应式数据��并且在这些数据变化时自动重新计算。 -
在这个函数里面有一个关键的一点是会设置一个
dirty
属性来控制这个数据是否是最新的。 -
利用
get
和set
方法,它们是用于定义对象属性存取器的特殊方法。它们允许你定义对象的属性,这些属性的读取和写入操作可以触发自定义的函数。
-
代码实现:
// 响应式数据结构
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
函数?那怎么去做依赖更新的呢?
当执行副作用函数时,我们不会直接调用 track
或 trigger
函数。这就体现了响应式系统创建响应式变量使用Proxy
对象的强大之处了,因为它会拦截原对象的访问和修改。拦截之后实现了对于 track
或 trigger
函数的调用。
设置 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.a
或 reactiveObj.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.a
或state.b
)发生变化时,计算属性的getter
函数不会立即执行,而是等待下次访问计算属性的值时才执行。 -
这样,如果在依赖属性变化后,应用程序的当前逻辑不需要立即访问计算属性的值,就不会触发不必要的计算。
-
-
evaluate
函数在computed
对象的value
属性的get
方法中被调用,用于在需要时计算并返回getter
的值。 -
isDirty
标志被初始化为true
,表示getter
的值尚未被计算或需要重新计算。
第一次访问阶段:
- 用户首次访问
computedValue.value
。 - 由于
isDirty
为true
,evaluate
函数被调用。 evaluate
函数内部,effect
以lazy
模式执行getter
函数,计算state.a + state.b
的结果。getter
函数在执行过程中会通过track
函数追踪到对state.a
和state.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
属性时,就会触发重新计算。
- 在标准的Vue 3响应式系统中,
第二次访问阶段:
- 用户再次访问
computedValue.value
。 isDirty
再次检查,发现为true
,所以evaluate
函数被调用。evaluate
函数内的effect
函数重新执行getter
,此时getter
中的state.a
和state.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等等方法实现响应式数据的创建,监控等等。
响应式系统的工作流程:
- 初始化响应式变量
- 依赖追踪:依赖收集和副作用函数
- 渲染更新
reactive
函数底层实现:
响应式对象的创建在Vue 3
中是非常重要的这也是初始化响应式变量的基础,并且它并不像Vue 2
中需要循环遍历每一个对象然后使用getter
和 setter
方法实现响应式,在Vue 3
中响应式对象的创建是通过Proxy
对象来实现的,它拦截了响应式对象的读取和修改操作,提高了性能。
依赖追踪底层实现:
依赖追踪其实就是通过实现track函数和effect函数即副作用函数实现的
当你访问或修改响应式对象的属性时,Vue 会通过 Proxy 的 get
和 set
方法追踪这些操作。在访问或修改时,Vue 通过track函数会记录哪些 Effect 函数依赖于这些数据,通过维护一个Map来实现依赖追踪。
Effect
函数:
对于Effect
函数,通常指的是那些依赖于响应式数据并在响应式数据发生变化时会需要重新执行的函数。所以适用副作用函数能够避免不必要的更新,提高性能。我们需要知道它的应用场景以及实现它的一些细节点。包括每一创建一个新的_effect
对象之前都要去清理副作用函数的依赖cleanup(effectFn)
。
渲染更新:
当响应式数据发生变化时,Vue 调用trigger函数通知所有依赖于这些数据的 Effect 函数,触发它们重新执行,从而导致视图更新。所以在这里也有渲染器的参与。
渲染器:
渲染器的作用就是将这些虚拟的DOM
最后真真正正的转换为真实的DOM
,它的源码最主要的提供了两个东西一个是RendererOptions
接口(提供了一些操作)和createRenderer
函数(创建渲染器实例)。
正是基于响应式系统和渲染器才能实现Composition API
。
Teleport
和 Suspense API
:
当然我们Vue 3
中还提供了一些其它的API
比如说分别是 Teleport
和 Suspense
,它们分别解决了不同的UI
设计和异步加载的问题。
实战应用:
你提到的简易setup
函数构建和自定义Hook
的例子,很好地体现了Composition API
的实际应用,鼓励开发者将逻辑分解为更小、更可复用的单元。
希望对您学习有帮助!Composition API
里面还有很多内容比如说生命周期钩子等等,都非常值得学习的。如果有问题希望能够大家一起交流交流。
转载自:https://juejin.cn/post/7397622646631383049