UNPKG

@msom/reaction

Version:

@msom/reaction

220 lines (207 loc) 6.13 kB
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 }); } }