手写 vue 源码 ===:自定义调度器、递归调用规避与深度代理
目录
引言
自定义调度器(Scheduler)
什么是调度器?
调度器的实现原理
自定义调度器的实际应用
切面编程(AOP)思想在调度器中的应用
递归调用规避
递归调用的问题
Vue 如何规避递归调用
深度代理(Deep Proxy)
什么是深度代理?
Vue 中的深度代理实现
bind 方法的应用
bind 方法的作用
为什么需要 bind?
总结
引言
在上一篇文章中,我们深入探讨了 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.depspreCleanEffect(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 的响应式系统,以及在遇到复杂问题时进行调试和优化。