手写 vue 源码 ===:自定义调度器、递归调用规避与深度代理
目录
[TOC]
引言
在上一篇文章中,我们深入探讨了 Vue 响应式系统中的依赖清理机制。本文将继续深入 Vue 响应式系统的其他高级特性,包括自定义调度器、递归调用规避、深度代理等,并探讨其中涉及的 bind
方法、切面编程思想以及递归实现流程。
自定义调度器(Scheduler)
什么是调度器?
调度器是 Vue 响应式系统中的一个重要概念,它允许我们控制 effect 的执行时机和方式。当响应式数据发生变化时,默认情况下会立即触发相关的 effect 执行。而通过自定义调度器,我们可以改变这一默认行为。
调度器的实现原理
在 Vue 的响应式系统中,调度器是通过 effect
函数的第二个参数传入的:
export function effect(fn, options: any = {}) {
// 创建一个 effect 只要依赖的属性变化,就会重新执行
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});
// 执行
_effect.run();
if (options) {
Object.assign(_effect, options); //用用户传入的配置,来覆盖默认的配置
}
const runner = _effect.run.bind(_effect); //bind 改变this指向 并且返回一个函数
runner.effect = _effect; //将effect挂载到runner上
return runner;
}
当我们创建一个 ReactiveEffect
实例时,可以传入一个调度器函数作为第二个参数。当响应式数据变化触发 effect 执行时,会先检查是否有调度器,如果有则执行调度器而不是直接执行 effect:
export function triggerEffects(dep) {
for (let effect of dep.keys()) {
// 如果不是正在执行,那么就执行调度器
if (!effect._runing) {
if (effect.scheduler) {
effect.scheduler();
}
}
}
}
自定义调度器的实际应用
让我们看一个实际的例子,展示如何使用自定义调度器:
//调度器
let runner = effect(() => {
document.body.innerHTML = `<h1>${state.flag ? state.name : state.age}</h1>`
}, {
scheduler: () => {
console.log('scheduler执行了,不自动更新');//AOP 面向切面编程
runner() //重新渲染
}
})
在这个例子中,我们创建了一个 effect 并传入了一个自定义调度器。当响应式数据变化时,不会直接执行 effect 的回调函数,而是先执行调度器函数。在调度器函数中,我们可以决定是否执行 effect,或者何时执行 effect。
切面编程(AOP)思想在调度器中的应用
上面的例子中提到了 AOP(面向切面编程)。AOP 是一种编程范式,它允许我们在不修改原有代码的情况下,通过"切面"的方式添加新的行为。
在 Vue 的响应式系统中,调度器就是一个典型的 AOP 应用:
- 原始行为:响应式数据变化时执行 effect
- 切面:调度器函数
- 新行为:响应式数据变化时先执行调度器,由调度器决定何时执行 effect
这种设计使得我们可以在不修改 Vue 核心代码的情况下,灵活地控制 effect 的执行方式,例如:
- 延迟执行 effect(使用
setTimeout
) - 批量执行 effect(收集多个变化后一次性执行)
- 条件执行 effect(根据某些条件决定是否执行)
递归调用规避
递归调用的问题
在响应式系统中,如果在 effect 中修改了触发该 effect 的响应式数据,就会导致递归调用,例如:
effect(() => {
state.count = state.count + 1
})
这段代码会导致无限循环:读取 state.count
会收集依赖,修改 state.count
会触发依赖执行,而依赖执行又会修改 state.count
,如此循环往复。
Vue 如何规避递归调用
Vue 通过在 ReactiveEffect
类中添加一个 _runing
标志来规避递归调用:
class ReactiveEffect {
_trackId = 0; // 当前的 effect 执行了几次
deps = []; // 当前的 effect 依赖了哪些属性
_depsLength = 0; // 当前的 effect 依赖的属性有多少个
_runing = 0; // 当前的 effect 是否在执行
public active = true; //默认是响应式的
constructor(public fn, public scheduler) {}
run() {
// 如果当前状态是停止的,执行后,啥都不做
if (!this.active) {
return this.fn();
}
let lastEffect = activeEffect;
try {
activeEffect = this; // 当前的 effect 「依赖收集」
// 每次执行前需要将上一次的依赖清空 effect.deps
preCleanEffect(this);
this._runing++; //执行前,将当前的effect设置为正在执行
return this.fn(); //依赖收集 「state.name ,state.age」
} finally {
this._runing--; //执行后,将当前的effect设置为未执行
postCleanEffect(this);
activeEffect = lastEffect; // 执行完毕后 恢复上一次的 activeEffect
}
}
}
在 run 方法中,执行前将 _runing 设为 1,执行后将 _runing 设为 0。在 triggerEffects 函数中,会检查 effect 是否正在执行,如果是则不触发:
export function triggerEffects(dep) {
for (let effect of dep.keys()) {
// 如果不是正在执行,那么就执行调度器
if (!effect._runing) {
if (effect.scheduler) {
effect.scheduler();
}
}
}
}
深度代理(Deep Proxy)
什么是深度代理?
深度代理是指不仅对对象本身进行代理,还对对象的嵌套属性(如果是对象)也进行代理。这样,无论访问对象的哪一层属性,都能触发依赖收集和更新。
Vue 中的深度代理实现
Vue 的深度代理是在 getter 中实现的:
export const mutableHandlers: ProxyHandler<any> = {
get(target: any, key: any, receiver: any) {
// 如果访问的是代理对象的属性,直接返回
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 依赖收集「收集这个对象上的这个属性,和 effect 关联」
// 「当取值的时候,应该让 响应式属性,和 effect 建立联系」
track(target, key);
let res = Reflect.get(target, key, receiver); // 等价于receiver[key]
if (isObject(res)) {
// 如果取值是对象,则递归代理
return reactive(res);
}
return res;
},
}
深度代理的递归流程如下:
- 调用 reactive(obj) 创建一个代理对象
- 当访问代理对象的属性时,触发 get 方法
- 在 get 方法中,先进行依赖收集
- 然后获取属性值 res
- 如果 res 是一个对象,则调用 reactive(res) 对其进行代理
- 返回代理后的对象
这样,当我们访问嵌套属性时,例如 state.address.city,会发生以下过程:
- 访问 state.address,触发 state 的 get 方法
- 在 get 方法中,获取 address 属性,发现它是一个对象,调用 reactive(address) 创建代理
- 返回 address 的代理对象
- 访问 address 代理对象的 city 属性,触发 address 代理对象的 get 方法
- 在 get 方法中,获取 city 属性,它不是对象,直接返回
通过这种递归的方式,Vue 实现了对嵌套对象的深度代理。
// 深度监听
effect(() => {
document.body.innerHTML = state.address.city
})
setTimeout(() => {
state.address.city = "上海"
}, 1000)
在这个例子中,我们创建了一个 effect,它依赖于 state.address.city。当我们修改 state.address.city 时,effect 会重新执行,更新页面内容。
这是因为 state.address 是一个代理对象,当我们访问 state.address.city 时,会触发 state.address 的 get 方法,从而收集依赖。当我们修改 state.address.city 时,会触发
state.address 的 set 方法,从而触发依赖更新。
bind 方法的应用
在 Vue 的响应式系统中, bind
方法被用于创建 effect 的运行器(runner)
export function effect(fn, options: any = {}) {
// 创建一个 effect 只要依赖的属性变化,就会重新执行
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});
// 执行
_effect.run();
if (options) {
Object.assign(_effect, options); //用用户传入的配置,来覆盖默认的配置
}
const runner = _effect.run.bind(_effect); //bind 改变this指向 并且返回一个函数
runner.effect = _effect; //将effect挂载到runner上
return runner;
}
bind 方法的作用
bind 方法是 JavaScript 中函数对象的一个方法,它创建一个新函数,该函数的 this 被绑定到指定的值。在 Vue 的响应式系统中, bind 方法的作用是:
- 创建一个新函数 runner,它的 this 被绑定到 _effect
- 这样,无论在哪里调用 runner,它内部的 this 都指向 _effect
- 这确保了 runner 可以正确地访问 _effect 的属性和方法
为什么需要 bind?
在 JavaScript 中,函数的 this 值取决于函数的调用方式,而不是函数的定义方式。如果我们直接返回 _effect.run,那么当调用这个函数时,this 可能不指向 _effect,导致错误。
通过使用 bind,我们确保了无论如何调用 runner,它内部的 this 都指向 _effect,从而保证了函数的正确执行。
总结
本文深入探讨了 Vue 响应式系统的几个高级特性:
- 自定义调度器 :通过调度器,我们可以控制 effect 的执行时机和方式,实现更灵活的响应式行为。这是 AOP(面向切面编程)思想在 Vue 中的一个应用。
- 递归调用规避 :Vue 通过在 effect 执行前后设置标志位,避免了在 effect 中修改响应式数据导致的递归调用问题。
- 深度代理 :Vue 通过在 getter 中递归调用 reactive 函数,实现了对嵌套对象的深度代理,使得无论访问对象的哪一层属性,都能触发依赖收集和更新。
- bind 方法的应用 :Vue 使用 bind 方法创建 effect 的运行器,确保了无论在哪里调用运行器,它内部的
this
都指向正确的 effect 实例。
通过理解这些高级特性,我们可以更深入地理解 Vue 响应式系统的工作原理,以及如何利用这些特性构建更高效、更灵活的 Vue 应用。
在实际开发中,虽然我们可能不需要直接操作这些底层 API,但了解它们的工作原理,可以帮助我们更好地理解 Vue 的响应式系统,以及在遇到复杂问题时进行调试和优化。