effect 会立即执行传入的函数,并在函数的依赖发生变化时重新运行该函数(后文统一称为依赖函数)
列表查询是业务中最常见的需求,通常是默认查询一次,当页码、分页大小、关键词发生变化时重新查询,一般我们是在onMounted中触发一次查询,分页,关键词变化时手动触发下查询,如果用efftect,代码会精简很多。
import { effect, reactive } from 'vue'
const state = reactive({
pageIndex: 1,
pageSize: 20
})
effect(() => {
let params = {
pageNum: state.pageIndex,
pageSize: state.pageSize
}
request({
method: 'post',
url: 'xxx',
params
})
})
众所周知vue3是用了Proxy把对象进行了代理,在get中进来依赖搜集,在set中触发依赖函数的执行,具体是怎么操作的呢,我们来看下源码。
后文的源码中会去掉处理边界条件的部分,这部分不影响对源码的理解。
// 用createReactiveObject 函数创建响应式,对象类型和集合类型分开处理,收集的依赖放在reactiveMap中
export function reactive(target: object) {
return createReactiveObject(
target, // 代理的对象
false, // 是否是只读
mutableHandlers, // 为对象创建响应式
mutableCollectionHandlers, // 为集合类型创建响应式
reactiveMap // 保存对象的代理结果
)
}
reactiveMap 是一个WeakMap,WeakMap 可以把对象作为key,value可以是任何类型,用来判断当前对象是否代理过。
export const reactiveMap = new WeakMap<Target, any>()
进入createReactiveObject函数中
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果已经代理过就直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 省去边界条件处理后,这个函数就是 new Proxy, 然后再把 proxy和target放在reactiveMap中
// reactiveMap 就是 这个函数里的proxyMap
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers // 判断类型
)
proxyMap.set(target, proxy)
return proxy
}
进入baseHandlers文件中,Proxy的 get 和 set 是通过createGetter 和 createSetter 这两个函数生成的
// 依赖收集完成后 返回值
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const targetIsArray = isArray(target)
// 在用了Proxy后,数组和对象可以一起处理了
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
// 这个track就是进行依赖收集的函数
track(target, TrackOpTypes.GET, key)
}
return res
}
}
targetMap 是依赖存放的对象,大致是 对象——对象key——key对应的依赖这样的结构
const targetMap = new WeakMap<any, KeyToDepMap>()
// eg
const state = reactive({
age: 18,
name: '小明'
})
// 对state这个响应式对象来说,大致的依赖结构如下
// 在后文中会继续分析每一层具体的类型
{
state: {
age: [依赖1, 依赖2,依赖3],
name: [依赖1, 依赖2,依赖3]
}
}
export let activeEffect: ReactiveEffect | undefined
export let shouldTrack = true
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 先忽略shouldTrack,activeEffect这两个变量
// 直接看if里面的代码,就是存放依赖的过程
if (shouldTrack && activeEffect) {
// 1、先判断对象有没有进行过依赖收集
let depsMap = targetMap.get(target)
if (!depsMap) {
// targetMap的key是对象,值是Map类型的
targetMap.set(target, (depsMap = new Map()))
}
// 2、再判断对象的key有没有进行过依赖收集
// dep 就是依赖最终存放的地方
let dep = depsMap.get(key)
if (!dep) {
// createDep() 返回的是一个 Set 可以看到依赖最终是放在一个Set中
depsMap.set(key, (dep = createDep()))
}
// 到这里用于存放依赖的depsMap创建好了,其结构是
// WeakMap - Map - Set
// trackEffects 就是将依赖放在dep这个set中
trackEffects(dep)
}
}
// 可以看出activeEffect最终是add到dep中,可以猜测activeEffect就是依赖
export function trackEffects(dep: Dep) {
let shouldTrack = false
// 判断是否存过依赖
shouldTrack = !dep.has(activeEffect)
if (shouldTrack) {
dep.add(activeEffect)
activeEffect!.deps.push(dep)
}
}
可以先回到文章开头看下effect的用法
export function effect<T = any>(
fn: () => T, // fn就是文章开头那段执行接口查询的代码
options?: ReactiveEffectOptions // effect可以传入第二个参数,第二个参数中有lazy时可以手动控制依赖函数调用的时机
): ReactiveEffectRunner {
// ReactiveEffect 就是跟activeEffect, shouldTrack密切相关的类
const _effect = new ReactiveEffect(fn)
// 这里调用了run方法,往下看ReactiveEffect就可以理解run是什么了
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export class ReactiveEffect<T = any> {
deps: Dep[] = []
constructor(
public fn: () => T, // fn是依赖函数
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
// 可以先忽略
recordEffectScope(this, scope)
}
// 下面是关键, shouldTrack和activeEffect都登场了
run() {
let lastShouldTrack = shouldTrack
try {
// 原来activeEffect就是ReactiveEffect
activeEffect = this
shouldTrack = true
// 在effect中会默认执行run函数,当执行run函数时,就会执行fn依赖函数
// 执行依赖函数时,就会触发对象的get,get触发后就会执行track
// track 里面的 activeEffect 就是这里的 this
// ReactiveEffect 就是依赖,执行依赖的run就是执行依赖函数
// 可以想像出set过程就是执行dep里面所有ReactiveEffect的run方法的过程
return this.fn()
} finally {
shouldTrack = lastShouldTrack
}
}
}
至此依赖搜集的过程就分析完了,从effect函数为入口总结下:
在ReactiveEffect中有段很精彩的位运算代码,可以单独写一篇文章分析。
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
const result = Reflect.set(target, key, value, receiver)
// trigger 值改变时触发依赖函数
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 从depsMap中获取依赖
let deps: (Dep | undefined)[]
deps.push(depsMap.get(key))
// 执行依赖
triggerEffects(deps[0])
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
) {
const effects = isArray(dep) ? dep : [...dep]
// computed 计算属性也是利用effect实现的,ReactiveEffect中还有个computed
// 下面两个for循环,先触发计算属性的依赖,再触发非计算属性的依赖
// 可以看到计算属性的依赖函数是优先触发的
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect)
}
}
}
function triggerEffect( effect: ReactiveEffect) {
// 在get中就猜测,触发依赖就是执行ReactiveEffect的run方法
effect.run()
}
至此vue3的响应式原理分析完毕,从API使用的作为入口看vue3的源码是一个不错的方式。