@msom/reaction
Version:
@msom/reaction
220 lines (207 loc) • 6.13 kB
text/typescript
import { getGlobalData, setGlobalData } from "@msom/common";
export interface IObserver {
notify(): void;
addReaction(reaction: Reaction): void;
removeReaction(reaction: Reaction): void;
}
export type $REACTION = {
tracking?: (ob: IObserver) => void;
reaction?: Reaction;
};
setGlobalData("@msom/reaction", {} as $REACTION);
export type ReactionOption = {
tracker: () => void;
callback?: () => void;
scheduler?: "nextTick" | "nextFrame" | undefined | ((cb: () => void) => void);
};
export class Reaction {
private declare option: ReactionOption;
private declare tracked: Set<IObserver>;
// 上一次更新还未执行则取消上一次更新
private declare cancel?: () => void;
constructor(option: ReactionOption) {
this.tracked = new Set();
this.option = option;
this.updateNextTick();
this.track();
}
private _cancel() {
this.cancel && this.cancel();
this.cancel = undefined;
}
/**
* 根据传入的delay选项,初始化微队列函数
* @returns
*/
updateNextTick() {
const { scheduler } = this.option;
if (!scheduler) return;
if (scheduler === "nextTick") {
// 判断浏览器环境还是node环境
if (typeof process !== "undefined" && process.nextTick) {
// node环境
this.nextTick = function (this: Reaction, cb: () => void) {
// 取消上一次的nextTick
this._cancel();
let canceled = false;
process.nextTick(() => {
if (!canceled) {
cb();
}
this.cancel = undefined;
});
this.cancel = () => {
// 取消nextTick的对调函数执行
canceled = true;
};
};
} else {
// 浏览器环境
this.nextTick = function (this: Reaction, cb: () => void) {
// 取消上一次的nextTick
this._cancel();
let canceled = false;
queueMicrotask(() => {
if (!canceled) {
cb();
}
this.cancel = undefined;
});
this.cancel = () => {
// 取消nextTick的对调函数执行
canceled = true;
};
};
}
} else if (scheduler === "nextFrame") {
this.nextTick = function (this: Reaction, cb: () => void) {
this._cancel();
if (Reflect.has(globalThis, "requestAnimationFrame")) {
const rafId = requestAnimationFrame(() => {
cb();
this.cancel = undefined;
});
this.cancel = () => {
// 取消requestAnimationFrame的对调函数执行
cancelAnimationFrame(rafId);
this.cancel = undefined;
};
} else {
const id = setTimeout(() => {
cb();
this.cancel = undefined;
}, 1000 / 60);
this.cancel = () => {
clearTimeout(id);
this.cancel = undefined;
};
}
};
} else {
this.nextTick = scheduler;
}
}
nextTick(cb: () => void) {
// 将函数放进微队列中,等待执行
this._cancel();
cb();
}
track() {
const { tracker } = this.option;
this.destroy();
const reactionData = getGlobalData("@msom/reaction") as $REACTION;
// 保存原始的tracking函数
const { tracking, reaction } = reactionData;
try {
// 更新tracking函数
Object.assign(reactionData, {
tracking: this.addObserver.bind(this),
reaction: this,
});
// 执行tracker函数
tracker();
} catch (e) {
// 清空已经追踪的observer
this.destroy();
console.error(e);
} finally {
// 恢复原始的tracking函数
Object.assign(reactionData, { tracking, reaction });
}
}
notify() {
const { reaction } = getGlobalData("@msom/reaction") as $REACTION;
if (reaction && reaction === this) {
console.error(
"The value of the dependent observer is being changed in the current tracking"
);
} else {
this.nextTick(() => {
this.runcall();
});
}
}
exec() {
this.runcall();
return this;
}
private runcall() {
const { callback } = this.option;
callback ? callback() : this.track();
}
disposer() {
return this.destroy.bind(this);
}
destroy() {
this.tracked.forEach(this.removeObserver.bind(this));
}
addObserver(observer: IObserver) {
this.tracked.add(observer);
observer.addReaction(this);
}
removeObserver(observer: IObserver) {
this.tracked.delete(observer);
observer.removeReaction(this);
}
}
export function createReaction(
tracker: () => void,
callback: () => void,
option?: { scheduler?: ReactionOption["scheduler"] }
): Reaction;
export function createReaction(
tracker: () => void,
option?: { scheduler?: ReactionOption["scheduler"] }
): Reaction;
export function createReaction(
tracker: () => void,
callback?: (() => void) | { scheduler?: ReactionOption["scheduler"] },
option?: { scheduler?: ReactionOption["scheduler"] }
): Reaction {
if (typeof callback === "function") {
return new Reaction({ tracker, callback, scheduler: option?.scheduler });
} else {
if (callback) {
if (option) throw "error params.";
return new Reaction({
tracker,
scheduler: callback.scheduler,
});
} else {
return new Reaction({ tracker, scheduler: option?.scheduler });
}
}
}
export function withoutTrack<T>(callback: () => T): T {
const reactionData = getGlobalData("@msom/reaction") as $REACTION;
const { tracking, reaction } = reactionData;
reactionData.tracking = undefined;
reactionData.reaction = undefined;
try {
return callback();
} catch (e) {
throw e;
} finally {
Object.assign(reactionData, { tracking, reaction });
}
}