「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」
上一篇阅读了响应式系统中关于如何创建响应式对象相关的源码,即通过Proxy对target目标对象进行代理,并通过Proxy的馅饼函数对对象的操作进行劫持。在get函数中会根据入参决定是创建reactive、shallowReactive,还是readonly代理等,并且满足条件下会触发track函数追踪收集缓存数据到targetMap,这里targetMap是WeakMap的数据类型。。。
OK,更详细的内容,可通过如下链接点击阅读,当然targetMap为啥是WeakMap类型,而不是Map呢?
简单说明下:首先WeakMap的键名只能是对象,并且是对对象的弱引用,即垃圾回收机制不会将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
下面进入主题。
track
track追踪收集,是在执行代理对象的get陷阱函数进行的操作,它是将数据缓存到targetMap。下面看看具体的代码实现:
export function track(target: object, type: TrackOpTypes, key: unknown) {
// shouldTrack:是否应该收集依赖
// activeEffect:当前激活的efftct,也就是effect的回调函数,数据变化后执行的副作用函数
if (!shouldTrack || activeEffect === undefined) {
return
}
// targetMap就是缓存数据的对象,weakMap类型
// 每个target对应一个depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个key对应一个dep,注意这里是set集合类型
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的effct作为依赖
dep.add(activeEffect)
// 激活的activeEffect收集dep作为依赖
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
框架是实现响应式系统,也就是当数据变化时能够做出一些响应。所以,可以猜出,这里收集的依赖,应该就是数据变化后执行的副作用函数。
// 其他代码省略
effect(() => {
// 副作用函数,收集的应该是这玩意
patch()
})
通过track我们也可以看出targetMap数据结构是这样的:
// targetMap简易描述
WeakMap -> {
[target:代理对象]:{
[key:代理对象的key]:new Set(effect)
}
}
接着上面的例子,再来看看,这里activeEffect具体是如何收集的。
effect
上面例子,我们猜测track收集追踪的是effect的内容,这里看下代码。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
// 如果已经是effect函数,则指向原始函数
fn = fn.raw
}
// 创建响应式的副作用函数
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// 非lazy,则直接执行一次,这里lazy属性,computed计算属性会用到
effect()
}
return effect
}
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {// 未激活状态
// 如果是非调度执行,则直接执行fn,这里是原始函数,effect入参时已做了转换
return options.scheduler ? undefined : fn()
}
// effectStack:effect全局栈
if (!effectStack.includes(effect)) {
// 清空effect引用的依赖
// 通过遍历effect.deps保存的effect,清空effect
cleanup(effect)
try {
// 设置 sholdTrack = true,允许收集依赖
enableTracking()
// effect压入全局栈
effectStack.push(effect)
// 设置effect为当前激活effect
activeEffect = effect
// 执行原始函数
return fn()
} finally {
// 出栈
effectStack.pop()
// 重置 sholdTrack = false
resetTracking()
// 指向最后一个effect
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
// effect函数表示
effect._isEffect = true
// 是否激活状态
effect.active = true
// 包装的原始函数
effect.raw = fn
// effect对应的依赖
effect.deps = []
// effect相关配置
effect.options = options
return effect
}
不出意料,activeEffect
确实是通过 effect
函数进行的赋值初始化。这里的createReactiveEffect
函数看着是不是头大,但我们知道它是为了创建一个新的effect函数。抛开框架业务上的实现,我们看看函数的本质:
// 这里是给fn函数包裹了一层wrapper,并赋值给activeEffect
var activeEffect;
function wrapper(fn){
var effect = function(...args){
activeEffect = fn;
fn(...args)
}
return effect
}
看到这里,我们知道了effect主要是将全局的activeEffect变量指向当前effect,然后执行被包裹的原始函数fn。 从track追踪收集数据,以及副作用函数是如何被处理收集入缓存的,现在看看如何触发执行。
trigger
trigger函数是在代理对象触发set陷阱函数执行的,用于执行副作用函数。看看代码:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 从依赖收集缓存对象targetMap获取target的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 不存在,则返回,也就是未被追踪track
return
}
// 创建运行的effects集合
const effects = new Set<ReactiveEffect>()
// 用于添加effects的函数
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect))
}
}
if (type === TriggerOpTypes.CLEAR) {
// 清楚集合类型,添加effect
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
//SET | ADD | DELETE 类型之一,添加effect
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// 用于执行effect
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// 执行调度
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
// 直接执行
effect()
}
}
// 遍历执行effect
effects.forEach(run)
}
简而言之,trigger
主要实现了从 targetMap
拿到 target
对应的依赖集合 depsMap
,根据 key
从depsMap
找对对应的 effects
并添加到运行的 effects
集合中,然后遍历运行时的 effects
执行相关的副作用函数。
到这里我们大致讲解了reactive api的大致实现思路,当然关于具体的实现细节并没有深究,比如上面的添加 effect
,为啥要先执行一次 cleanup
呢?
Ref API
前面的例子,都是围绕reactive api来分析响应式系统的实现,我们知道这个api只能处理对象或者数据类型,对基础类型(比如:Number、String、Boolean)是不支持的。因此Vue3提供了ref API。看下代码
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
// 已经是ref类型,则返回自身
return rawValue
}
// 传入的是对象或数据,转换为reactive对象
let value = shallow ? rawValue : convert(rawValue)
const r = {
// ref对象标识
__v_isRef: true,
get value() {
// 收集依赖,key固定为value
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
// 判断value值是否存在修改
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
// 派发通知,执行副作用函数
trigger(r, TriggerOpTypes.SET, 'value', newVal)
}
}
}
return r
}
ref api实现通过创建具备value属性的对象,并对其set、get函数进行劫持,get函数进行收集数据依赖,set进行触发操作,并返回该对象来实现响应式。
总结
至此我们便分析了Vue3响应式系统的主体实现思路,感兴趣,你也可以根据该流程自主实现一个最小响应式系统。(如有不准确地方,欢迎留言指正)