Vue3-effect

148 阅读4分钟

effect

用法

<template>
	<div id="app"></div>
</template>

<script setup>
import { effect } from 'vue';
const eft = effect(() => {
  app.innerHTML = 'lhm';
});
console.log(eft); // eft Class
</script>

思路分析

  • 在vue3中,effect是响应式的核心模块,effect是一个副作用函数
  • effect接收两个参数
    • fn是回调函数,默认会执行一次,在依赖数据更改时会在此执行回调
    • options是传入的参数(本次只实现scheduler和onStop)
      • lazy是否延迟触发 effect
      • computed 是否为计算属性
      • scheduler 调度函数
      • onTrack 追踪时触发
      • onTrigger 触发回调时触发
      • onStop 停止监听时触发

目录结构如下:

reactivity
├── copy
├── dist
├── node_modules
├── package.json
├── src
│   ├── effect.ts
│   ├── index.ts
│   └── reactive.ts
└── types

实现

  1. 默认暴露一个effect函数供调用

  2. 创建一个reactiveEffect类

  3. 实现run方法,默认初次调起回调函数

// effect.ts
class ReactiveEffect {
    constructor(public fn, private options) {

    }
    run () {
      return this.fn();
    }
}

export function effect(fn: Function, options) {
    const _effect = new ReactiveEffect(fn, options);
    _effect.run();
    return _effect;
}

简单实现了effect函数之后,需要结合上一篇文章提到的reactive进行依赖收集和触发更新

  • 依赖收集
    • 当被代理的对象属性被访问时,需要进行依赖收集。
    • 在effect函数被调用时,即触发run函数时,由于effect是可以嵌套使用的,因此,vue2利用栈的方式记录着effect的父子关系,vue3则巧妙的运用了类似tree的父子关系记录着effect的层级,在run函数被触发时,需要记录其自身作为activeEffect及其parent父effect。
    • 每个被proxy代理的属性,在被访问时都需要收集其对应的effect。
    • 在vue3中,利用WeakMap来记录每个属性对应的effect函数,大致结构如下

<WeakMap>{}:<Map>{
  key: <Set>[effect]
}
  • 修改effect.ts
// effect.ts
class ReactiveEffect {
    constructor(public fn, private options) {

    }
    run () {
      	if (!this.active) {
            // 如果是失活状态,直接执行回调函数
            return this.fn();
        }
        try {
            // 如果不是失活,则需要进行依赖收集后再执行回调函数
            // tree父子关系类似
            this.parent = activeEffect;
            activeEffect = this;
            this.fn();
        } finally {
            // 执行完之后,把当前的activeEffect变成parent
            activeEffect = this.parent;
            this.parent = undefined;
        }
    }
}

export function effect(fn: Function, options) {
    const _effect = new ReactiveEffect(fn, options);
    _effect.run();
    return _effect;
}

// 收集依赖
export function track(target, key) {
    // 判断当前是否存在激活的activeEffect,否则不是在effect中取之,不需要进行依赖收集
    if (!activeEffect) return;
    // 依赖收集
    // 先判断是否收集过该对象
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    let shouldTrack = !dep.has(activeEffect);
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
    }
}

// 触发更新
export function trigger(target, key, newValue, oldVlaue) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    const dep = depsMap.get(key);
    if (dep) {
        const effects = [...dep];
        effects.forEach(effect => {
            if (activeEffect !== effect) { // 防止在effect里面自己触发新的更新,会死循环
               effect.run();
            }
        });
    }
}
  • 修改reactive.ts,用户取值时触发get,收集依赖,用户修改值时,触发set,触发依赖的收集到的所有effect,页面更新
// reactive.ts
const reactiveMap = new WeakMap();
const const enumn ReactiveFlags {
  IS_REACTIVE: '__v_isReactive',
}
export function reactive(target) {
    // 不对非对象类型的数据进行处理
    if (!isObject(target)) return;
  	const exisitsProxy = reactiveMap.get(target); // 获取当前是否有代理过此对象
  	if (exisitsProxy) return exisitsProxy; // 如果代理过,直接返回该对象
  	if (target[ReactiveFlags.IS_REACTIVE]) return target; // 如果已经是proxy对象,直接返回
    const proxy = new Proxy(target, {
        get(target, key, receiver) {
            if (key === ReactiveFlags.IS_REACTIVE) return true;
            track(target, key); // 用户取值,收集依赖
            const r = Reflect.get(target, key, receiver);
            if (isObject(r)) {
                // 如果还是一个对象,需要进行深层代理
                reactive(r);
            }
            return r;
        },
        set(target, key, value, receiver) {
            const oldVlaue = target[key]; // 没有修改之前的值
            const r = Reflect.set(target, key, value, receiver);
            if (oldVlaue !== value) {
                // 触发副作用effect函数更新页面
                trigger(target, key, value, oldVlaue);
            }
            return r;
        }
    });
  	reactiveMap.set(target, proxy); // 首次代理,缓存该对象
    return proxy;
}
  • stopAPI的实现,主要思路是调用effect时,将reactiveEffect实例暴露给外部,再控制其内部是否失活来决定是否进行更新
// effect.ts
class ReactiveEffect {
    constructor(public fn, private options) {

    }
    run () {
      	if (!this.active) {
            // 如果是失活状态,直接执行回调函数
            return this.fn();
        }
        try {
            // 如果不是失活,则需要进行依赖收集后再执行回调函数
            // tree父子关系类似
            this.parent = activeEffect;
            activeEffect = this;
            this.fn();
        } finally {
            // 执行完之后,把当前的activeEffect变成parent
            activeEffect = this.parent;
            this.parent = undefined;
        }
    }
}

export function effect(fn: Function, options) {
    const _effect = new ReactiveEffect(fn, options);
    _effect.run();
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner;
}
......
  • scheduler调度函数,则是直接传入reactiveEffect中,用户触发更新时,判断如果有调度函数,则走调度函数,不再调用run方法
// effect
.....
// 触发更新
export function trigger(target, key, newValue, oldVlaue) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    const dep = depsMap.get(key);
    if (dep) {
        const effects = [...dep];
        effects.forEach(effect => {
            if (activeEffect !== effect) { // 防止在effect里面自己触发新的更新,会死循环
                // 判断是否有自定义回调,如果有执行自定义回调
                if (!effect.options.scheduler) {
                    effect.run();
                } else {
                    effect.options.scheduler();
                }
            }
        });
    }
}
......