diff --git a/README.md b/README.md index 994a008..f3f97d0 100644 --- a/README.md +++ b/README.md @@ -156,23 +156,65 @@ function App() { - - - -## Manually control the cache +## Cache Control -1. Add the `name` attribute to the `` tag that needs to control the cache. +### Automatic control cache + +Add the `when` attribute to the `` tag that needs to control the cache. The value is as follows + +#### When the `when` type is `Boolean` + +- **true**: Cache after uninstallation +- **false**: Not cached after uninstallation + +```javascript + +``` + +#### When the `when` type is `Array` + +The **1th** parameter indicates whether it needs to be cached at the time of uninstallation. + +The **2th** parameter indicates whether to unload all cache contents of ``, including all `` nested in ``. + +```javascript +// For example: +// The following indicates that it is not cached when uninstalling +// And uninstalls all nested `` + + ... + + ... + + ... + + ... + + ... + +``` + +#### When the `when` type is `Function` + +The return value is the above `Boolean` or `Array`, which takes effect as described above. + +### Manually control the cache + +1. Add the `name` attribute to the `` tag that needs to control the cache. 2. Get control functions using `withAliveScope` or `useAliveController` - **drop(name)** - Unload the `` node in cache state by name. The name can be of type `String` or `RegExp`. Note that only the first layer of content that hits KeepAlive is unloaded and will not be uninstalled in KeepAlive. Nested, missed KeepAlive + Unload the `` node in cache state by name. The name can be of type `String` or `RegExp`. Note that only the first layer of content that hits `` is unloaded and will not be uninstalled in ``. Would not unload nested `` - **dropScope(name)** - Unloads the `` node in cache state by name. The name optional type is `String` or `RegExp`, which will unload all content of KeepAlive, including all KeepAlive nested in KeepAlive. + Unloads the `` node in cache state by name. The name optional type is `String` or `RegExp`, which will unload all content of ``, including all `` nested in ``. - **clear()** - will clear all `` in the cache + will clear all `` in the cache - **getCachingNodes()** @@ -180,11 +222,33 @@ function App() { ```javascript ... -import { withAliveScope, useAliveController } from 'react-activation' +import KeepAlive, { withAliveScope, useAliveController } from 'react-activation' +... + + ... + + ... + + ... + + ... + + ... + ... function App() { const { drop, dropScope, clear, getCachingNodes } = useAliveController() + useEffect(() => { + drop('Test') + // or + drop(/Test/) + // or + dropScope('Test') + + clear() + }) + return ( ... ) @@ -209,10 +273,12 @@ class App extends Component { Pass the `children` attribute of `` to `` and render it with `` -After rendering ``, the content is transferred to `` through DOM operation. +After rendering ``, the content is transferred to `` through `DOM` operation. Since `` will not be uninstalled, caching can be implemented. + + - - - ## Breaking Change @@ -305,8 +371,9 @@ Since `` will not be uninstalled, caching can be implemented. Choose a repair method - - Create `Context` using `createContext` exported from `react-activation` - - Fix the affected `Context` with `fixContext` exported from `react-activation` + - Create `Context` using `createContext` exported from `react-activation` + + - Fix the affected `Context` with `fixContext` exported from `react-activation` ```javascript ... @@ -328,7 +395,7 @@ Since `` will not be uninstalled, caching can be implemented. 3. Affects the functionality that depends on the level of the React component, as follows - - [x] ~~Error Boundaries (fixed)~~ - - [ ] React.Suspense & React.lazy (to be fixed) + - [x] ~~Error Boundaries~~ (Fixed) + - [x] ~~React.Suspense & React.lazy~~ (Fixed) - [ ] React Synthetic Event Bubbling Failure - [ ] Other undiscovered features diff --git a/README_CN.md b/README_CN.md index 922a8e5..9498079 100644 --- a/README_CN.md +++ b/README_CN.md @@ -154,24 +154,98 @@ function App() { - - - -## 手动控制缓存 +## 缓存控制 + +### 自动控制缓存 + +给需要控制缓存的 `` 标签增加 `when` 属性,取值如下 + +#### 当 `when` 类型为 `Boolean` 时 + +- **true**: 卸载时缓存 +- **false**: 卸载时不缓存 + +```javascript + +``` + +#### 当 `when` 类型为 `Array` 时 + +**第 1 位**参数表示是否需要在卸载时缓存 + +**第 2 位**参数表示是否卸载 `` 的所有缓存内容,包括 `` 中嵌套的所有 `` + +```javascript +// 例如:以下表示卸载时不缓存,并卸载掉嵌套的所有 `` + + ... + + ... + + ... + + ... + + ... + +``` + +#### 当 `when` 类型为 `Function` 时 + +返回值为上述 `Boolean` 或 `Array`,依照上述说明生效 + +### 手动控制缓存 1. 给需要控制缓存的 `` 标签增加 `name` 属性 2. 使用 `withAliveScope` 或 `useAliveController` 获取控制函数 - - **drop(name)**: 按 name 卸载缓存状态下的 KeepAlive 节点,name 可选类型为 `String` 或 `RegExp`,注意,仅卸载命中KeepAlive 的第一层内容,不会卸载 KeepAlive 中嵌套的、未命中的 KeepAlive - - **dropScope(name)**:按 name 卸载缓存状态下的 KeepAlive 节点,name 可选类型为 `String` 或 `RegExp`,将卸载命中KeepAlive 的所有内容,包括 KeepAlive 中嵌套的所有 KeepAlive - - **clear()**:将清空所有缓存中的 KeepAlive - - **getCachingNodes()**:获取所有缓存中的节点 + - **drop(name)**: + + 按 name 卸载缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,注意,仅卸载命中 `` 的第一层内容,不会卸载 `` 中嵌套的、未命中的 `` + + - **dropScope(name)** + + 按 name 卸载缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,将卸载命中 `` 的所有内容,包括 `` 中嵌套的所有 `` + + - **clear()** + + 将清空所有缓存中的 KeepAlive + + + - **getCachingNodes()** + + 获取所有缓存中的节点 ```javascript ... -import { withAliveScope, useAliveController } from 'react-activation' +import KeepAlive, { withAliveScope, useAliveController } from 'react-activation' +... + + ... + + ... + + ... + + ... + + ... + ... function App() { const { drop, dropScope, clear, getCachingNodes } = useAliveController() + useEffect(() => { + drop('Test') + // or + drop(/Test/) + // or + dropScope('Test') + + clear() + }) + return ( ... ) @@ -196,10 +270,12 @@ class App extends Component { 将 `` 的 `children` 属性传递到 `` 中,通过 `` 进行渲染 -`` 完成渲染后通过 DOM 操作,将内容转移到 `` 中 +`` 完成渲染后通过 `DOM` 操作,将内容转移到 `` 中 由于 `` 不会被卸载,故能实现缓存功能 + + - - - ## Breaking Change 由实现原理引发的额外问题 @@ -238,7 +314,7 @@ class App extends Component { `ClassComponent` 中上述错误可通过利用 `withActivation` 高阶组件修复 - `FunctionComponent` 目前暂无处理方式,可使用 setTimeout 或 nextTick 延时获取 ref + `FunctionComponent` 目前暂无处理方式,可使用 `setTimeout` 或 `nextTick` 延时获取 `ref` ```javascript @withActivation @@ -292,8 +368,9 @@ class App extends Component { 修复方式任选一种 - - 使用从 `react-activation` 导出的 `createContext` 创建上下文 - - 使用从 `react-activation` 导出的 `fixContext` 修复受影响的上下文 + - 使用从 `react-activation` 导出的 `createContext` 创建上下文 + + - 使用从 `react-activation` 导出的 `fixContext` 修复受影响的上下文 ```javascript ... @@ -315,7 +392,7 @@ class App extends Component { 3. 对依赖于 React 层级的功能造成影响,如下 - - [x] ~~Error Boundaries(已修复)~~ - - [ ] React.Suspense & React.lazy(待修复) + - [x] ~~Error Boundaries~~(已修复) + - [x] ~~React.Suspense & React.lazy~~(已修复) - [ ] React 合成事件冒泡失效 - [ ] 其他未发现的功能 diff --git a/docs/reactActivationPrinciple.gif b/docs/reactActivationPrinciple.gif new file mode 100644 index 0000000..6e5443b Binary files /dev/null and b/docs/reactActivationPrinciple.gif differ diff --git a/package.json b/package.json index f50d51a..90f7d26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-activation", - "version": "0.0.2", + "version": "0.1.0", "description": " for React like in vue", "main": "index.js", "scripts": { diff --git a/src/core/AliveScope.js b/src/core/AliveScope.js index 8fea08c..78ac55e 100644 --- a/src/core/AliveScope.js +++ b/src/core/AliveScope.js @@ -9,25 +9,29 @@ export default class AliveScope extends Component { store = new Map() nodes = new Map() - update = (id, { name, children, ctx$$ }) => + // FIXME: 每次 update 均触发 forceUpdate,可能存在性能问题,待验证 + update = (id, params) => new Promise(resolve => { + const node = this.nodes.get(id) || null + const isNew = !node + const now = Date.now() + + if (isNew) { + this.helpers = { ...this.helpers } + } + this.nodes.set(id, { id, - name, - children, - ctx$$ + createTime: now, + updateTime: now, + ...node, + ...params }) this.forceUpdate(resolve) }) keep = (id, params) => new Promise(resolve => { - const isNew = !this.nodes.has(id) - - if (isNew) { - this.helpers = { ...this.helpers } - } - this.update(id, { id, ...params @@ -41,10 +45,11 @@ export default class AliveScope extends Component { isRegExp(name) ? name.test(node.name) : node.name === name ) - drop = name => - this.dropNodes(this.getCachingNodesByName(name).map(node => node.id)) + dropById = id => this.dropNodes([id]) + dropScopeByIds = ids => this.dropNodes(this.getScopeIds(ids)) - dropScope = name => { + getScopeIds = ids => { + // 递归采集 scope alive nodes id const getCachingNodesId = id => { const aliveNodesId = get(this.getCache(id), 'aliveNodesId', []) @@ -55,46 +60,36 @@ export default class AliveScope extends Component { return [id, ...aliveNodesId] } - return this.dropNodes( - flatten( - this.getCachingNodesByName(name).map(({ id }) => getCachingNodesId(id)) - ) - ) + return flatten(ids.map(id => getCachingNodesId(id))) } + drop = name => + this.dropNodes(this.getCachingNodesByName(name).map(node => node.id)) + + dropScope = name => + this.dropScopeByIds(this.getCachingNodesByName(name).map(({ id }) => id)) + dropNodes = nodesId => new Promise(resolve => { - const willDropIds = nodesId - .filter(id => { - const cache = this.store.get(id) - const canDrop = get(cache, 'cached') - - if (canDrop) { - // 用在多层 KeepAlive 同时触发 drop 时,避免触发深层 KeepAlive 节点的缓存生命周期 - cache.willDrop = true - } - - return canDrop - }) - .map(id => { - this.nodes.delete(id) + nodesId.forEach(id => { + const cache = this.store.get(id) + const canDrop = get(cache, 'cached') - return id - }) + if (canDrop) { + // 用在多层 KeepAlive 同时触发 drop 时,避免触发深层 KeepAlive 节点的缓存生命周期 + cache.willDrop = true + this.nodes.delete(id) + } + }) this.helpers = { ...this.helpers } - this.forceUpdate(() => { - resolve() - - willDropIds.map(id => { - this.store.delete(id) - }) - }) + this.forceUpdate(resolve) }) clear = () => this.dropNodes(this.getCachingNodes().map(({ id }) => id)) getCache = id => this.store.get(id) + getNode = id => this.nodes.get(id) getCachingNodes = () => [...this.nodes.values()] // 静态化节点上下文内容,防止重复渲染 @@ -103,8 +98,12 @@ export default class AliveScope extends Component { update: this.update, drop: this.drop, dropScope: this.dropScope, + dropById: this.dropById, + dropScopeByIds: this.dropScopeByIds, + getScopeIds: this.getScopeIds, clear: this.clear, getCache: this.getCache, + getNode: this.getNode, getCachingNodes: this.getCachingNodes } diff --git a/src/core/ContextBridge.js b/src/core/Bridge/Context.js similarity index 99% rename from src/core/ContextBridge.js rename to src/core/Bridge/Context.js index 316922f..8401a51 100644 --- a/src/core/ContextBridge.js +++ b/src/core/Bridge/Context.js @@ -6,7 +6,7 @@ import React, { PureComponent, useContext, useRef, useEffect } from 'react' import createReactContext from 'create-react-context' -import { run, get, nextTick, isUndefined, isFunction } from '../helpers' +import { run, get, nextTick, isUndefined, isFunction } from '../../helpers' const fixedContext = [] const updateListenerCache = new Map() diff --git a/src/core/Bridge/ErrorBoundary.js b/src/core/Bridge/ErrorBoundary.js new file mode 100644 index 0000000..af58016 --- /dev/null +++ b/src/core/Bridge/ErrorBoundary.js @@ -0,0 +1,34 @@ +import { Component } from 'react' + +import { run } from '../../helpers' + +export default class ErrorBoundaryBridge extends Component { + // Error Boundary 透传至对应 KeepAlive 实例位置 + static getDerivedStateFromError = () => null + componentDidCatch(error) { + const { error$$: throwError } = this.props + + run(throwError, undefined, error, () => { + run(throwError, undefined, null) + }) + } + + render() { + return this.props.children + } +} + +export class ErrorThrower extends Component { + state = { + error: null + } + + throwError = (error, cb) => this.setState({ error }, cb) + render() { + if (this.state.error) { + throw this.state.error + } + + return run(this.props.children, undefined, this.throwError) + } +} diff --git a/src/core/Bridge/Suspense.js b/src/core/Bridge/Suspense.js new file mode 100644 index 0000000..e0b34f2 --- /dev/null +++ b/src/core/Bridge/Suspense.js @@ -0,0 +1,78 @@ +import React, { lazy, Suspense, Component, Fragment } from 'react' + +import { run, isUndefined, isFunction } from '../../helpers' + +// 兼容性检测 +const isSupported = isFunction(lazy) && !isUndefined(Suspense) +const SusNotSupported = ({ children }) => run(children) + +const Lazy = isSupported ? lazy(() => new Promise(() => null)) : () => null + +class FallbackListener extends Component { + componentDidMount() { + run(this.props, 'onStart') + } + + componentWillUnmount() { + run(this.props, 'onEnd') + } + + render() { + return null + } +} + +function SuspenseBridge({ children, sus$$ }) { + return ( + // 捕获 Keeper 内部可能存在的 lazy,并触发对应 KeepAlive 位置上的 LazyBridge + + } + > + {children} + + ) +} + +export const LazyBridge = isSupported + ? class LazyBridge extends Component { + state = { + suspense: false + } + + onSuspenseStart = () => { + this.setState({ + suspense: true + }) + } + + onSuspenseEnd = () => { + this.setState({ + suspense: false + }) + } + + sus$$ = { + onSuspenseStart: this.onSuspenseStart, + onSuspenseEnd: this.onSuspenseEnd + } + + render() { + const { children } = this.props + + return ( + + {run(children, undefined, this.sus$$)} + {/* 渲染 Lazy 以触发 KeepAlive 所处位置外部可能存在的 Suspense */} + {this.state.suspense && } + + ) + } + } + : SusNotSupported + +export default isSupported ? SuspenseBridge : SusNotSupported diff --git a/src/core/Bridge/index.js b/src/core/Bridge/index.js new file mode 100644 index 0000000..6689ed9 --- /dev/null +++ b/src/core/Bridge/index.js @@ -0,0 +1,53 @@ +import React from 'react' + +import { ProviderBridge, ConsumerBridge } from './Context' +import SuspenseBridge, { LazyBridge } from './Suspense' +import ErrorBoundaryBridge, { ErrorThrower } from './ErrorBoundary' + +import { run } from '../../helpers' + +// 用于 Keeper 中,实现 Keeper 向外或向内的桥接代理 +export default function Bridge({ id, children, bridgeProps }) { + const { sus$$, ctx$$, error$$ } = bridgeProps + + return ( + /* 由内向外透传 componentDidCatch 捕获的 error */ + + {/* 由内向外透传 lazy 行为 */} + + {/* 由外向内透传可能存在的 Consumer 数据 */} + + {children} + + + + ) +} + +// 用于 KeepAlive 中,实现 KeepAlive 向外或向内的桥接代理 +export function Acceptor({ id, children }) { + return ( + /* 由内向外透传 componentDidCatch 捕获的 error */ + + {error$$ => ( + /* 由内向外透传 lazy 行为 */ + + {sus$$ => ( + /* 由外向内透传可能被捕获的 Provider 数据 */ + + {ctx$$ => + run(children, undefined, { + bridgeProps: { + sus$$, + ctx$$, + error$$ + } + }) + } + + )} + + )} + + ) +} diff --git a/src/core/KeepAlive.js b/src/core/KeepAlive.js index 1f13ba9..082d61f 100644 --- a/src/core/KeepAlive.js +++ b/src/core/KeepAlive.js @@ -1,6 +1,14 @@ import React, { Component } from 'react' -import { get, run, globalThis as root, isFunction, debounce } from '../helpers' +import { + get, + run, + globalThis as root, + nextTick, + isFunction, + isArray, + debounce +} from '../helpers' import { expandKeepAlive } from './withAliveScope' import { @@ -17,6 +25,14 @@ const getErrorTips = name => 您现在可见的更新结果存在严重的性能问题 可能遇到了隐含的 bug,请不要使用 KeepAlive 并联系作者解决` +const parseWhenResult = res => { + if (isArray(res)) { + return res + } + + return [res] +} + class KeepAlive extends Component { // 本段为 KeepAlive 更新隐患检测,通过检测 KeepAlive 瞬时更新次数来判断是否进入死循环,并在 update 中强制阻止更新 updateTimes = 0 @@ -52,6 +68,10 @@ class KeepAlive extends Component { this[lifecycleName] = () => { const { id, _helpers } = this.props const cache = _helpers.getCache(id) + const node = _helpers.getNode(id) + if (node) { + node.updateTime = Date.now() + } // 若组件即将卸载则不再触发缓存生命周期 if (!cache || cache.willDrop) { @@ -131,16 +151,15 @@ class KeepAlive extends Component { } } - cache = null init = () => { - const { _helpers, id, children, ctx$$, name } = this.props + const { _helpers, id, children, ...rest } = this.props // 将 children 渲染至 AliveScopeProvider 中 _helpers .keep(id, { - name, children, - ctx$$ + getInstance: () => this, + ...rest }) .then(cache => { this.inject() @@ -151,11 +170,10 @@ class KeepAlive extends Component { } else { cache.inited = true } - cache.keepAliveInstance = this }) } - update = ({ _helpers, id, children, ctx$$, name }) => { + update = ({ _helpers, id, name, ...rest }) => { if (this.needForceStopUpdate(name)) { return } @@ -166,8 +184,8 @@ class KeepAlive extends Component { // this.eject(false) _helpers.update(id, { name, - children, - ctx$$ + getInstance: () => this, + ...rest }) // this.inject(false) } @@ -181,20 +199,41 @@ class KeepAlive extends Component { // 组件卸载时重置 dom 状态,保证 react dom 操作正常进行,并触发 unactivate 生命周期 componentWillUnmount() { - const { id, _helpers } = this.props + const { id, _helpers, when: calcWhen = true } = this.props + const cache = _helpers.getCache(id) + const [when, isScope] = parseWhenResult(run(calcWhen)) + + if (!cache) { + return + } + this.eject() + delete cache.getInstance + + if (!when) { + if (isScope) { + const needToDrop = [ + cache, + ..._helpers.getScopeIds([id]).map(id => _helpers.getCache(id)) + ] + + needToDrop.forEach(cache => { + cache.cached = true + cache.willDrop = true + }) + nextTick(() => _helpers.dropScopeByIds([id])) + } else { + cache.cached = true + cache.willDrop = true + nextTick(() => _helpers.dropById(id)) + } + } // 触发 willUnactivate 生命周期 run(this, LIFECYCLE_UNACTIVATE) - const cache = _helpers.getCache(id) - delete cache.keepAliveInstance } render() { - if (this.catchError) { - throw this.catchError - } - return (
run(listener, [LIFECYCLE_ACTIVATE])) } @@ -42,22 +48,6 @@ export default class Keeper extends Component { .forEach(([, listener]) => run(listener, [LIFECYCLE_UNACTIVATE])) } - // Error Boundary 透传至对应 KeepAlive 实例 - static getDerivedStateFromError = error => null - componentDidCatch(error) { - const { id, store } = this.props - - const cache = store.get(id) - const instance = get(cache, 'keepAliveInstance') - - if (instance) { - instance.catchError = error - instance.forceUpdate(() => { - delete instance.catchError - }) - } - } - // // 原先打算更新过程中先重置 dom 节点状态,更新后恢复 dom 节点 // // 但考虑到性能消耗可能过大,且可能因 dom 操作时机引发其他 react 渲染问题,故不使用 // // 对应 KeepAlive 处 update 也注释起来不使用 @@ -106,7 +96,7 @@ export default class Keeper extends Component { } render() { - const { ctx$$, id, children, ...props } = this.props + const { id, children, bridgeProps, ...props } = this.props return (
- + {children} - +
) diff --git a/src/core/getKeyByFiberNode.js b/src/core/getKeyByFiberNode.js index ed50999..1f1e3f1 100644 --- a/src/core/getKeyByFiberNode.js +++ b/src/core/getKeyByFiberNode.js @@ -33,6 +33,7 @@ const genRenderPath = node => { } // 使用节点 _ka 属性或下标与其 key 作为 Y 坐标 +// FIXME: 使用 index 作为 Y 坐标是十分不可靠的行为,待想出更好的法子替代 const getNodeId = fiberNode => `${get(fiberNode, 'pendingProps._ka', fiberNode.index)}:${fiberNode.key || ''}` diff --git a/src/core/lifecycles.js b/src/core/lifecycles.js index 0a1a3cf..efd623f 100644 --- a/src/core/lifecycles.js +++ b/src/core/lifecycles.js @@ -7,7 +7,14 @@ import React, { } from 'react' import hoistStatics from 'hoist-non-react-statics' -import { get, run, nextTick, isObject, isFunction, isUndefined } from '../helpers' +import { + get, + run, + nextTick, + isObject, + isFunction, + isUndefined +} from '../helpers' import { AliveNodeConsumer, aliveNodeContext } from './context' diff --git a/src/core/withAliveScope.js b/src/core/withAliveScope.js index a50575a..7789e47 100644 --- a/src/core/withAliveScope.js +++ b/src/core/withAliveScope.js @@ -3,17 +3,17 @@ import hoistStatics from 'hoist-non-react-statics' import { isFunction } from '../helpers' -import { ConsumerBridge } from './ContextBridge' +import { Acceptor } from './Bridge' import AliveIdProvider from './AliveIdProvider' import { AliveScopeConsumer, aliveScopeContext } from './context' export const expandKeepAlive = KeepAlive => { const renderContent = ({ id, helpers, props }) => ( - - {ctxValue => ( - + + {bridgeProps => ( + )} - + ) function HookExpand(props) { diff --git a/src/index.js b/src/index.js index b33a23f..696178d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import AliveScope from './core/AliveScope' import { withActivation, useActivate, useUnactivate } from './core/lifecycles' import KeepAlive from './core/KeepAlive' -import { fixContext, createContext } from './core/ContextBridge' +import { fixContext, createContext } from './core/Bridge/Context' import withAliveScope, { useAliveController } from './core/withAliveScope' export default KeepAlive