Vue3源码--响应式原理1(effect)
最近学习了下Vue3的源码,抽空写一些自己对3.x源码的解读,同时算是学习的一个总结吧,也能加深自己的印象。
就先从3.x的响应式系统说起吧。
回忆
首先大概回忆一下2.x的响应式系统,主要由这几个模块组成,Observer,Watcher,Dep。
Observer负责通过defineProperty劫持Data,每个Data都各自在闭包中维护一个Dep的实例,用于收集依赖着它的Watcher。Dep维护一个公共的Target属性,用于保存当前的需要被收集依赖的Watcher。每次Data被劫持的getter执行的时候,如果Dep.Target!==undefine
, dep和Watcher实例就互相收集对方~
2.x的响应式系统其实是围绕着Watcher,也可以说围绕着watch API的,包括render是一个renderWatcher,computed是通过lazyWatcher实现。这并不是一个好的设计模式,不符合六个设计原则的(单一职责原则,开闭原则)。而响应式系统也无法独立出来。
对比
那么3.x是怎样实现这一块的内容的呢。
首先3.x响应式系统相关的代码在packages/reactivity/src里。3.x的响应式系统的核心由两个模块构成: effect, reactive。
reactive模块的功能比较简单,就是给数据设置代理,类似于2.x的Observer,不同的点在于是用的Proxy去做代理。
effect模块,传入一个函数,然后让这个函数需要被响应式数据影响,目前具体在3.x中包括,watch API,computed API,还有组件的更新都是依赖effect实现的,但是这个模块没有暴露在Vue对象上面。所以说effect模块是一个偏向于底层只有基础功能的模块,相比2.x,这明显是一个较好的设计模式。
Effect
关于effect模块,最主要的是里面的effect,track,trigger三个方法。
effect方法是一个高阶函数,或者也可以说是工厂方法,接收一个函数作为参数,返回一个effect实例方法,它使这个函数中的响应式数据可追踪到这个effect实例,如果有响应式数据发生了改变,就会再次执行这个effect,可以参照源码中调用这个方法的三个地方computed.ts
,apiWatch.ts
,renderer.ts
。
首先来看看track:以下是track方法的主要逻辑以及注释,track方法按字面的解释就是追踪,会在数据Proxy的get代理中调用,track这个数据本身。其实简单说就做了一件事情,把当前的active effect收集到响应式数据的depsMap里面。
其实并不复杂,这里和2.x不同的是,2.x是每个数据各自都在闭包中维护deps对象,这里是用一个全局的Store去保存响应式数据影响的effects,实现了模块的解耦。
// target为传入的响应式数据对象,type为操作类型,key为target上被追踪的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果shouldTrack为false 或者 当前没有活动中的effect,不需要执行追踪的逻辑
// shouldTrack为依赖追踪提供一个全局的开关,可以很方便暂停/开启,比如用于setup以及生命周期执行的时候
if (!shouldTrack || activeEffect === undefined) {
return
}
// 所有响应式数据都是被封装的对象,所以用一个Map来保存更方便,Map的key为响应式数据的对象
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 同样为每个响应式数据按key建立一个Set,用来保存target[key]所影响的effects
let dep = depsMap.get(key)
if (dep === void 0) {
// 用一个Set去保存effects,省去了去重的判断
depsMap.set(key, (dep = new Set()))
}
// 如果target[key]下面没有当前活动中的effect,就把这个effect加入到这个deps中
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
看完track方法的逻辑之后,effect方法的主要逻辑其实就呼之欲出了,那就是启动响应式追踪---设置shouldTrack为true,设置activeEffect为当前的effect,然后再调用传入的方法并追踪依赖,最后返回一个封装后的实例effect方法。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// createReactiveEffect是一个工厂方法,返回一个函数实例
const effect = createReactiveEffect(fn, options)
// 如果不是lazy effect(lazy effect主要用于computed),立即执行这个effect
if (!options.lazy) {
effect()
}
return effect
}
// createReactiveEffect是一个工厂方法,返回一个函数实例
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
// 如果effect.active为false,跳过追踪直接调用传入的函数
if (!effect.active) {
return fn(...args)
}
if (!effectStack.includes(effect)) {
// 清除effect中之前记录的deps
cleanup(effect)
try {
// 设置shouldTrack为true
enableTracking()
// 设置activeEffect为当前的effect,另外把当前的effect入栈(比如渲染子组件的时候,这个栈就起作用了)
effectStack.push(effect)
activeEffect = effect
// 执行传入effect的函数
return fn(...args)
} finally {
effectStack.pop()
// 设置shouldTrack为上一次的shouldTrack(注:和effect一样,shouldTrack也有一个栈)
resetTracking()
// 设置activeEffect为上一个activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
最后来看一下trigger方法,trigger方法的调用在Proxy的set代理中,作用就是在修改一个响应式数据的时候,执行这个响应式对象的depsMap中所有的effect。
// target为修改的响应式数据对象,type为操作类型,key为target上具体修改的参数
// newValue,oldValue, oldTarget都很好理解
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 如果操作类型是CLEAR,说明数据类型是Map,或者Set(注意,3.x的响应式系统是支持Map和Set的)
// CLEAR操作需要触发集合上的所有属性的effects
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(dep => {
// addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect
addRunners(effects, computedRunners, dep)
})
// 如果是更改length长度,说明是个数组,只需要触发key在这个新的length之后的数据
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
addRunners(effects, computedRunners, dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// 大部分的情况,触发这个key下面的effets
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
if (
type === TriggerOpTypes.ADD ||
type === TriggerOpTypes.DELETE ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
// 如果是添加/删除数组里的项,或者Set,Map的add,delete,set几个方法,同时也会改变length或者size,
// 在Map和Set里面,受size影响的一些方法(比如size,forEach,entries,keys,values),都会把effect收集到ITERATE_KEY里面。
// 具体可参考packages/reactivity/src/collectionHandler.ts里面的实现
const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
scheduleRun(
effect,
target,
type,
key,
__DEV__
? {
newValue,
oldValue,
oldTarget
}
: undefined
)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
// run每个effect
computedRunners.forEach(run)
effects.forEach(run)
}
// addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect
// 普通的effect存在effects里面,computed effect存在computedRunners里面
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 调度将要执行的effect,是否传入effect.options.scheduler决定了执行的方式
// 若没有传入,就立即同步执行,若有,则执行调度方法,传入effect
// 3.x中关于异步调度方法的实现可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法
function scheduleRun(
effect: ReactiveEffect,
target: object,
type: TriggerOpTypes,
key: unknown,
extraInfo?: DebuggerEventExtraInfo
) {
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect)
} else {
effect()
}
}
以上源码都是基于 vue-next-alpha8 版本。
effect模块相关的内容就这些,下一篇是关于reactive模块的。