持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
proxy.mjs
import { activeEffect } from "./effect.mjs";
export var bucket = new WeakMap();
export function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) bucket.set(target, (depsMap = new Map()));
let deps = depsMap.get(key);
if (!deps) depsMap.set(key, (deps = new Set()));
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
export function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
// 因为执行副作用函数中,会进行清空effects的操作,但副作用函数的执行又会使effects中有新增操作,导致执行Set的forEach循环
// 类似 const set = new Set([1]);set.forEach(i=>{set.delete(1);set.add(1);console.log('运行中')}) 会循环
const effectsToRun = new Set(effects);
effectsToRun &&
effectsToRun.forEach((effectFn) => {
// 为了防止 proxy.x ++ 导致无限递归,因为此步骤 = proxy.x = proxy.x +1;
// 当访问 proxy.x时候,会添加一份副作用函数,proxy.x加一后进行赋值,会触发trigger,然后再次执行这份副作用函数,重复此步骤下去,导致递归爆栈。
// 解决办法是:当我们执行这个副作用函数时候,此时activeEffect为自身,然后触发trigger,所以只需要在trigger时加一个判断条件。
if (activeEffect === effectFn) return;
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else effectFn();
});
}
export function createProxy(data) {
const proxy = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, val) {
target[key] = val;
trigger(target, key);
return true;
},
});
return proxy;
}
effect.mjs
export var activeEffect = null;
// 维护一份调用栈,防止收集错副作用函数(一般出现在嵌套的副作用函数之中)。
export var activeEffectStack = [];
const effect = function (fn, options = {}) {
// 执行后返回对应结果
const effectFn = () => {
cleanup(effectFn);
// 执行此副作用函数时候,将自身推入栈中。
activeEffect = effectFn;
activeEffectStack.push(effectFn);
const res = fn();
// 执行完后,恢复调用栈。
activeEffectStack.pop();
activeEffect = activeEffectStack[activeEffectStack.length - 1];
return res;
};
// 将用户的参数选项、添加至副作用函数
effectFn.options = options;
effectFn.deps = [];
// 如果选项中包含lazy懒加载 第一次不执行
if (!options.lazy) effectFn();
// 用户可以手动执行
return effectFn;
};
// 每次执行副作用函数都清空,与副作用函数相关的依赖,为了防止遗留的依赖被添加进来。
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
export default effect;
computed.js(computed函数实现)
import effect from "./effect.mjs";
import { track, trigger } from "./proxy.mjs";
function computed(getter) {
let dirty = true;
let value = null;
const effectFn = effect(getter, {
// 开启了懒加载,第一次不执行,只有.value才会进行依赖收集。
lazy: true,
scheduler(fn) {
// 当计算属性依赖的数据变化,同时,obj.value触发,此时dirty为false,手动trigger
if (!dirty) {
// 当依赖数据变化,副作用函数执行,此时脏为true(代表下一次不走缓存)。
dirty = true;
trigger(obj, "value");
}
fn();
},
});
const obj = {
get value() {
// 如果为脏,就需要手动执行副作用拿到结果,如果不为脏,就走缓存。(一旦触发了.value 此时dirty就为false)
if (dirty) {
value = effectFn();
dirty = false;
}
// 为了防止effect(()=>{console.log(obj.value)})无法收集依赖,手动track。
track(obj, "value");
return value;
},
};
return obj;
}
export default computed;
watch.mjs(watch函数实现)
import effect from "./effect.mjs";
function traverse(value, seen = new Set()) {
// 遍历读取对象,使每个key都有对应的副作用函数
if (typeof value !== "object" || value === null || seen.has(value)) return;
seen.add(value);
for (const k in value) {
traverse(value[k]);
}
return value;
}
export default function watch(source, cb, options) {
let getter;
// 过期的副作用,解决竞态问题
let cleanup;
function onInvalidate(fn) {
cleanup = fn;
}
// 需要传入旧value和新value
let oldValue, newValue;
// 如果传入的source为()=>source这种形式,就记录该key的副作用
if (typeof source === "function") {
getter = source;
// 否则,记录下该对象所有key的副作用
} else {
getter = () => traverse(source);
}
const job = (fn) => {
newValue = fn();
// 执行job时,消除过期内容
cleanup && cleanup();
cb(oldValue, newValue, onInvalidate);
oldValue = JSON.parse(JSON.stringify(newValue));
};
// 副作用就是拿到最新的值
const effectFn = effect(() => getter(), {
lazy: true,
// 加入调度器,更新值后传入新值与旧值
scheduler: job,
});
if (options?.immdiate) {
job(effectFn);
} else {
oldValue = effectFn();
}
}
测试文件
import effect from "./effect.mjs";
import computed from "./computed.mjs";
import { createProxy } from "./proxy.mjs";
import watch from "./watch.mjs";
const data = {
name: "xhj",
sum: 0,
};
const proxyData = createProxy(data);
// 计算属性
const sum = computed(() => {
console.log("computed", proxyData.sum);
return proxyData.sum;
});
// 这边需要使用到计算属性sum,不然不会进行收集依赖
console.log(sum.value);
// watch监听
watch(proxyData, (oldVal, newVal) => {
console.log("watch", oldVal.sum, newVal.sum);
});
proxyData.sum++;
测试结果
至此,粗略的响应式已实现,欢迎小伙伴们评论区讨论交流~