vue3源码学习--副作用side effect

687 阅读4分钟

引言

书接上回,上回我们梳理了解了reactive的核心内部原理,这次我们聊聊effect的相关内容,探寻下它又有哪些神奇之处呢.....

副作用

effect全称叫side effect,副作用。

什么是副作用呢,一个函数运行后产生了可以影响其外部或可以看到的效果,就叫副作用,比如console.log,document. Body. Append,alert再或者是showModel(在页面中展示一个弹层),或者window.open打开一个新窗口。

哪些函数没有副作用呢,只用来计算结果的函数,比如Math.max,JSON.parse,它们的运行除了返回结果外不会有其它效果,这就叫不产生副作用。

基本上可以简化的理解为副作用就是执行某种操作,无副作用就是执行某种计算

基础使用

首先看下面 effect 的传参,fn 是回调函数,options 是传入的参数。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

其中 option 的参数如下,都是属于可选的。

参数

含义

lazy

是否延迟触发 effect

computed

是否为计算属性

scheduler

调度函数

onTrack

追踪时触发

onTrigger

触发回调时触发

onStop

停止监听时触发

export interface ReactiveEffectOptions {
  lazy?: boolean
  computed?: boolean
  scheduler?: (job: ReactiveEffect) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

代码实现

const rea = reactive({
    a: 1,
    b: 2,
    c: 3
})
// 1, 2 
effect(() => {
    console.log(rea.a, rea.b)
})
setTimeout(()=>{
    rea.a = 2
})

上面的的小例子会输出1,2,effect默认会执行一次,当其中的依赖数据变化了,会重新再次执行

原理如下:

// 主要是用栈的形式维护当前执行的effect和历史effect的关系let effectStack = []let activeEffect;class ReactiveEffect {    //标识当前是否被激活    flagActive = true;    //依赖收集的数组    deps = []    // 涉及effect依赖哪些属性、哪些属性有相关effect的多对多关系    constructor(public fn) {    }    run () {        if (!this.flagActive) {            this.fn()        }        try {            effectStack.push(activeEffect = this)            // 执行相关回调函数,会执行 reactive中的get函数            this.fn()        } finally {            effectStack.pop()            // 执行完上一个effect,出栈,获取最新的栈顶数据            activeEffect = effectStack[effectStack.length - 1]        }    }}/** * 提供给reactive中取值问题*/export function track (target, key) {}export function effect (fn) {    const _effect = new ReactiveEffect(fn);    _effect.run()}

验证case 1

effect(() => {
    console.log(rea.a)
    effect(() => {
        console.log(rea.b)
    })
    console.log(rea.c)
})
执行过程:
1、effectStack = [最外层effect] activeEffect = 最外层effect
2、effectStack = [最外层effect, 内部effect] activeEffect = 内部effect
3、内部effect: 收集 rea.b的属性
4、内部effect完成,出栈:effectStack = [最外层effect] activeEffect = 最外层effect
5、最终:内部effect: 收集 rea.b的属性, 最外层effect: 收集 rea.a 和 rea.c 的属性

循环问题

effect在什么情况下触发呢?effect会默认执行一次,同时当前effect收集的属性发生变化时会再次触发,我们看下面的示例:

case 2:

const rea = reactive({
    a: 1,
    b: 2
})
effect(()=>{
    rea.a +=1 //会怎么样
})

上面出现的情况很明显,就是effectStack中一直在追加当前的effect出现死循环,针对这种情况我们需要兼容下,如果当前effectStack中已经存在当前的 effect 对象就不需要再次触发了

// 主要是用栈的形式维护当前执行的effect和历史effect的关系let effectStack = []let activeEffect;class ReactiveEffect {    //标识当前是否被激活    flagActive = true;    //依赖收集的数组    deps = []    // 涉及effect依赖哪些属性、哪些属性有相关effect的多对多关系    constructor(public fn) {    }    run () {        if (!this.flagActive) {            this.fn()        }        // 判断是否已经存在当前effect --start        if(!effectStack.includes(this)){          // todo        }          // 判断是否已经存在当前effect --end    }}

effect与属性结构关系维护

同一个属性对应多个effect应该如何管理维护?

case 3:

const rea = reactive({
    a: 1,
    b: 2
})
effect1(() => {
    console.log(rea.a)
})
effect2(() => {
    console.log(rea.a)
})

因为 effect与依赖属性是多对多的关系,需要在track函数中处理这种关系:

  • 一个属性对应多个effect

  • 一个effect可能收集多个属性

最终代码如下

// 主要是用栈的形式维护当前执行的effect和历史effect的关系
let effectStack = []
let activeEffect;
const targetMap = new WeakMap()
class ReactiveEffect {
    //标识当前是否被激活
    flagActive = true;
    //依赖收集的数组
    deps = []
    // 涉及effect依赖哪些属性、哪些属性有相关effect的多对多关系
    constructor(public fn) {

    }
    run() {
        if (!this.flagActive) {
            this.fn()
        }
        // 判断是否已经存在当前effect
        if (!effectStack.includes(this)) {
            try {
                effectStack.push(activeEffect = this)
                // 执行相关回调函数,会执行 reactive中的get函数
                this.fn()
            } finally {
                effectStack.pop()
                // 执行完上一个effect,出栈,获取最新的栈顶数据
                activeEffect = effectStack[effectStack.length - 1]
            }
        }

    }
}

/**
 * @description 是否需要跟踪收集
 * @return {Boolean}  是否需要收集标识
 */
export function isTracking() {
    return activeEffect !== undefined
}

/**
 * 提供给reactive中取值问题  get调用
 */
export function track(target, key) {
    if (!isTracking()) {
        return
    }
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    let needTracked = !dep.has(activeEffect)
    if (needTracked) {
        // 一个属性对应多个effect
        dep.add(activeEffect)

        // 一个effect可能收集多个属性
        activeEffect.deps.push(dep)
    }

}

/**
 * @description 提供reactive中set调用
 */
export function trigger(target, key) {
    let depsMap = targetMap.get(target)
    // 修改的属性没有依赖,直接返回
    if (!depsMap) {
        return
    }
    // 存放 set集合
    let deps = []
    if (key !== undefined) {
        deps.push(depsMap.get(key))
    }
    let effects = []
    for (const dep of deps) {
        effects.push(...dep)
    }
    for (const effect of effects) {
        if (effect !== activeEffect) {
            effect.run()
        }
    }
}

export function effect(fn) {
    const _effect = new ReactiveEffect(fn);

    _effect.run()
}

针对vue3中effect的解析和实现,暂时告一段落。

未完待续.....