UNPKG

@v4fire/client

Version:

V4Fire client core library

361 lines (282 loc) • 11.1 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ import { getPropertyInfo } from 'core/component/reflection'; import { wrapWithSuspending } from 'core/async'; import { beforeHooks } from 'core/component/const'; import { customWatcherRgxp } from 'core/component/watch/const'; import type { ComponentInterface } from 'core/component/interface'; import type { BindRemoteWatchersParams } from 'core/component/watch/interface'; /** * Binds watchers and event listeners that were registered as remote to the specified component instance. * This method has some "copy-paste" chunks, but it's done for better performance because it's a very hot function. * * Basically, this function takes watchers from a meta property of the component, * but you can provide custom watchers to initialize by using the second parameter of the function. * * @param component * @param [params] - additional parameters */ export function bindRemoteWatchers(component: ComponentInterface, params?: BindRemoteWatchersParams): void { const p = params ?? {}; const {unsafe} = component, {$watch, meta, hook, meta: {hooks}} = unsafe, // Link to the self component async instance selfAsync = unsafe.$async, // Link to an async instance $a = p.async ?? selfAsync; const // True if the component is deactivated right now isDeactivated = hook === 'deactivated', // True if the component isn't created yet isBeforeCreate = Boolean(beforeHooks[hook]); const watchersMap = p.watchers ?? meta.watchers, // True if the method was invoked with passing custom async instance as a property customAsync = $a !== unsafe.$async; // Iterate over all registered watchers and listeners and initialize their for (let keys = Object.keys(watchersMap), i = 0; i < keys.length; i++) { let watchPath = keys[i]; const watchers = watchersMap[watchPath]; if (!watchers) { continue; } let // Link to a context of the watcher, // by default it is a component is passed to the function watcherCtx = component, // True if this watcher can initialize only when the component is created watcherNeedCreated = true, // True if this watcher can initialize only when the component is mounted watcherNeedMounted = false; // Custom watchers looks like ':foo', 'bla:foo', '?bla:foo' // and uses to listen to some events instead listen of changing of component fields const customWatcher = customWatcherRgxp.exec(watchPath); if (customWatcher) { const m = customWatcher[1]; watcherNeedCreated = m === ''; watcherNeedMounted = m === '?'; } const exec = () => { // If we have a custom watcher we need to find a link to the event emitter. // For instance: // `':foo'` -> watcherCtx == ctx; key = `'foo'` // `'document:foo'` -> watcherCtx == document; key = `'foo'` if (customWatcher) { const l = customWatcher[2]; if (l !== '') { watcherCtx = Object.get(component, l) ?? Object.get(globalThis, l) ?? component; watchPath = customWatcher[3].toString(); } else { watcherCtx = component; watchPath = customWatcher[3].dasherize(); } } // Iterates over all registered handlers for this watcher for (let i = 0; i < watchers.length; i++) { const watchInfo = watchers[i], rawHandler = watchInfo.handler; const asyncParams = { label: watchInfo.label, group: watchInfo.group, join: watchInfo.join }; if (!customAsync) { if (asyncParams.label == null && !watchInfo.immediate) { let defLabel; if (watchInfo.method != null) { defLabel = `[[WATCHER:${watchPath}:${watchInfo.method}]]`; } else if (Object.isString(watchInfo.handler)) { defLabel = `[[WATCHER:${watchPath}:${watchInfo.handler}]]`; } else { defLabel = `[[WATCHER:${watchPath}:${(<Function>watchInfo.handler).name}]]`; } asyncParams.label = defLabel; } asyncParams.group = asyncParams.group ?? 'watchers'; } const eventParams = { ...asyncParams, options: watchInfo.options, single: watchInfo.single }; let handler: AnyFunction; // Right now, we need to create a wrapper for our "raw" handler // because there are some conditions for the watcher: // 1. It can provide or not provide arguments from an event that it listens; // 2. The handler can be specified as a function or as a method name from a component. // Also, we have two different cases: // 1. We listen to a custom event, OR we watch for some component property, // but we don't need to analyze the old value of the property; // 2. We watch for some component property, and we need to analyze the old value of the property. // These cases are based on one problem: if we watch for some property that isn't primitive, // like a hash table or a list, and we add a new item to this structure but don't change the original object, // the new and old values will be equal. // class bButton { // @field() // foo = {baz: 0}; // // @watch('foo') // onFooChange(newVal, oldVal) { // console.log(newVal === oldVal); // } // // created() { // this.foo.baz++; // } // } // To fix this problem, we can check a handler if it requires the second argument by using a length property, // and if the argument is needed, we can clone the old value and keep it within a closure. // The situation when we need to keep the old value (a property watcher with handler length more than one), // or we don't care about it (an event listener). if (customWatcher || !Object.isFunction(rawHandler) || rawHandler.length > 1) { handler = (a, b, ...args) => { args = watchInfo.provideArgs === false ? [] : [a, b].concat(args); if (Object.isString(rawHandler)) { if (!Object.isFunction(component[rawHandler])) { throw new ReferenceError(`The specified method "${rawHandler}" to watch is not defined`); } if (asyncParams.label != null) { $a.setImmediate(() => component[rawHandler](...args), asyncParams); } else { component[rawHandler](...args); } } else if (watchInfo.method != null) { rawHandler.call(component, ...args); } else { rawHandler(component, ...args); } }; // The situation for a property watcher when we define the standard length of one argument } else { handler = (val: unknown, ...args) => { const argsToProvide = watchInfo.provideArgs === false ? [] : [val, ...args]; if (Object.isString(rawHandler)) { if (!Object.isFunction(component[rawHandler])) { throw new ReferenceError(`The specified method "${rawHandler}" to watch is not defined`); } if (asyncParams.label != null) { $a.setImmediate(() => component[rawHandler](...argsToProvide), asyncParams); } else { component[rawHandler](...argsToProvide); } } else if (watchInfo.method != null) { rawHandler.call(component, ...argsToProvide); } else { rawHandler(component, ...argsToProvide); } }; } // Apply a watcher wrapper if specified. // Mind that the wrapper must return a function as a result, // but it can be packed to a promise. if (watchInfo.wrapper) { handler = <typeof handler>watchInfo.wrapper(component.unsafe, handler); } // To improve initialization performance, we should separately handle the promise situation // ("copy-paste", but works better) if (Object.isPromise(handler)) { $a.promise(handler, asyncParams).then((handler) => { if (!Object.isFunction(handler)) { throw new TypeError('A handler to watch is not a function'); } if (customWatcher) { // True if an event can listen by using the component itself, // because the `watcherCtx` does not look like an event emitter const needDefEmitter = watcherCtx === component && !Object.isFunction(watcherCtx['on']) && !Object.isFunction(watcherCtx['addListener']); if (needDefEmitter) { unsafe.$on(watchPath, handler); } else { const watch = (watcherCtx) => $a.on(watcherCtx, watchPath, <AnyFunction>handler, eventParams, ...watchInfo.args ?? []); if (Object.isPromise(watcherCtx)) { $a.promise(watcherCtx, asyncParams).then(watch).catch(stderr); } else { watch(watcherCtx); } } return; } // eslint-disable-next-line prefer-const let link, unwatch; const emitter = (_, wrappedHandler) => { handler = wrappedHandler; $a.worker(() => { if (link != null) { $a.off(link); } }, asyncParams); return () => unwatch?.(); }; link = $a.on(emitter, 'mutation', handler, wrapWithSuspending(asyncParams, 'watchers')); const toWatch = p.info ?? getPropertyInfo(watchPath, component); unwatch = $watch.call(component, toWatch, watchInfo, handler); }).catch(stderr); } else { if (customWatcher) { // True if an event can listen by using the component itself, // because the `watcherCtx` does not look like an event emitter const needDefEmitter = watcherCtx === component && !Object.isFunction(watcherCtx['on']) && !Object.isFunction(watcherCtx['addListener']); if (needDefEmitter) { unsafe.$on(watchPath, handler); } else { const addListener = (watcherCtx) => $a.on(watcherCtx, watchPath, handler, eventParams, ...watchInfo.args ?? []); if (Object.isPromise(watcherCtx)) { $a.promise(watcherCtx, asyncParams).then(addListener).catch(stderr); } else { addListener(watcherCtx); } } continue; } // eslint-disable-next-line prefer-const let link, unwatch; const emitter = (_, wrappedHandler) => { handler = wrappedHandler; $a.worker(() => { if (link != null) { $a.off(link); } }, asyncParams); return () => unwatch?.(); }; link = $a.on(emitter, 'mutation', handler, wrapWithSuspending(asyncParams, 'watchers')); const toWatch = p.info ?? getPropertyInfo(watchPath, component); unwatch = $watch.call(component, toWatch, watchInfo, handler); } } }; // Add listener to a component `created` hook if the component isn't created yet if (watcherNeedCreated && isBeforeCreate) { hooks.created.unshift({fn: exec}); continue; } // Add listener to a component `mounted/activate`d hook if the component isn't mounted/activates yet // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (watcherNeedMounted && (isBeforeCreate || component.$el == null)) { hooks[isDeactivated ? 'activated' : 'mounted'].unshift({fn: exec}); continue; } exec(); } }