UNPKG

refui

Version:

The JavaScript framework that refuels your UI projects, across web, native, and embedded

1,040 lines (909 loc) 22.9 kB
/* Copyright Yukino Song, SudoMaker Ltd. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { collectDisposers, nextTick, read, peek, watch, onDispose, freeze, signal, isSignal, contextValid } from 'refui/signal' import { hotEnabled, enableHMR } from 'refui/hmr' import { nop, emptyArr, removeFromArr, isThenable, markStatic, nullRefObject } from 'refui/utils' import { isProduction } from 'refui/constants' const KEY_CTX = Symbol(isProduction ? '' : 'K_Ctx') const rootUserCtx = Object.create(null) let currentCtx = null let currentUserCtx = rootUserCtx function _captured({ ctx, user }, ...args) { const prevCtx = currentCtx const prevUserCtx = currentUserCtx currentCtx = ctx currentUserCtx = user try { return this(...args) } finally { currentUserCtx = prevUserCtx currentCtx = prevCtx } } const _invalidateCapture = function () { this.ctx = null this.user = rootUserCtx } function _capture(fn) { const state = { ctx: currentCtx, user: currentUserCtx } onDispose(_invalidateCapture.bind(state)) return _captured.bind(fn, state) } function capture(fn) { return freeze(_capture(fn)) } function _runInSnapshot(fn, ...args) { return fn(...args) } function snapshot() { return capture(_runInSnapshot) } function render(instance, R) { const ctx = instance[KEY_CTX] if (!ctx) { return } const { run, render: renderComponent } = ctx if (!renderComponent || typeof renderComponent !== 'function') { return R.ensureElement(renderComponent) } return run(renderComponent, R)[0] } function dispose(instance) { const ctx = instance[KEY_CTX] if (!ctx) { return } ctx.dispose() } function getCurrentSelf() { return currentCtx?.self } async function _lazyLoad(loader, ident, ...args) { const run = snapshot() if (!this.cache) { this.cache = new Promise(async function (resolve, reject) { let result = await loader() if (result && !((ident === undefined || ident === null) && typeof result === 'function')) { result = result[ident ?? 'default'] } if (!result) { reject(new SyntaxError('Lazy loader failed to resolve component')) return } if (hotEnabled) { const component = result result = function (...args) { return function (R) { return R.c(component, ...args) } } } resolve(result) }) } return run(await this.cache, ...args) } function lazy(loader, ident) { return _lazyLoad.bind({ cache: null }, loader, ident) } function memo(fn) { let cached = null const captured = capture(fn) return function (...args) { if (cached) return cached return (cached = captured(...args)) } } function useMemo(fn) { return function () { return memo(fn) } } function dummyRun(fn, R) { let result = null const cleanup = collectDisposers([], function () { result = R.ensureElement(fn()) }) return [result, cleanup] } function Fn({ name = 'Fn', ctx, catch: catchErr }, handler, handleErr) { if (!handler) { return nop } if (!catchErr) { catchErr = handleErr } const run = currentCtx?.run ?? dummyRun return function (R) { const fragment = R.createFragment(name) let currentRender = null let currentDispose = null watch( _capture(function () { const newHandler = read(handler) if (!newHandler) { currentDispose?.() currentRender = currentDispose = null return } const newRender = newHandler(ctx) if (newRender === currentRender) { return } currentRender = newRender if (newRender !== undefined && newRender !== null) { const prevDispose = currentDispose currentDispose = run(function () { let newResult = null let errored = false try { newResult = R.ensureElement(newRender) } catch (err) { errored = true const errorHandler = peek(catchErr) if (errorHandler) { newResult = R.ensureElement(errorHandler(err, name, ctx)) } else { throw err } } if (!errored && prevDispose) { prevDispose() } if (newResult !== undefined && newResult !== null) { R.appendNode(fragment, newResult) onDispose(nextTick.bind(null, R.removeNode.bind(null, newResult))) } else { if (errored && prevDispose) { onDispose(prevDispose) } } }, R)[1] } else { currentDispose?.() currentDispose = null } }) ) return fragment } } markStatic(Fn) function For({ name = 'For', entries, track, indexed, expose }, itemTemplate) { let currentData = [] let kv = track && new Map() let ks = indexed && new Map() let nodeCache = new Map() let disposers = new Map() function _clear() { for (let [, _dispose] of disposers) _dispose(true) nodeCache = new Map() disposers = new Map() if (ks) ks = new Map() } function flushKS() { if (ks) { const currentDataLength = currentData.length for (let i = 0; i < currentDataLength; i++) { const sig = ks.get(currentData[i]) sig.set(i) } } } onDispose(_clear) function clear() { if (!currentData.length) return _clear() if (kv) kv = new Map() currentData = [] if (isSignal(entries) && entries.peek()?.length) entries.set([]) } if (expose) { function getItem(itemKey) { return kv ? kv.get(itemKey) : itemKey } function remove(itemKey) { const itemData = getItem(itemKey) removeFromArr(peek(entries), itemData) entries.trigger() } expose({ getItem, remove, clear }) } return function (R) { const fragment = R.createFragment(name) function getItemNode(itemKey) { let node = nodeCache.get(itemKey) if (!node) { const item = kv ? kv.get(itemKey) : itemKey let idxSig = ks ? ks.get(itemKey) : 0 if (ks && !idxSig) { idxSig = signal(0) ks.set(itemKey, idxSig) } const dispose = collectDisposers( [], function () { node = R.c(itemTemplate, { item, index: idxSig }) nodeCache.set(itemKey, node) }, function (batch) { if (!batch) { nodeCache.delete(itemKey) disposers.delete(itemKey) if (ks) ks.delete(itemKey) if (kv) kv.delete(itemKey) } if (node) R.removeNode(node) } ) disposers.set(itemKey, dispose) } return node } // eslint-disable-next-line complexity watch( _capture(function () { /* eslint-disable max-depth */ const data = read(entries) if (!data || !data.length) return clear() let oldData = currentData if (track) { kv = new Map() const key = read(track) currentData = data.map(function (i) { const itemKey = i[key] kv.set(itemKey, i) return itemKey }) } else currentData = [...data] let newData = null if (oldData.length) { const currentDataLength = currentData.length const obsoleteDataKeys = [...new Set([...currentData, ...oldData])].slice(currentDataLength) if (obsoleteDataKeys.length === oldData.length) { _clear() newData = currentData } else { const obsoleteDataKeysLength = obsoleteDataKeys.length if (obsoleteDataKeysLength) { for (let i = 0; i < obsoleteDataKeysLength; i++) { disposers.get(obsoleteDataKeys[i])() removeFromArr(oldData, obsoleteDataKeys[i]) } } const newDataKeys = [...new Set([...oldData, ...currentData])].slice(oldData.length) const hasNewKeys = !!newDataKeys.length let newDataCursor = 0 while (newDataCursor < currentDataLength) { if (!oldData.length) { if (newDataCursor) newData = currentData.slice(newDataCursor) break } const frontSet = [] const backSet = [] let frontChunk = [] let backChunk = [] let prevChunk = frontChunk let oldDataCursor = 0 let oldItemKey = oldData[0] let newItemKey = currentData[newDataCursor] const oldDataLength = oldData.length while (oldDataCursor < oldDataLength) { const isNewKey = hasNewKeys && newDataKeys.includes(newItemKey) if (isNewKey || oldItemKey === newItemKey) { if (prevChunk !== frontChunk) { backSet.push(backChunk) backChunk = [] prevChunk = frontChunk } frontChunk.push(newItemKey) if (isNewKey) { R.insertBefore(getItemNode(newItemKey), getItemNode(oldItemKey)) } else { oldDataCursor += 1 oldItemKey = oldData[oldDataCursor] } newDataCursor += 1 newItemKey = currentData[newDataCursor] } else { if (prevChunk !== backChunk) { frontSet.push(frontChunk) frontChunk = [] prevChunk = backChunk } backChunk.push(oldItemKey) oldDataCursor += 1 oldItemKey = oldData[oldDataCursor] } } if (prevChunk === frontChunk) { frontSet.push(frontChunk) } backSet.push(backChunk) frontSet.shift() const frontSetLength = frontSet.length for (let i = 0; i < frontSetLength; i++) { const fChunk = frontSet[i] const bChunk = backSet[i] if (fChunk.length <= bChunk.length) { const beforeAnchor = getItemNode(bChunk[0]) backSet[i + 1] = bChunk.concat(backSet[i + 1]) bChunk.length = 0 const fChunkLength = fChunk.length for (let j = 0; j < fChunkLength; j++) { R.insertBefore(getItemNode(fChunk[j]), beforeAnchor) } } else if (backSet[i + 1].length) { const beforeAnchor = getItemNode(backSet[i + 1][0]) const bChunkLength = bChunk.length for (let j = 0; j < bChunkLength; j++) { R.insertBefore(getItemNode(bChunk[j]), beforeAnchor) } } else { R.appendNode(fragment, ...bChunk.map(getItemNode)) } } oldData = [].concat(...backSet) } } } else { newData = currentData } if (newData) { const newDataLength = newData.length for (let i = 0; i < newDataLength; i++) { const node = getItemNode(newData[i]) if (node) R.appendNode(fragment, node) } } flushKS() }) ) return fragment } } markStatic(For) function If({ condition, true: trueCondition, else: otherwise }, trueBranch, falseBranch) { if (otherwise) { falseBranch = otherwise } if (trueCondition) { condition = trueCondition } if (isSignal(condition)) { condition = condition.get.bind(condition) } if (typeof condition === 'function') { return Fn({ name: 'If' }, function () { if (condition()) { return trueBranch } else { return falseBranch } }) } if (condition) return trueBranch return falseBranch } markStatic(If) function _dynContainer(name, catchErr, ctx, props, ...children) { let current = null let renderFn = null return Fn( { name, ctx }, () => { const component = read(this) if (current === component) { return renderFn } if (component === undefined || component === null) { return (current = renderFn = null) } current = component renderFn = function (R) { return R.c(component, props, ...children) } return renderFn }, catchErr ) } function Dynamic({ is, current, ...props }, ...children) { if (current) { props.$ref = current } return _dynContainer.call(is, 'Dynamic', null, null, props, ...children) } markStatic(Dynamic) let currentFutureList = null // Internal, no document/.d.ts needed // DON'T USE UNLESS YOU UNDERSTAND WHAT IT DOES function _asyncContainer(name, fallback, catchErr, onLoad, suspensed, props, children) { const component = signal() let currentDispose = null let disposed = false let _resolve = null onDispose(function () { disposed = true }) // `this` should and is guaranteed to be a Promise let resolvedFuture = this if (onLoad) { resolvedFuture = resolvedFuture.then(async function (val) { if (disposed) { return } await onLoad() return val }) } resolvedFuture = resolvedFuture.then( capture(function (result) { if (disposed) { _resolve?.() return } currentDispose?.() currentDispose = watch(function () { component.set(read(result)) }) }) ) if (catchErr) { resolvedFuture = resolvedFuture.catch( capture(function (error) { if (disposed) { _resolve?.() return } currentDispose?.() currentDispose = watch(function () { const handler = read(catchErr) if (handler) { if (typeof handler === 'function') { component.set(handler({ ...props, error }, ...children)) } else { component.set(handler) } } }) }) ) } if (fallback) { nextTick( capture(function () { if (disposed || component.peek()) { _resolve?.() return } currentDispose?.() currentDispose = watch(function () { const handler = read(fallback) if (handler) { if (typeof handler === 'function') { component.set(function () { return handler({ ...props }, ...children) }) } else { component.set(handler) } } }) }) ) } if (!fallback && suspensed && currentFutureList) { let resolve = null let reject = null const promise = new Promise(function (r, j) { resolve = r reject = j }) _resolve = resolve currentFutureList.push(resolvedFuture, promise) let currentFn = component.peek() let currentRender = null const _props = { ...props, name: null, onLoad() { resolve() }, catch(props) { const _handler = peek(catchErr) if (_handler) { const handled = _handler(props) resolve() return handled } else { reject(props.error) } } } return Fn({ name: isProduction ? null : `${name}(suspensed)` }, function () { const renderFn = component.get() if (currentFn === renderFn) { return currentRender } currentFn = renderFn return (currentRender = Suspense(_props, renderFn)) }) } return Fn({ name }, component.get.bind(component)) } function Async( { name = 'Async', future, fallback, catch: catchErr, onLoad, suspensed = true, ...props }, then, now, handleErr ) { while (typeof future === 'function') { future = future() } future = (isThenable(future) ? future : Promise.resolve(future)).then( capture(function (result) { let lastResult = null let lastHandler = null return Fn({ name: 'Then' }, function () { if (!contextValid) { return } const handler = read(then) if (handler === lastHandler) { return lastResult } lastHandler = handler return (lastResult = handler?.({ ...props, result })) }) }) ) return _asyncContainer.bind(future, name, fallback ?? now, catchErr ?? handleErr, onLoad, suspensed, props, emptyArr) } markStatic(Async) function Suspense({ name = 'Suspense', fallback, catch: catchErr, onLoad, ...props }, ...children) { if (!children.length) { return } return function (R) { const prevFutureList = currentFutureList currentFutureList = [] const future = new Promise(function (resolve) { resolve(children.map(R.ensureElement)) }) const _future = currentFutureList.length ? Promise.all(currentFutureList).then(function () { return future }) : future const result = _asyncContainer.call(_future, name, fallback, catchErr, onLoad, false, props, children)(R) currentFutureList = prevFutureList return result } } markStatic(Suspense) function Transition( { name = 'Transition', data, onLoad: userOnLoad, fallback, loading: userLoading, pending: userPending, catch: catchErr }, then, now, handleErr ) { return function (R) { const loading = isSignal(userLoading) ? userLoading : signal(false) const pending = isSignal(userPending) ? userPending : signal(false) const currentElement = signal() data ??= Object.create(null) let currentState = null let disposed = false let currentDispose = null let pendingDispose = null let pendingElement = null onDispose(function () { disposed = true if (pendingDispose) { pendingDispose() pendingDispose = null pendingElement = null pending.set(false) } if (currentDispose) { currentDispose() currentDispose = null } currentState = null }) function createState() { const leaving = signal(false) const entered = signal(false) const entering = entered.inverse() return { __proto__: null, loading, pending, leaving, entered, entering, data } } async function _onLoad(_pendingElement, state) { if (disposed) { return } if (userOnLoad) { if (currentState && currentElement.peek()) { currentState.leaving.set(true) } let swapped = false function swap() { if (swapped) { return } currentElement.set(_pendingElement) swapped = true return nextTick() } await userOnLoad(state, !!currentState, swap) currentState = state if (swapped) { return } } currentElement.set(_pendingElement) } watch(function () { const state = createState() const future = new Promise(function (resolve) { resolve(then(state)) }) let _dispose = null let _element = null if (pendingDispose) { pendingDispose() pendingDispose = null } async function onLoad() { if (_element !== pendingElement) { _dispose() return } loading.set(false) await _onLoad(pendingElement, state) if (_dispose !== pendingDispose) { _dispose() return } pending.set(false) currentDispose?.() currentDispose = pendingDispose pendingDispose = null pendingElement = null } pendingDispose = _dispose = collectDisposers([], function () { pendingElement = _element = Suspense( { name: 'TransitionContainer', onLoad, catch: catchErr ?? handleErr, state }, function () { return future } )(R) }) loading.set(true) pending.set(true) }) if (fallback) { nextTick( capture(function () { if (disposed || currentElement.peek()) { return } currentDispose = watch(function () { const handler = read(fallback) if (handler) { const state = createState() if (typeof handler === 'function') { _onLoad(handler.bind(null, state), state) } else { _onLoad(handler, state) } } }) }) ) } return Fn({ name }, currentElement.get.bind(currentElement)) } } markStatic(Transition) function Render({ from }) { return Fn({ name: 'Render' }, function () { const instance = read(from) if (instance !== null && instance !== undefined) return render(instance, R) }) } markStatic(Render) class Component { constructor(tpl, props, ...children) { const ctx = { run: null, render: null, dispose: null, self: this } const prevCtx = currentCtx currentCtx = ctx const disposers = [] ctx.run = capture(function (fn, R) { let result = null const cleanup = collectDisposers( [], function () { result = R.ensureElement(fn(R)) }, function (batch) { if (!batch) { removeFromArr(disposers, cleanup) } } ) disposers.push(cleanup) return [result, cleanup] }) try { ctx.dispose = collectDisposers( disposers, function () { let renderFn = tpl(props, ...children) if (isThenable(renderFn)) { const { fallback, catch: catchErr, onLoad, suspensed = true, ..._props } = props renderFn = _asyncContainer.call(renderFn, 'Future', fallback, catchErr, onLoad, suspensed, _props, children) } ctx.render = renderFn }, () => { Object.defineProperty(this, KEY_CTX, { value: null, enumerable: false }) } ) } catch (error) { for (let i of disposers) i(true) throw error } finally { currentCtx = prevCtx } Object.defineProperty(this, KEY_CTX, { value: ctx, enumerable: false, configurable: true }) } } markStatic(Component) const createComponent = (function () { function createComponentRaw(tpl, props, ...children) { if (isSignal(tpl)) { return new Component( _dynContainer.bind(tpl, 'Dynamic(signal)', null, null), props ?? Object.create(null), ...children ) } const { $ref, ..._props } = props ?? nullRefObject const component = new Component(tpl, _props, ...children) if ($ref) { if (isSignal($ref)) { $ref.set(component) } else if (typeof $ref === 'function') { $ref(component) } else if (!isProduction) { throw new Error(`Invalid $ref type: ${typeof $ref}`) } } return component } if (hotEnabled) { function makeDyn(tpl, handleErr) { return _dynContainer.bind(tpl, null, handleErr, tpl) } return enableHMR({ makeDyn, Component, createComponentRaw }) } return createComponentRaw })() const contextSymbolMap = new WeakMap() function createContext(defaultVal, name = 'Context') { const contextSymbol = Symbol(name) currentUserCtx[contextSymbol] = defaultVal function Context({ value }, ...children) { return function (R) { const prevCtx = currentUserCtx const shadowCtx = Object.create(prevCtx) shadowCtx[contextSymbol] = value currentUserCtx = shadowCtx try { if (isProduction) { return R.ensureElement(children) } else { const wrapper = R.createFragment(name) R.appendNode(wrapper, ...children.map(R.ensureElement)) return wrapper } } finally { currentUserCtx = prevCtx } } } contextSymbolMap.set(Context, contextSymbol) markStatic(Context) return Context } function useContext(Context) { const contextSymbol = contextSymbolMap.get(Context) if (contextSymbol) { return currentUserCtx[contextSymbol] } } export { capture, snapshot, render, dispose, getCurrentSelf, lazy, memo, useMemo, Fn, For, If, Dynamic, Async, Suspense, Transition, Render, Component, createComponent, createContext, useContext, _asyncContainer }