第二节:vue3-Reactivity模块-effect

277 阅读5分钟

副作用函数 (effect执行渲染了页面) 如果此函数依赖的数据发生了变化 会重新执行

使用方法

 const {effect, reactive} = VueReactivity
 // effect 代表
 // reactive 将数据变成相应式的 proxy
 const state= reactive({name: 'lyp',age:'30'})
 console.log(state)
 effect(()=>{ // 此 effect 会默认执行一次,对响应式数据取值(取值的过程中数据会依赖当前的effecgt)
    state.age = Math.random()
    document.getElementById('app').innerHTML = state.name + '今年' + state.age +'岁了'
 })
 setTimeout(() => {
     state.age++
 }, 1000)

逻辑

  • 1、默认执行一次
  • 2、如果非激活的状态 只需要执行函数 不是的话 把activeEffect 暴露到全局 方便别的模块取到
  • 3、嵌套执行处理
  • 4、执行的时候 获取数据 进行依赖收集
  • 5、每次执行effect的时候都要将之前收集的内容清空
  • 6、停止effect的实现 在 作用域effect(effectScope)中有用到
  • 7、更新的时候判断是否有调度函数 实现组件的异步更新

入口方法

export function effect(fn, options:any = {}) {
    // 这里fn 可以根据状态变化重新执行, effect可以嵌套写  
    
    // 创建响应式的 effect
    const _effect = new ReactiveEffect(fn, options.scheduler) 
    _effect.run() // 默认执行一次
    const runner = _effect.run.bind(_effect) // 绑定this指向
    runner.effect = runner // 将effect 挂在到runner函数上 才能找到stop
    return runner
}

编写effect函数

export let activeEffect = undefined;// 当前正在执行的effect

class ReactiveEffect {
    // 这里表示在实例上新增了active属性
    public active = true // 这个 effect默认是激活状态
    deps = []; // 收集effect中使用到的属性
    public parent = null // 用来存储actEffect上一层的effect
    // 用户传递的参数 也会到this上  this.fn = fn
    constructor(public fn, public scheduler) { 

    }
    run() {
        // 如果非激活的状态 只需要执行函数 不需要进行依赖收集
        if (!this.active) {
            return this.fn();
        }
        // 这里就要依赖收集  核心就是 当前的effect 和稍后渲染的属性关联在一起
        try {
            // 存储前一个 effect赋值给 parent  第一次是null(如果是嵌套的effect会滞后一层)
            this.parent = activeEffect; 
            // 更新activeEffect 设置成正在激活  暴露给外部
            activeEffect = this; 
            
            // 这里需要在每次effect执行之前 将之前收集的内容清空  activeEffect.deps = [Set[],Set[]]
            cleanupEffect(this)
            return this.fn();
        } finally {
            // 执行完毕后还原activeEffect  用parent去还原(parent 是上一层的effect)
            activeEffect = this.parent; 
        }

    }
    stop(){
        if(this.active){ 
            cleanupEffect(this)  // 清空依赖
            this.active = false // 停止
        }
    }
}
export function effect(fn, options?) {
    const _effect = new ReactiveEffect(fn); // 创建响应式effect
    _effect.run(); // 让响应式effect默认执行
}

嵌套执行处理 逻辑

老方法 使用栈给最后一个赋值 [e1] -> 进入e2(加入[e1,e2]) -> e2结束删除e2 ([e1])


effect(()=>{  //effect1
    state.name
    effect(()=>{   //effect2
            state.age
    })
    state.address //effect1
})

// 这个执行流程 类似于树形结构  记录parent

effect(()=>{   // parent = null  activeEffect = e1
    state.name    //name ->e1
    effect(()=>{   // parent = e1  activeEffect = e2
            state.age
    })
    state.address  // e2结束 activeEffect=this.parent=e1
}) //e1结束 activeEffect=this.parent=null

依赖收集

默认执行effect时因为取了一次值会调用属性的get方法并且将当前的activeEffect暴露到了外边,因此会对属性,进行依赖收集

一个effect对应多个属性 一个属性对应多个effect;多对多的关系

实现原理

对象 的 某个属性 被 多个effect 依赖

WeekMap = { 对象target: Map{对象的属性key: Set[多个effect]} }

正向记录指:属性记录了所有的effect;反向记录 effect记录被哪些属性收集过,作用是为了方便清理依赖

// 上一章reactive模块 的get方法
get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);  // 依赖收集
    return res;
}

一共三层:targetMap(类型WeekMap)、depsMap(类型Map)、dep(类型Set)

