-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
94 changed files
with
6,524 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { | ||
IAction, | ||
addHiddenProp, | ||
boundActionDecorator, | ||
createAction, | ||
executeAction, | ||
fail, | ||
invariant, | ||
namedActionDecorator | ||
} from "../internal" | ||
|
||
export interface IActionFactory { | ||
// nameless actions | ||
<T extends Function | null | undefined>(fn: T): T & IAction | ||
// named actions | ||
<T extends Function | null | undefined>(name: string, fn: T): T & IAction | ||
|
||
// named decorator | ||
(customName: string): ( | ||
target: Object, | ||
key: string | symbol, | ||
baseDescriptor?: PropertyDescriptor | ||
) => void | ||
|
||
// unnamed decorator | ||
(target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void | ||
|
||
// @action.bound decorator | ||
bound(target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void | ||
} | ||
|
||
export const action: IActionFactory = function action(arg1, arg2?, arg3?, arg4?): any { | ||
// action(fn() {}) | ||
if (arguments.length === 1 && typeof arg1 === "function") | ||
return createAction(arg1.name || "<unnamed action>", arg1) | ||
// action("name", fn() {}) | ||
if (arguments.length === 2 && typeof arg2 === "function") return createAction(arg1, arg2) | ||
|
||
// @action("name") fn() {} | ||
if (arguments.length === 1 && typeof arg1 === "string") return namedActionDecorator(arg1) | ||
|
||
// @action fn() {} | ||
if (arg4 === true) { | ||
// apply to instance immediately | ||
arg1[arg2] = createAction(arg1.name || arg2, arg3.value) | ||
} else { | ||
return namedActionDecorator(arg2).apply(null, arguments) | ||
} | ||
} as any | ||
|
||
action.bound = boundActionDecorator as any | ||
|
||
export function runInAction<T>(block: () => T): T | ||
export function runInAction<T>(name: string, block: () => T): T | ||
export function runInAction(arg1, arg2?) { | ||
// TODO: deprecate? | ||
const actionName = typeof arg1 === "string" ? arg1 : arg1.name || "<unnamed action>" | ||
const fn = typeof arg1 === "function" ? arg1 : arg2 | ||
|
||
if (process.env.NODE_ENV !== "production") { | ||
invariant( | ||
typeof fn === "function" && fn.length === 0, | ||
"`runInAction` expects a function without arguments" | ||
) | ||
if (typeof actionName !== "string" || !actionName) | ||
fail(`actions should have valid names, got: '${actionName}'`) | ||
} | ||
|
||
return executeAction(actionName, fn, this, undefined) | ||
} | ||
|
||
export function isAction(thing: any) { | ||
return typeof thing === "function" && thing.isMobxAction === true | ||
} | ||
|
||
export function defineBoundAction(target: any, propertyName: string, fn: Function) { | ||
addHiddenProp(target, propertyName, createAction(propertyName, fn.bind(target))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { | ||
addHiddenProp, | ||
createAction, | ||
fail, | ||
BabelDescriptor, | ||
defineBoundAction, | ||
action | ||
} from "../internal" | ||
|
||
function dontReassignFields() { | ||
fail(process.env.NODE_ENV !== "production" && "@action fields are not reassignable") | ||
} | ||
|
||
export function namedActionDecorator(name: string) { | ||
return function(target, prop, descriptor: BabelDescriptor) { | ||
if (descriptor) { | ||
if (process.env.NODE_ENV !== "production" && descriptor.get !== undefined) { | ||
return fail("@action cannot be used with getters") | ||
} | ||
// babel / typescript | ||
// @action method() { } | ||
if (descriptor.value) { | ||
// typescript | ||
return { | ||
value: createAction(name, descriptor.value), | ||
enumerable: false, | ||
configurable: true, // See #1477 | ||
writable: true // for typescript, this must be writable, otherwise it cannot inherit :/ (see inheritable actions test) | ||
} | ||
} | ||
// babel only: @action method = () => {} | ||
const { initializer } = descriptor | ||
return { | ||
enumerable: false, | ||
configurable: true, // See #1477 | ||
writable: true, // See #1398 | ||
initializer() { | ||
// N.B: we can't immediately invoke initializer; this would be wrong | ||
return createAction(name, initializer!.call(this)) | ||
} | ||
} | ||
} | ||
// bound instance methods | ||
return actionFieldDecorator(name).apply(this, arguments) | ||
} | ||
} | ||
|
||
export function actionFieldDecorator(name: string) { | ||
// Simple property that writes on first invocation to the current instance | ||
return function(target, prop, descriptor) { | ||
Object.defineProperty(target, prop, { | ||
configurable: true, | ||
enumerable: false, | ||
get() { | ||
return undefined | ||
}, | ||
set(value) { | ||
addHiddenProp(this, prop, action(name, value)) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
export function boundActionDecorator(target, propertyName, descriptor, applyToInstance?: boolean) { | ||
if (applyToInstance === true) { | ||
defineBoundAction(target, propertyName, descriptor.value) | ||
return null | ||
} | ||
if (descriptor) { | ||
// if (descriptor.value) | ||
// Typescript / Babel: @action.bound method() { } | ||
// also: babel @action.bound method = () => {} | ||
return { | ||
configurable: true, | ||
enumerable: false, | ||
get() { | ||
defineBoundAction( | ||
this, | ||
propertyName, | ||
descriptor.value || descriptor.initializer.call(this) | ||
) | ||
return this[propertyName] | ||
}, | ||
set: dontReassignFields | ||
} | ||
} | ||
// field decorator Typescript @action.bound method = () => {} | ||
return { | ||
enumerable: false, | ||
configurable: true, | ||
set(v) { | ||
defineBoundAction(this, propertyName, v) | ||
}, | ||
get() { | ||
return undefined | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { | ||
Lambda, | ||
getNextId, | ||
invariant, | ||
EMPTY_OBJECT, | ||
deprecated, | ||
IReactionPublic, | ||
IReactionDisposer, | ||
isAction, | ||
Reaction, | ||
IEqualsComparer, | ||
action, | ||
comparer | ||
} from "../internal" | ||
|
||
export interface IAutorunOptions { | ||
delay?: number | ||
name?: string | ||
/** | ||
* Experimental. | ||
* Warns if the view doesn't track observables | ||
*/ | ||
requiresObservable?: boolean | ||
scheduler?: (callback: () => void) => any | ||
onError?: (error: any) => void | ||
} | ||
|
||
/** | ||
* Creates a named reactive view and keeps it alive, so that the view is always | ||
* updated if one of the dependencies changes, even when the view is not further used by something else. | ||
* @param view The reactive view | ||
* @returns disposer function, which can be used to stop the view from being updated in the future. | ||
*/ | ||
export function autorun( | ||
view: (r: IReactionPublic) => any, | ||
opts: IAutorunOptions = EMPTY_OBJECT | ||
): IReactionDisposer { | ||
if (process.env.NODE_ENV !== "production") { | ||
invariant(typeof view === "function", "Autorun expects a function as first argument") | ||
invariant( | ||
isAction(view) === false, | ||
"Autorun does not accept actions since actions are untrackable" | ||
) | ||
} | ||
|
||
const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId() | ||
const runSync = !opts.scheduler && !opts.delay | ||
let reaction: Reaction | ||
|
||
if (runSync) { | ||
// normal autorun | ||
reaction = new Reaction( | ||
name, | ||
function(this: Reaction) { | ||
this.track(reactionRunner) | ||
}, | ||
opts.onError, | ||
opts.requiresObservable | ||
) | ||
} else { | ||
const scheduler = createSchedulerFromOptions(opts) | ||
// debounced autorun | ||
let isScheduled = false | ||
|
||
reaction = new Reaction( | ||
name, | ||
() => { | ||
if (!isScheduled) { | ||
isScheduled = true | ||
scheduler(() => { | ||
isScheduled = false | ||
if (!reaction.isDisposed) reaction.track(reactionRunner) | ||
}) | ||
} | ||
}, | ||
opts.onError, | ||
opts.requiresObservable | ||
) | ||
} | ||
|
||
function reactionRunner() { | ||
view(reaction) | ||
} | ||
|
||
reaction.schedule() | ||
return reaction.getDisposer() | ||
} | ||
|
||
export type IReactionOptions = IAutorunOptions & { | ||
fireImmediately?: boolean | ||
equals?: IEqualsComparer<any> | ||
} | ||
|
||
const run = (f: Lambda) => f() | ||
|
||
function createSchedulerFromOptions(opts: IReactionOptions) { | ||
return opts.scheduler | ||
? opts.scheduler | ||
: opts.delay | ||
? (f: Lambda) => setTimeout(f, opts.delay!) | ||
: run | ||
} | ||
|
||
export function reaction<T>( | ||
expression: (r: IReactionPublic) => T, | ||
effect: (arg: T, r: IReactionPublic) => void, | ||
opts: IReactionOptions = EMPTY_OBJECT | ||
): IReactionDisposer { | ||
if (typeof opts === "boolean") { | ||
opts = { fireImmediately: opts } | ||
deprecated( | ||
`Using fireImmediately as argument is deprecated. Use '{ fireImmediately: true }' instead` | ||
) | ||
} | ||
if (process.env.NODE_ENV !== "production") { | ||
invariant( | ||
typeof expression === "function", | ||
"First argument to reaction should be a function" | ||
) | ||
invariant(typeof opts === "object", "Third argument of reactions should be an object") | ||
} | ||
const name = opts.name || "Reaction@" + getNextId() | ||
const effectAction = action( | ||
name, | ||
opts.onError ? wrapErrorHandler(opts.onError, effect) : effect | ||
) | ||
const runSync = !opts.scheduler && !opts.delay | ||
const scheduler = createSchedulerFromOptions(opts) | ||
|
||
let firstTime = true | ||
let isScheduled = false | ||
let value: T | ||
|
||
const equals = (opts as any).compareStructural | ||
? comparer.structural | ||
: opts.equals || comparer.default | ||
|
||
const r = new Reaction( | ||
name, | ||
() => { | ||
if (firstTime || runSync) { | ||
reactionRunner() | ||
} else if (!isScheduled) { | ||
isScheduled = true | ||
scheduler!(reactionRunner) | ||
} | ||
}, | ||
opts.onError, | ||
opts.requiresObservable | ||
) | ||
|
||
function reactionRunner() { | ||
isScheduled = false // Q: move into reaction runner? | ||
if (r.isDisposed) return | ||
let changed = false | ||
r.track(() => { | ||
const nextValue = expression(r) | ||
changed = firstTime || !equals(value, nextValue) | ||
value = nextValue | ||
}) | ||
if (firstTime && opts.fireImmediately!) effectAction(value, r) | ||
if (!firstTime && (changed as boolean) === true) effectAction(value, r) | ||
if (firstTime) firstTime = false | ||
} | ||
|
||
r.schedule() | ||
return r.getDisposer() | ||
} | ||
|
||
function wrapErrorHandler(errorHandler, baseFn) { | ||
return function() { | ||
try { | ||
return baseFn.apply(this, arguments) | ||
} catch (e) { | ||
errorHandler.call(this, e) | ||
} | ||
} | ||
} |
Oops, something went wrong.