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 停止监听时触发
- lazy是否延迟触发
目录结构如下:
reactivity
├── copy
├── dist
├── node_modules
├── package.json
├── src
│ ├── effect.ts
│ ├── index.ts
│ └── reactive.ts
└── types
实现
-
默认暴露一个effect函数供调用
-
创建一个reactiveEffect类
-
实现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();
}
}
});
}
}
......