前言
之前介绍了reactive
和ref
,接下来就是effect
了,effect
可以理解为依赖收集的过程,还是通过代码来看一下(简化代码中可以结合index1.html
看一下)
依赖收集
在reactive
和ref
中均有对依赖的收集,get
阶段和set
阶段分别会触发track/trackEffects
和trigger/triggerEffects
reactive
相关源码如下:
// get
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
}
}
// set
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
}
}
ref
相关源码如下:
export function trackRefValue(ref: RefBase<any>) {
if (isTracking()) {
ref = toRaw(ref)
// 没有dep添加dep属性,Set类型
if (!ref.dep) {
ref.dep = createDep()
}
trackEffects(ref.dep)
}
}
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
// 存在dep属性,通知依赖
if (ref.dep) {
triggerEffects(ref.dep)
}
}
class RefImpl<T> {
get value() {
trackRefValue(this)
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
triggerRefValue(this, newVal)
}
}
}
从源码中挑出了触发依赖收集的相关代码,这部分并不难理解,get
阶段触发track
或trackEffects
收集依赖(收集观察者),set
阶段触发trigger
或triggerEffects
触发依赖(通知观察者),而在effect
中track
最终会触发trackEffects
,trigger
最终会触发triggerEffects
,所以接下来从track
开始对effect
部分的解读,基本可以将整个过程分析完毕,不从trigger
开始的原因是没有依赖何来的触发依赖
track
track
代码如下(源码简化后):
const targetMap = new WeakMap()
let activeEffect
const createDep = (effects) => {
const dep = new Set(effects)
return dep
}
function isTracking() {
return activeEffect !== undefined
}
function track(target, type, key) {
if (!isTracking()) return
// 是否有缓存
let depsMap = targetMap.get(target)
// 无缓存存储,初始化为map
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取当前属性是否有缓存
let dep = depsMap.get(key)
// 无缓存存储,初始化为set
if(!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep);
}
function trackEffects(dep) {
// 是否存在
let shouldTrack = !dep.has(activeEffect)
// 不存在收集依赖
if(shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
代码中已经有部分注释,可以看出经过一些优化后,最终将activeEffect
加入到dep
中,数据存储关系如下: targetMap<weakMap> -> depsMap<Map> -> dep<Set> -> activeEffect<ReactiveEffect>
,接下来看一下activeEffect
是如何定义的,首先看一下effect
干了些什么
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn)
// 存在合并入_effect
if(options) extend(_effect, options)
// 初始执行run
_effect.run()
const runner = _effect.run.bind(_effect)
runner.effect = _effect
// 将runner返回出去,让外部可以主动调用run
return runner
}
初始化(或合适时机)调用effect
时会将ReactiveEffect
实例化并执行run
,执行run
时会将当前ReactiveEffect
实例赋值给activeEffect
(代码在ReactiveEffect
部分),最终将runner
返回出去,不仅在数据更新时可以调用,同样可以选择在合适的时机主动触发,这里涉及到了ReactiveEffect
,看一下实现:
class ReactiveEffect {
active = true
deps = []
constructor(fn, scheduler) {
this.fn = fn
// 自定义scheduler,若存在触发triggerEffects时执行,否则执行fn
this.scheduler = scheduler
}
run() {
// 将当前实例赋值给activeEffect
activeEffect = this
return this.fn()
}
stop() {
if(this.active) {
cleanupEffect(this)
this.active = false
}
}
}
// 清空effect
function cleanupEffect(effect) {
effect.deps.forEach((dep) => dep.delete(effect))
effect.deps.length = 0
}
function stop(runner) {
runner.effect.stop();
}
抛去其他考虑,track
的最简流程如下:
const targetMap = new WeakMap()
class ReactiveEffect {
active = true
deps = []
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
const runner = _effect.run.bind(_effect)
return runner
}
function track(target, key) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
trackEffects(dep);
}
function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect)
if(shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
场景假设
假设一个最简单的场景,更新id=app
的dom
:
const state = reactive({ a: 100 })
effect(
() => document.getElementById('app').innerText = state.a
)
当调用effect
时,fn即为() => document.getElementById('app').innerText = state.a
,activeEffect
被初始化。当调用state.a
时会将当前activeEffect
添加到dep
现在有这么一个操作state.a = 0
,则触发trigger
,如果trigger
能够触发当前activeEffect
中的run
就可已更新dom
,关系如下:
get -> track -> 收集activeEffect
set -> trigger -> 告知activeEffect
接下来看一下trigger
trigger
function trigger(
target,
type,
key,
newValue,
oldValue
) {
// 获取缓存
const depsMap = targetMap.get(target)
// 无缓存表示从来没有触发过track,直接return
if (!depsMap) return
let deps = [];
if (key !== void 0) deps.push(depsMap.get(key))
const effects = []
// 取出set数据
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
function triggerEffects(dep) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
if (!depsMap) return
这个动作还是比较重要的,targetMap
中如果没有该值则表示没有触发过track
,也就是没有观察者(没有通知对象),trigger
最后会取出所有effect
然后触发triggerEffects
,当开发者传入了scheduler
触发scheduler
,否则触发run -> fn
ref
ref
模块是添加了一个dep
属性然后直接调用trackEffects
、triggerEffects
,基本是上边的搞清楚这个很容易就搞懂了,不再过多解释
get -> trackEffects -> 收集activeEffect
set -> triggerEffects -> 告知activeEffect
结语
配合着简化代码操作一下方便理解