Vue
的响应式大家都知道,依赖收集和依赖派发这两个词汇也是经常听到的,但是这里的依赖指的是什么呢?
根据我上上篇的分析,依赖就是Vue
中的effect
,也就是Vue
中的副作用函数,这一篇也是上上篇的一个补充,这次我们来详细分析一下Vue
中的effect
是如何实现的,以及effect
的第二个参数还有调度器scheduler
的作用。
effect
Vue
中的effect
是一个函数,它接受一个函数作为参数,这个函数就是我们的副作用函数;
effect
函数会在执行的时候,会执行我们传入的函数,并且会将这个函数保存到一个全局的effect
函数的数组中,这样我们就可以在需要的时候,调用这个数组中的函数,从而达到我们的副作用函数的执行。
先简单的看一下effect
函数的实现:
/** * effect 函数 * @param fn 副作用函数 * @param options 配置项 * @return {any} 返回一个执行副作用函数的函数 */ function effect(fn, options) { // 如果传入的函数是一个 effect 函数,那么就直接取出它的 fn if (fn.effect) { fn = fn.effect.fn; } // 创建一个响应式副作用函数 const _effect = new ReactiveEffect(fn); // 如果用户传入了配置项 if (options) { // 合并配置项 extend(_effect, options); // 记录 effect 函数的作用域 if (options.scope) recordEffectScope(_effect, options.scope); } // 如果用户没有传入 lazy 配置项,那么就立即执行一次 effect 函数 if (!options || !options.lazy) { _effect.run(); } // 返回一个执行 effect 函数的函数 const runner = _effect.run.bind(_effect); // 将 effect 函数保存到 runner.effect 中 runner.effect = _effect; // 返回 runner return runner; }
通过上面代码,不去看实现的细节,我们可以知道的是:
effect
函数接受一个函数作为参数,这个函数就是我们的副作用函数;effect
函数还有第二个参数,这个参数是一个配置项,根据仅有的代码可以知道,这个配置项有两个属性,一个是lazy
,一个是scope
;effect
函数会返回一个执行副作用函数的函数;
也就是说一个最简单的effect
函数的实现只需要做上面这三件事情就可以了,简单实现如下:
function effect(fn, options) { // 创建一个 runner 函数,用来执行副作用函数 function runner() { fn(); } // 是否立即执行 if (!options || !options.lazy) { fn(); } // 返回 runner return runner; }
在这个最简单的实现中,我们发现其中的核心就是一个runner
函数,而在源码中,runner
函数是通过ReactiveEffect
类来实现的;
同时ReactiveEffect
还充当了effect
函数的默认配置项的一个角色,runner
函数只是ReactiveEffect
的其中一个方法,所以effect
的核心就是ReactiveEffect
类;
ReactiveEffect
ReactiveEffect
类的实现如下:
class ReactiveEffect { constructor(fn, scheduler = null, scope) { this.fn = fn; this.scheduler = scheduler; this.active = true; this.deps = []; this.parent = undefined; } run() { // ... } stop() { // ... } }
ReactiveEffect
类的实现在之前已经分析过了,这一章会分析的是细节操作,如果想了解
ReactiveEffect
具体做了什么可以看:【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析
通过上面的代码,我们可以得知ReactiveEffect
类最后实例化出来之后的结果如下:
// 创建一个响应式副作用函数 const _effect = new ReactiveEffect(fn); _effect = { fn: fn, scheduler: null, active: true, deps: [], parent: undefined, run() {}, stop() {} }
这些属性我们其实很难猜到他们的具体作用,这个时候就需要去查看ts
版的源码了,ts
版的源码如下:
export class ReactiveEffect<T = any> { active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation * @internal */ computed?: ComputedRefImpl<T> /** * @internal */ allowRecurse?: boolean /** * @internal */ private deferStop?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope ) { } run() { // ... } stop() { // ... } }
这样看肯定还是不怎么得劲,我再来帮大家整理一下:
interface ReactiveEffect<T = any> { // 副作用函数 fn: () => T; // 调度器 scheduler: EffectScheduler | null; // 当前 副作用函数 是否处于活动状态 active: boolean; // 当前 副作用函数 的所有依赖 deps: Dep[]; // 当前 副作用函数 的父级 副作用函数 parent: ReactiveEffect | undefined; // 计算属性,可以在创建副作用函数之后再赋值 computed?: ComputedRefImpl<T>; // 是否允许递归 allowRecurse?: boolean; // 是否延迟停止,这是个私有属性 deferStop?: boolean; // 停止时的回调函数 onStop?: () => void; // 只有开发环境才会有的属性 onTrack?: (event: DebuggerEvent) => void; onTrigger?: (event: DebuggerEvent) => void // 执行副作用函数 run(): void; // 停止副作用函数 stop(): void; }
这里的最需要关心的其实是scheduler
调度器,其他的属性几乎都不受用户控制,它们都是在运行时,因为处理各种问题而产生的;
例如active
属性是在当前副作用函数执行的时候会被设置为true
,在执行完毕之后会被设置为false
,因为可能会出现嵌套副作用函数的情况,副作用函数可能会相互影响,就需要通过active
属性来判断当前副作用函数是否处于活动状态;
例如deps
属性是在当前副作用函数执行的时候会被设置为当前副作用函数所依赖的所有属性,这样在下一次执行副作用函数的时候,就可以通过deps
属性来判断当前副作用函数是否需要重新执行;
还有其他的一些属性,大家感兴趣可以自己去深挖一下,这里就不一一分析了,而我们这次主要分析的是scheduler
调度器;
scheduler
scheduler
调度器是什么?可以看到ts
源码会对应这个类型,来看看:
export type EffectScheduler = (...args: any[]) => any
这个类型的定义很简单,就是一个函数,这个函数可以接收任意数量的参数,返回值是任意类型的值;
它的目的是可以让我们自定义副作用函数的执行方式,通常情况下我们在使用副作用函数的时候是会直接执行的,但是有时候我们可能需要自定义副作用函数的执行方式;
大家可以尝试如下代码:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0 }); effect(() => { console.log(state.count); }, { scheduler: () => { console.log('scheduler'); }, }); state.count++;
可以看到,我们在创建副作用函数的时候,通过scheduler
属性来自定义了副作用函数的执行方式,这个时候并不会直接执行副作用函数,而是会通过scheduler
属性来执行副作用函数;
而scheduler
属性是我们自定义的,所以是需要我们手动来执行副作用函数的,这个时候我们可以通过scheduler
属性来实现一些自定义的功能,例如:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0 }); const runner = effect(() => { console.log(state.count); }, { scheduler: () => { console.log('scheduler'); if (state.count % 2 === 0) { runner(); } }, }); state.count++; state.count++; state.count++; state.count++; state.count++; state.count++;
还记得effect
会返回一个runner
函数吗?这个runner
函数就是用来执行副作用函数的,这里通过scheduler
属性来实现了一个自定义的功能,就是当state.count
是偶数的时候,才会再次执行副作用函数;
这个玩意儿可以用来做什么呢?例如我们可以通过scheduler
属性来实现一个防抖的功能,例如:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0 }); let timer = null; const runner = effect(() => { console.log(state.count); }, { scheduler: () => { console.log("scheduler"); clearTimeout(timer); timer = setTimeout(() => { runner(); }, 300); }, }); state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++; state.count++;
可以看到这里只有两次打印state.count
的值,所以方案完全可行,当然你也可以做一些其他的功能;
这里就再提一嘴watch
方法,通过讲解scheduler
属性,是不是发现和watch
方法的行为很相似呢?我们用scheduler
属性来实现一个简单的watch
方法:
import {reactive, effect} from 'vue'; const state = reactive({ count: 0 }); function watch(getter, cb, options = {}) { let oldValue = getter(); const runner = effect(() => { const newValue = getter(); if (oldValue !== newValue) { cb(newValue, oldValue); oldValue = newValue; } else if (options.immediate === true) { cb(newValue, undefined); } }, { scheduler() { runner(); } }); } watch( () => state.count, (newValue, oldValue) => { console.log(newValue, oldValue); } ) watch( () => state.count, (newValue, oldValue) => { console.log(newValue, oldValue); }, { immediate: true } ) state.count++;
这个结果大家自行尝试一下,这里就不贴图了;
lazy
lazy
属性是用来控制副作用函数是否在创建的时候就执行一次的,如果设置为true
的话,那么副作用函数就不会在创建的时候就执行一次,而是需要手动执行一次;
例如我们上面的示例,每次runner
函数都会至少执行两次,第一次是在创建副作用函数的时候,后面就都是响应式对象的值发生变化的时候;
如果我们设置lazy
属性为true
的话,那么副作用函数就不会在创建的时候就执行一次,而是需要手动执行一次,例如:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0 }); const runner = effect(() => { console.log(state.count); }, { lazy: true, }); state.count++; state.count++; state.count++;
上面这样是不会有任何打印的,因为副作用函数没有执行,没有执行就不会收集依赖,所以也就不会有任何打印;
需要注意的是要在响应式对象发生变化之前手动执行一次runner
函数,否则就不会有任何打印;
我们手动执行一次runner
函数,就可以看到打印了三次state.count
的值,这就是lazy
属性的作用,也是lazy
属性使用的一个细节;
其他杂项
effect
方法还有一些其他的属性,例如onTrack
、onTrigger
、onStop
等,这些属性都是用来做一些额外的事情的,例如:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0 }); const runner = effect( () => { console.log(state.count); }, { onTrack(event) { console.log("onTrack", event); }, onTrigger(event) { console.log("onTrigger", event); }, onStop() { console.log("onStop"); } } ); state.count++; state.count++; state.count++; runner.effect.stop(); state.count++; state.count++;
这里的onTrack
和onTrigger
都是在开发环境下才会有的,主要用来调试当前的副作用函数有多少依赖,以及依赖发生了什么变化;
看名字就知道,onTrack
是在收集依赖的时候触发的,onTrigger
是在依赖发生变化的时候触发的;
onStop
是在副作用函数被停止的时候触发的,例如上面的示例,我们手动调用了runner.effect.stop()
方法,那么就会触发onStop
方法;
并且调用runner.effect.stop()
方法之后,再次修改state.count
的值,也不会有任何打印,因为副作用函数已经被停止了;
总结
effect
方法是Vue3
中用来创建副作用函数的方法,通过effect
方法可以创建一个副作用函数,副作用函数可以收集依赖,当依赖发生变化的时候,就会重新执行副作用函数;
effect
方法还有一些其他的属性,例如lazy
、onTrack
、onTrigger
、onStop
等,这些属性都是用来做一些额外的事情的;
当我们了解了effect
方法之后,后面会发现Vue3
中的watch
、computed
、watchEffect
等方法都是基于effect
方法实现的;
这为我们后续了解Vue3
的源码打下了基础,effect
方法还有很多细节,但是已经不在使用上体现了,而是在依赖收集和依赖触发的时候体现;
下一篇文章我们会来看看Vue3
中是如何收集依赖的,以及依赖是如何触发的,同这篇文章一样,也是对上上篇文章的一个补充;