// 依赖收集
const targetMap = new WeakMap(); // 记录依赖关系
export function track(target, type, key) {
    if (activeEffect) { // 存在activeEffect才继续执行
        let depsMap = targetMap.get(target); // {对象:map} 第一次没有
        if (!depsMap) { // 没有的话记录依赖
            // WeakMap: {target: Map}; depsMap = Map{}
            targetMap.set(target, (depsMap = new Map())) 
        }
        let dep = depsMap.get(key); // 在map中查找是否对属性做了依赖收集、 第一次没有
        if (!dep) {  // 没有的话 记录
            depsMap.set(key, (dep = new Set()))// WeakMap{target{ key :Set[]}}; dep = Set[]
        }
        let shouldTrack = dep.has(activeEffect) // 是否已收集 Set中不存在activeEffect 就存进去、第一次没有记录
        if (!shouldTrack) {
            dep.add(activeEffect);
            // 让effect记住对应的dep(反向记录),这样后续可以用于清理 activeEffect.deps= [Set,Set]
            // 记录的是每个属性对应的effect
            activeEffect.deps.push(dep); 
        }
    }
}

将属性和对应的effect维护成映射关系,后续属性变化可以触发对应的effect函数重新run

触发更新

// 上一章reactive模块 的set方法
set(target, key, value, receiver) {
    // 等会赋值的时候可以重新触发effect执行
    let oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver);
    if (oldValue !== value) {
        trigger(target, 'set', key, value, oldValue)
    }
    return result;
}
export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target); // 获取对应的映射表 没人依赖 就是没人使用不用更新
    if (!depsMap) {
        return
    }

    let effects = depsMap.get(key); // 找到了 属性对应的effect 没有的话  就是没人使用不用更新
    if(effects){
        // 永远在执行之前 先拷贝一份在执行  防止死循环
        effects = new Set(effects)
        effects.forEach(effect => {
            //effect !== activeEffect 是为了 防止死循环 因为activeEffect 是当前的effect 默认已经执行了;
            // run执行用户操作
            if (effect !== activeEffect){
                if(effect.scheduler){
                    effect.scheduler()  // 如果用户传了scheduler 就让scheduler()
                }else{
                    effect.run() // 否则默认刷新视图
                }
            }; 
        })
        triggerEffects(effects)
    }
}

分支切换与cleanup

在渲染时我们要避免副作用函数产生的遗留

const state = reactive({ flag: true, name: 'lyp', age: 30 })
effect(() => { // 副作用函数 (effect执行渲染了页面)
    console.log('render')
    document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
    state.flag = false;
    setTimeout(() => {
        console.log('修改name,原则上不更新')
        state.name = 'zf'
    }, 1000);
}, 1000)
function cleanupEffect(effect) {
    // 清理effect deps是effect对应的多个属性
    // deps的某一项 里面装的是该属性(例如name、age)对应的effect
    const { deps } = effect; 
    // 循环每个属性 清理所有属性 对 当前effect 的记录
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect);
    }
    // 清理 当前effect 对 所有属性的记录
    effect.deps.length = 0;
}
class ReactiveEffect {
    active = true;
    deps = []; // 收集effect中使用到的属性
    parent = undefined;
    constructor(public fn) { }
    run() {
        try {
            this.parent = activeEffect; // 当前的effect就是他的父亲
            activeEffect = this; // 设置成正在激活的是当前effect
            
            
            // 这里需要在每次effect执行之前 将之前收集的内容清空  activeEffect.deps = [Set[],Set[]]
            cleanupEffect(this);
            
            
            return this.fn(); // 先清理在运行
        }
    }
}

set死循环

触发时会进行清理操作(清理effect),在重新进行收集(收集effect)。在循环过程中会导致死循环。

let effect = () => {};
let s = new Set([effect])
s.forEach(item=>{s.delete(effect); s.add(effect)}); // 这样就导致死循环了

stop 的使用

let runner =  effect(() => { // 副作用函数 (effect执行渲染了页面)
    console.log('render')
});
runner.effect.stop()

调度执行

调度的使用 实现组件的异步更新

let waiting = false
let runner =  effect(() => { // 副作用函数 (effect执行渲染了页面)
    console.log('render')
}, {
    scheduler(){  // 调度 如何更新自己决定
        if(!waiting){
            waiting = true
            setTimeout(() => {
                runner()
                waiting = false
            })
        }
    }
});

调度实现

trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式

export function effect(fn, options:any = {}) {
    // 创建响应式effect 接收scheduler
    const _effect = new ReactiveEffect(fn,options.scheduler); 
}

export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return
    }
    let effects = depsMap.get(key);
    if (effects) {
        effects = new Set(effects);
        for (const effect of effects) {
            if (effect !== activeEffect) { 
                if(effect.scheduler){ // 如果有调度函数则执行调度函数
                    effect.scheduler()
                }else{
                    effect.run(); 
                }
            }
        }
    }
}