├── .gitignore ├── docs ├── basicReactActivation.gif └── reactActivationPrinciple.gif ├── babel.js ├── src ├── core │ ├── NodeKey.js │ ├── Bridge │ │ ├── Context │ │ │ ├── index.js │ │ │ ├── ProviderBridge.js │ │ │ ├── ConsumerWrapper.js │ │ │ ├── fixContext.js │ │ │ └── ConsumerBridge.js │ │ ├── ErrorBoundary.js │ │ ├── index.js │ │ └── Suspense.js │ ├── context │ │ ├── reactContext.js │ │ ├── FakeScopeContext.js │ │ └── index.js │ ├── Freeze.js │ ├── lifecycles.js │ ├── withAliveScope.js │ ├── AliveScope.js │ ├── Keeper.js │ └── KeepAlive.js ├── helpers │ ├── is │ │ └── index.js │ ├── saveScrollPosition.js │ └── createReactContext.js └── index.js ├── .npmignore ├── .prettierrc ├── index.js ├── babel.config.js ├── LICENSE ├── rollup.config.js ├── .github └── workflows │ └── publish.yml ├── package.json ├── index.d.ts ├── README_CN.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | 4 | # editor 5 | .idea 6 | .vscode -------------------------------------------------------------------------------- /docs/basicReactActivation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CJY0208/react-activation/HEAD/docs/basicReactActivation.gif -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const nodeKeyPlugin = require('react-node-key/babel'); 3 | 4 | module.exports = nodeKeyPlugin; 5 | -------------------------------------------------------------------------------- /docs/reactActivationPrinciple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CJY0208/react-activation/HEAD/docs/reactActivationPrinciple.gif -------------------------------------------------------------------------------- /src/core/NodeKey.js: -------------------------------------------------------------------------------- 1 | import NodeKey from 'react-node-key' 2 | 3 | // 根据 FiberNode 所处位置来确定 NodeKey 4 | export default NodeKey 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | node_modules 3 | src 4 | .babelrc 5 | .gitignore 6 | .npmignore 7 | .prettierrc 8 | rollup.config.js 9 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./lib/index.min.js'); 5 | } else { 6 | module.exports = require('./lib/index.js'); 7 | } 8 | -------------------------------------------------------------------------------- /src/helpers/is/index.js: -------------------------------------------------------------------------------- 1 | // 值类型判断 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 | export const isRegExp = (val) => val instanceof RegExp 3 | // 值类型判断 ------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /src/core/Bridge/Context/index.js: -------------------------------------------------------------------------------- 1 | import { fixContext, autoFixContext, createContext } from './fixContext' 2 | 3 | export { fixContext, autoFixContext, createContext } 4 | export { default as ProviderBridge } from './ProviderBridge' 5 | export { default as ConsumerBridge } from './ConsumerBridge' 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/env', { modules: false }], '@babel/react'], 3 | plugins: [ 4 | '@babel/plugin-proposal-class-properties', 5 | 'react-node-key/babel', 6 | [ 7 | 'babel-plugin-import', 8 | { 9 | libraryName: 'szfe-tools', 10 | camel2DashComponentName: false, 11 | }, 12 | ], 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /src/core/context/reactContext.js: -------------------------------------------------------------------------------- 1 | import createContext from '../../helpers/createReactContext' 2 | 3 | // 整个 KeepAlive 功能的上下文,将 KeepAlive 的组件藏于其 Provider 中,保证其不会被卸载 4 | export const aliveScopeContext = createContext() 5 | export const { 6 | Provider: AliveScopeProvider, 7 | Consumer: AliveScopeConsumer, 8 | } = aliveScopeContext 9 | 10 | // KeepAlive 组件的上下文,实现缓存生命周期功能 11 | export const aliveNodeContext = createContext() 12 | export const { 13 | Provider: AliveNodeProvider, 14 | Consumer: AliveNodeConsumer, 15 | } = aliveNodeContext 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import AliveScope from './core/AliveScope' 2 | import { withActivation, useActivate, useUnactivate } from './core/lifecycles' 3 | import KeepAlive from './core/KeepAlive' 4 | import { 5 | fixContext, 6 | createContext, 7 | autoFixContext, 8 | } from './core/Bridge/Context' 9 | import withAliveScope, { useAliveController } from './core/withAliveScope' 10 | import NodeKey from './core/NodeKey' 11 | 12 | export default KeepAlive 13 | export { 14 | KeepAlive, 15 | AliveScope, 16 | withActivation, 17 | fixContext, 18 | autoFixContext, 19 | useActivate, 20 | useUnactivate, 21 | createContext, 22 | withAliveScope, 23 | useAliveController, 24 | NodeKey, 25 | } 26 | -------------------------------------------------------------------------------- /src/core/Bridge/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import { run } from 'szfe-tools' 3 | 4 | export default class ErrorBoundaryBridge extends Component { 5 | // Error Boundary 透传至对应 KeepAlive 实例位置 6 | static getDerivedStateFromError = () => null 7 | componentDidCatch(error) { 8 | const { error$$: throwError } = this.props 9 | 10 | run(throwError, undefined, error, () => { 11 | run(throwError, undefined, null) 12 | }) 13 | } 14 | 15 | render() { 16 | return this.props.children 17 | } 18 | } 19 | 20 | export class ErrorThrower extends Component { 21 | state = { 22 | error: null, 23 | } 24 | 25 | throwError = (error, cb) => this.setState({ error }, cb) 26 | render() { 27 | if (this.state.error) { 28 | throw this.state.error 29 | } 30 | 31 | return run(this.props.children, undefined, this.throwError) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/saveScrollPosition.js: -------------------------------------------------------------------------------- 1 | import { 2 | globalThis as root, 3 | get, 4 | run, 5 | value, 6 | isFunction, 7 | isExist, 8 | flatten, 9 | } from 'szfe-tools' 10 | 11 | function isScrollableNode(node = {}) { 12 | if (!isExist(node)) { 13 | return false 14 | } 15 | 16 | return ( 17 | node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight 18 | ) 19 | } 20 | 21 | function getScrollableNodes(from) { 22 | if (!isFunction(get(root, 'document.querySelectorAll'))) { 23 | return [] 24 | } 25 | 26 | return [...value(run(from, 'querySelectorAll', '*'), []), from].filter( 27 | isScrollableNode 28 | ) 29 | } 30 | 31 | export default function saveScrollPosition(from) { 32 | const nodes = [...new Set([...flatten(from.map(getScrollableNodes))])] 33 | 34 | const saver = nodes.map((node) => [ 35 | node, 36 | { 37 | x: node.scrollLeft, 38 | y: node.scrollTop, 39 | }, 40 | ]) 41 | 42 | return function revert() { 43 | saver.forEach(([node, { x, y }]) => { 44 | node.scrollLeft = x 45 | node.scrollTop = y 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CJY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | 5 | export default [ 6 | { 7 | input: 'src/index.js', 8 | output: { 9 | file: 'lib/index.min.js', 10 | format: 'umd', 11 | name: 'ReactActivation', 12 | exports: 'named', 13 | }, 14 | external: (name) => 15 | [ 16 | 'react', 17 | 'create-react-context', 18 | 'hoist-non-react-statics', 19 | 'react-node-key', 20 | 'react/jsx-runtime', 21 | 'react/jsx-dev-runtime', 22 | ].includes(name) || /szfe-tools/.test(name), 23 | plugins: [ 24 | resolve(), 25 | babel({ 26 | exclude: 'node_modules/**', 27 | }), 28 | uglify(), 29 | ], 30 | }, 31 | { 32 | input: 'src/index.js', 33 | output: { 34 | file: 'lib/index.js', 35 | format: 'cjs', 36 | exports: 'named', 37 | sourcemap: true, 38 | }, 39 | external: (name) => 40 | [ 41 | 'react', 42 | 'create-react-context', 43 | 'hoist-non-react-statics', 44 | 'react-node-key', 45 | 'react/jsx-runtime', 46 | 'react/jsx-dev-runtime', 47 | ].includes(name) || /szfe-tools/.test(name), 48 | plugins: [ 49 | resolve(), 50 | babel({ 51 | exclude: 'node_modules/**', 52 | }), 53 | ], 54 | }, 55 | ] 56 | -------------------------------------------------------------------------------- /src/core/context/FakeScopeContext.js: -------------------------------------------------------------------------------- 1 | import { Component, PureComponent } from 'react' 2 | import { EventBus, run, debounce } from 'szfe-tools' 3 | 4 | export const eventBus = new EventBus() 5 | 6 | export class FakeScopeProvider extends Component { 7 | static eventBus = eventBus 8 | static currentContextValue = undefined 9 | 10 | constructor(props) { 11 | super(props) 12 | FakeScopeProvider.currentContextValue = props.value 13 | } 14 | 15 | shouldComponentUpdate(nextProps) { 16 | if (nextProps.value !== this.props.value) { 17 | FakeScopeProvider.currentContextValue = nextProps.value 18 | eventBus.emit('update', nextProps.value) 19 | } 20 | 21 | return ( 22 | nextProps.children !== this.props.children || 23 | nextProps.value !== this.props.value 24 | ) 25 | } 26 | 27 | render() { 28 | const { children } = this.props 29 | return children 30 | } 31 | } 32 | 33 | export class FakeScopeConsumer extends PureComponent { 34 | state = { 35 | context: FakeScopeProvider.currentContextValue, 36 | } 37 | 38 | constructor(props) { 39 | super(props) 40 | eventBus.on('update', this.updateListener) 41 | } 42 | 43 | updateListener = debounce((nextContextValue) => { 44 | this.setState({ 45 | context: nextContextValue, 46 | }) 47 | }) 48 | 49 | componentWillUnmount() { 50 | eventBus.off('update', this.updateListener) 51 | } 52 | 53 | render() { 54 | const { children } = this.props 55 | const { context } = this.state 56 | return run(children, undefined, context) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/Bridge/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { run } from 'szfe-tools' 3 | 4 | import { ProviderBridge, ConsumerBridge } from './Context' 5 | import SuspenseBridge, { LazyBridge } from './Suspense' 6 | import ErrorBoundaryBridge, { ErrorThrower } from './ErrorBoundary' 7 | 8 | // 用于 Keeper 中,实现 Keeper 向外或向内的桥接代理 9 | export default function Bridge({ id, children, bridgeProps }) { 10 | const { sus$$, ctx$$, error$$ } = bridgeProps 11 | 12 | return ( 13 | /* 由内向外透传 componentDidCatch 捕获的 error */ 14 | 15 | {/* 由内向外透传 lazy 行为 */} 16 | 17 | {/* 由外向内透传可能存在的 Consumer 数据 */} 18 | 19 | {children} 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | // 用于 KeepAlive 中,实现 KeepAlive 向外或向内的桥接代理 27 | export function Acceptor({ id, children }) { 28 | return ( 29 | /* 由内向外透传 componentDidCatch 捕获的 error */ 30 | 31 | {(error$$) => ( 32 | /* 由内向外透传 lazy 行为 */ 33 | 34 | {(sus$$) => ( 35 | /* 由外向内透传可能被捕获的 Provider 数据 */ 36 | 37 | {(ctx$$) => 38 | run(children, undefined, { 39 | bridgeProps: { 40 | sus$$, 41 | ctx$$, 42 | error$$, 43 | }, 44 | }) 45 | } 46 | 47 | )} 48 | 49 | )} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/core/Freeze.js: -------------------------------------------------------------------------------- 1 | // Fork from react-freeze 2 | // https://github.com/software-mansion/react-freeze/blob/main/src/index.tsx 3 | import React, { Component, Suspense, Fragment } from 'react' 4 | 5 | // function Suspender({ freeze, children }) { 6 | // const promiseCache = useRef({}).current 7 | // if (freeze && !promiseCache.promise) { 8 | // promiseCache.promise = new Promise((resolve) => { 9 | // promiseCache.resolve = resolve 10 | // }) 11 | // throw promiseCache.promise 12 | // } else if (freeze) { 13 | // throw promiseCache.promise 14 | // } else if (promiseCache.promise) { 15 | // promiseCache.resolve() 16 | // promiseCache.promise = undefined 17 | // } 18 | 19 | // return {children} 20 | // } 21 | 22 | class Suspender extends Component { 23 | promiseCache = {} 24 | render() { 25 | const { freeze, children } = this.props 26 | const { promiseCache } = this 27 | 28 | if (freeze && !promiseCache.promise) { 29 | promiseCache.promise = new Promise((resolve) => { 30 | promiseCache.resolve = resolve 31 | }) 32 | throw promiseCache.promise 33 | } else if (freeze) { 34 | throw promiseCache.promise 35 | } else if (promiseCache.promise) { 36 | promiseCache.resolve() 37 | promiseCache.promise = undefined 38 | } 39 | 40 | return {children} 41 | } 42 | } 43 | 44 | export default function Freeze({ freeze, children, placeholder = null }) { 45 | return ( 46 | 47 | {children} 48 | 49 | ) 50 | } -------------------------------------------------------------------------------- /src/core/Bridge/Context/ProviderBridge.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { run, isUndefined } from 'szfe-tools' 3 | 4 | export default class ProviderBridge extends PureComponent { 5 | unmount = null 6 | constructor(props) { 7 | super(props) 8 | 9 | const { value: ctxValues } = props 10 | 11 | if (ctxValues.length === 0) { 12 | this.state = { 13 | ctxValue: null, 14 | } 15 | 16 | return 17 | } 18 | 19 | const [{ ctx, value, onUpdate }] = ctxValues 20 | 21 | this.state = { 22 | ctxValue: value, 23 | } 24 | 25 | this.unmount = onUpdate((value) => { 26 | this.setState({ 27 | ctxValue: value, 28 | }) 29 | }) 30 | } 31 | 32 | componentWillUnmount() { 33 | run(this.unmount) 34 | } 35 | 36 | // componentDidCatch(error) { 37 | // console.error('ProviderBridge Error', error) 38 | // } 39 | 40 | render() { 41 | const { value: propCtxValues, children } = this.props 42 | const ctxValues = propCtxValues.filter(Boolean) 43 | 44 | if (ctxValues.length === 0) { 45 | return children 46 | } 47 | 48 | const { ctxValue } = this.state 49 | const [{ ctx }, ...restValues] = ctxValues 50 | const { Provider } = ctx 51 | 52 | const nextChildren = !isUndefined(ctxValue) ? ( 53 | {children} 54 | ) : ( 55 | children 56 | ) 57 | 58 | // 递归 ProviderBridge 修复多个上下文 59 | // 此处未考虑待修复上下文顺序问题,按先来后到顺序处理,但理论上不应存在顺序问题 60 | return restValues.length > 0 ? ( 61 | {nextChildren} 62 | ) : ( 63 | nextChildren 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | npm-publish: 10 | name: npm-publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@master 15 | 16 | - name: Check if version has been updated 17 | id: check 18 | uses: EndBug/version-check@v1 19 | with: 20 | file-url: https://unpkg.com/react-activation/package.json 21 | static-checking: localIsNew 22 | 23 | - name: Log if version has been updated 24 | if: steps.check.outputs.changed == 'true' 25 | run: 'echo "Version change found in commit ${{ steps.check.outputs.commit }}! New version: ${{ steps.check.outputs.version }} (${{ steps.check.outputs.type }})"' 26 | 27 | - name: Set up Node.js 28 | if: steps.check.outputs.changed == 'true' 29 | uses: actions/setup-node@master 30 | with: 31 | node-version: 13.11.0 32 | 33 | - name: Install Dependencies 34 | if: steps.check.outputs.changed == 'true' 35 | uses: bahmutov/npm-install@v1 36 | with: 37 | useLockFile: false 38 | 39 | - name: Build 40 | if: steps.check.outputs.changed == 'true' 41 | run: 'sudo npm run build' 42 | 43 | - name: Set up Git 44 | if: steps.check.outputs.changed == 'true' 45 | run: 'git config --global --add safe.directory /github/workspace' 46 | 47 | - name: Publish 48 | if: steps.check.outputs.changed == 'true' 49 | uses: pascalgn/npm-publish-action@1.3.9 50 | with: 51 | commit_pattern: "^Release (\\S+)" 52 | env: 53 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-activation", 3 | "version": "0.13.4", 4 | "description": " for React like in vue", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup --config", 8 | "format": "prettier --write \"./src/**/*.js\"", 9 | "manual:publish": "npm run build && np --no-cleanup --yolo --no-publish && npm publish" 10 | }, 11 | "keywords": [ 12 | "multiple tabs", 13 | "React Keep Alive", 14 | "Keep Alive", 15 | "keep-alive", 16 | "KeepAlive", 17 | "activation", 18 | "react cache", 19 | "react tabs cache", 20 | "react router cache", 21 | "vue keep-alive" 22 | ], 23 | "sideEffects": false, 24 | "files": [ 25 | "lib", 26 | "babel.js", 27 | "index.js", 28 | "index.d.ts", 29 | "package.json", 30 | "README.md" 31 | ], 32 | "author": "CJY0208", 33 | "license": "ISC", 34 | "homepage": "https://github.com/CJY0208/react-activation", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/CJY0208/react-activation.git" 38 | }, 39 | "peerDependencies": { 40 | "react": ">=16" 41 | }, 42 | "dependencies": { 43 | "hoist-non-react-statics": "^3.3.0", 44 | "react-node-key": "^0.4.0", 45 | "szfe-tools": "^0.0.0-beta.7" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.5.5", 49 | "@babel/plugin-proposal-class-properties": "^7.5.5", 50 | "@babel/preset-env": "^7.5.5", 51 | "@babel/preset-react": "^7.0.0", 52 | "babel-plugin-import": "^1.13.3", 53 | "np": "^6.2.5", 54 | "prettier": "^2.2.1", 55 | "rollup": "^1.11.3", 56 | "rollup-plugin-babel": "^4.3.2", 57 | "rollup-plugin-node-resolve": "^4.2.3", 58 | "rollup-plugin-uglify": "^6.0.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core/context/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import React, { useState, useContext, useEffect } from 'react' 3 | import { run, debounce, isFunction } from 'szfe-tools' 4 | 5 | import { 6 | aliveScopeContext, 7 | AliveScopeProvider as AliveScopeReactProvider, 8 | AliveScopeConsumer as AliveScopeReactConsumer, 9 | aliveNodeContext, 10 | AliveNodeProvider, 11 | AliveNodeConsumer, 12 | } from './reactContext' 13 | import { 14 | eventBus as fakeContextEventBus, 15 | FakeScopeProvider, 16 | FakeScopeConsumer, 17 | } from './FakeScopeContext' 18 | 19 | export const useScopeContext = () => { 20 | if (!isFunction(useContext)) { 21 | return {} 22 | } 23 | 24 | const scopeReactContext = useContext(aliveScopeContext) 25 | 26 | if (scopeReactContext) { 27 | return scopeReactContext 28 | } 29 | 30 | const [context, setContext] = useState(FakeScopeProvider.currentContextValue) 31 | 32 | useEffect(() => { 33 | const updateListener = debounce(setContext) 34 | fakeContextEventBus.on('update', updateListener) 35 | return () => fakeContextEventBus.off('update', updateListener) 36 | }, []) 37 | 38 | return context 39 | } 40 | 41 | export const AliveScopeProvider = ({ children, ...props }) => ( 42 | 43 | {children} 44 | 45 | ) 46 | export const AliveScopeConsumer = ({ children }) => ( 47 | 48 | {(reactContext) => 49 | !!reactContext ? ( 50 | run(children, undefined, reactContext) 51 | ) : ( 52 | {children} 53 | ) 54 | } 55 | 56 | ) 57 | export { 58 | aliveScopeContext, 59 | aliveNodeContext, 60 | AliveNodeProvider, 61 | AliveNodeConsumer, 62 | } 63 | -------------------------------------------------------------------------------- /src/core/Bridge/Suspense.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense, Component, Fragment } from 'react' 2 | import { run, isUndefined, isFunction } from 'szfe-tools' 3 | 4 | // 兼容性检测 5 | const isSupported = isFunction(lazy) && !isUndefined(Suspense) 6 | const SusNotSupported = ({ children }) => run(children) 7 | 8 | const Lazy = isSupported ? lazy(() => new Promise(() => null)) : () => null 9 | 10 | class FallbackListener extends Component { 11 | componentDidMount() { 12 | run(this.props, 'onStart') 13 | } 14 | 15 | componentWillUnmount() { 16 | run(this.props, 'onEnd') 17 | } 18 | 19 | render() { 20 | return null 21 | } 22 | } 23 | 24 | function SuspenseBridge({ children, sus$$ }) { 25 | return ( 26 | // 捕获 Keeper 内部可能存在的 lazy,并触发对应 KeepAlive 位置上的 LazyBridge 27 | 33 | } 34 | > 35 | {children} 36 | 37 | ) 38 | } 39 | 40 | export const LazyBridge = isSupported 41 | ? class LazyBridge extends Component { 42 | state = { 43 | suspense: false, 44 | } 45 | 46 | onSuspenseStart = () => { 47 | this.setState({ 48 | suspense: true, 49 | }) 50 | } 51 | 52 | onSuspenseEnd = () => { 53 | this.setState({ 54 | suspense: false, 55 | }) 56 | } 57 | 58 | sus$$ = { 59 | onSuspenseStart: this.onSuspenseStart, 60 | onSuspenseEnd: this.onSuspenseEnd, 61 | } 62 | 63 | render() { 64 | const { children } = this.props 65 | 66 | return ( 67 | 68 | {run(children, undefined, this.sus$$)} 69 | {/* 渲染 Lazy 以触发 KeepAlive 所处位置外部可能存在的 Suspense */} 70 | {this.state.suspense && } 71 | 72 | ) 73 | } 74 | } 75 | : SusNotSupported 76 | 77 | export default isSupported ? SuspenseBridge : SusNotSupported 78 | -------------------------------------------------------------------------------- /src/core/Bridge/Context/ConsumerWrapper.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import { run, get, isUndefined } from 'szfe-tools' 3 | 4 | import { updateListenerCache } from './fixContext' 5 | 6 | // 在 KeepAlive 位置使用待修复上下文的 Consumer 探测可能存在的上下文关系 7 | // 若成功捕获上下文则保存其内容,用以后续 Keeper 中上下文的重建 8 | export default class ConsumerWrapper extends Component { 9 | updateListener = null 10 | ctxInfo = null 11 | constructor(props) { 12 | super(props) 13 | 14 | const { value, ctx, id } = props 15 | if (isUndefined(value)) { 16 | return 17 | } 18 | 19 | // 因 Consumer 探测器存在于 KeepAlive 外层故会随着 KeepAlive 卸载 20 | // componentWillUnmount 中保留了已生成的更新监听器 21 | // 此处重新挂载后恢复与对应 Keeper 中 ProviderBridge 的联系 22 | this.updateListener = get(updateListenerCache.get(ctx), id, new Map()) 23 | run(this.updateListener, 'forEach', (fn) => fn(value)) 24 | this.ctxInfo = { 25 | ctx, 26 | value, 27 | // 注册上下文更新的监听,保证上下文更新时 Keeper 中 ProviderBridge 内容的同步 28 | onUpdate: (updator) => { 29 | this.updateListener.set(updator, updator) 30 | 31 | // 返回更新监听器的注销方法 32 | return () => this.updateListener.delete(updator) 33 | }, 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | const { value, ctx, id } = this.props 39 | if (isUndefined(value)) { 40 | return 41 | } 42 | 43 | // 因 Consumer 探测器存在于 KeepAlive 外层故会随着 KeepAlive 卸载 44 | // 此处保留其中已生成的更新监听器,用以在重新挂载后保持与对应 Keeper 中 ProviderBridge 的联系 45 | updateListenerCache.set(ctx, { 46 | ...get(updateListenerCache.get(ctx), undefined, {}), 47 | [id]: this.updateListener, 48 | }) 49 | } 50 | 51 | // 利用 shouldComponentUpdate 尽早将上下文更新的咨询通知到对应 Keeper 中 ProviderBridge 52 | // TODO: 改用 componentWillReceiveProps 更早地进行更新,需注意与 getDerivedStateFromProps 新生命周期的兼容及可能存在的死循环问题 53 | shouldComponentUpdate({ value }) { 54 | const { value: prevValue } = this.props 55 | const shouldUpdate = prevValue !== value 56 | 57 | if (shouldUpdate) { 58 | run(this.updateListener, 'forEach', (fn) => fn(value)) 59 | } 60 | 61 | return true 62 | } 63 | 64 | render() { 65 | const { value, renderWrapper, renderContent, id } = this.props 66 | 67 | return renderWrapper((ctx$$) => 68 | renderContent(isUndefined(value) ? ctx$$ : [...ctx$$, this.ctxInfo]) 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReactNode, 3 | ReactNodeArray, 4 | Context, 5 | Component, 6 | ComponentType, 7 | } from 'react' 8 | 9 | export declare type GetProps = C extends ComponentType ? P : never 10 | 11 | type DivProps = React.HTMLAttributes 12 | 13 | export interface KeepAliveProps { 14 | children: ReactNode | ReactNodeArray 15 | name?: string 16 | id?: string 17 | cacheKey?: string 18 | when?: boolean | Array | (() => boolean | Array) 19 | saveScrollPosition?: boolean | string 20 | autoFreeze?: boolean 21 | wrapperProps?: DivProps 22 | contentProps?: DivProps 23 | [key: string]: any 24 | } 25 | 26 | export declare class KeepAlive extends Component {} 27 | export default KeepAlive 28 | 29 | export declare class AliveScope extends Component<{ 30 | children: ReactNode | ReactNodeArray 31 | }> {} 32 | 33 | export declare class NodeKey extends Component<{ 34 | prefix?: string 35 | onHandleNode?: (node: any, mark?: string) => string | undefined | null 36 | }> {} 37 | 38 | export function fixContext(context: Context): void 39 | export function createContext( 40 | defaultValue: T, 41 | calculateChangedBits?: (prev: T, next: T) => number 42 | ): Context 43 | // type ContextFixEntry = [host: any, ...methods: any[]] 44 | export function autoFixContext(...configs: any[]): void 45 | 46 | export function useActivate(effect: () => void): void 47 | export function useUnactivate(effect: () => void): void 48 | 49 | export interface CachingNode { 50 | createTime: number 51 | updateTime: number 52 | name?: string 53 | id: string 54 | [key: string]: any 55 | } 56 | 57 | export interface KeeperDropConfig { 58 | delay: string 59 | refreshIfDropFailed: boolean 60 | } 61 | export interface AliveController { 62 | drop: (name: string | RegExp, config?: KeeperDropConfig) => Promise 63 | dropScope: ( 64 | name: string | RegExp, 65 | config?: KeeperDropConfig 66 | ) => Promise 67 | dropById: (id: string, config?: KeeperDropConfig) => Promise 68 | dropScopeByIds: (ids: string[], config?: KeeperDropConfig) => Promise 69 | refresh: (name: string | RegExp) => Promise 70 | refreshScope: (name: string | RegExp) => Promise 71 | refreshById: (id: string) => Promise 72 | refreshScopeByIds: (ids: string[]) => Promise 73 | clear: () => Promise 74 | getCachingNodes: () => Array 75 | } 76 | export function useAliveController(): AliveController 77 | 78 | export declare function withActivation>>( 79 | component: C 80 | ): C 81 | export declare function withAliveScope>>( 82 | component: C 83 | ): C 84 | -------------------------------------------------------------------------------- /src/core/Bridge/Context/fixContext.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, Fragment } from 'react' 2 | import { run, get, isString, isFunction, memoize, EventBus, isExist } from 'szfe-tools' 3 | 4 | import createReactContext from '../../../helpers/createReactContext' 5 | import { aliveScopeContext, aliveNodeContext } from '../../context' 6 | 7 | export const fixedContext = [] 8 | export const updateListenerCache = new Map() 9 | export const eventBus = new EventBus() 10 | 11 | export const fixContext = memoize((ctx) => { 12 | if (!isExist(ctx)) { 13 | return 14 | } 15 | 16 | // 排除 KeepAlive 功能的上下文 17 | if ([aliveScopeContext, aliveNodeContext].includes(ctx)) { 18 | return 19 | } 20 | 21 | // #259: 结合 use-context-selector 时,修复被删除的 Consumer 22 | if (!isExist(ctx.Consumer)) { 23 | function Consumer({ children }) { 24 | const ctxValue = run(useContext, undefined, ctx) 25 | 26 | return {run(children, undefined, ctxValue)} 27 | } 28 | 29 | // 重新声明 Consumer 30 | ctx.Consumer = Consumer 31 | } 32 | 33 | fixedContext.push(ctx) 34 | setTimeout(() => eventBus.emit('update')) 35 | }) 36 | 37 | export const createContext = (defaultValue, calculateChangedBits) => { 38 | const ctx = createReactContext(defaultValue, calculateChangedBits) 39 | 40 | fixContext(ctx) 41 | return ctx 42 | } 43 | 44 | const tryFixCtx = memoize((type) => { 45 | // 尝试读取 Provider 或 Consumer 中的 context 静态属性 46 | const ctx = get(type, '_context') || get(type, 'context') // 16.3.0 版本为 context,之后为 _context 47 | 48 | // 判断是否为 ReactContext 类型 49 | if (get(ctx, '$$typeof') === get(aliveScopeContext, '$$typeof')) { 50 | fixContext(ctx) 51 | } 52 | }) 53 | 54 | const override = (configs) => { 55 | configs.forEach(([host, ...methods]) => { 56 | methods.forEach((method) => { 57 | if ( 58 | !isFunction(get(host, method)) || 59 | get(host, [method, '_overridden']) 60 | ) { 61 | return 62 | } 63 | const originMethod = host[method].bind(host) 64 | host[method] = (type, ...args) => { 65 | if (!isString(type)) { 66 | tryFixCtx(type) 67 | } 68 | return originMethod(type, ...args) 69 | } 70 | host[method]._overridden = true 71 | }) 72 | }) 73 | } 74 | 75 | /** 76 | * 通过覆写 React.createElement 方法来探测 Provider 或 Consumer 的创建,并攫取其中 context 主动进行修复 77 | * TODO:同时兼容 React 17+,目前仅默认兼容 React.createElement 方法 78 | * React 17+ 为 require('react/jsx-runtime') 或 require('react/jsx-dev-runtime') 的 jsx、jsxs、jsxDEV 方法 79 | * 但由于无法动态 require,暂未想到方式同时兼容 80 | * 若需兼容 17+,目前手法为 81 | * 82 | * autoFixContext( 83 | * [require('react/jsx-runtime'), 'jsx', 'jsxs', 'jsxDEV'], 84 | * [require('react/jsx-dev-runtime'), 'jsx', 'jsxs', 'jsxDEV'] 85 | * ) 86 | * 87 | * Note: 需注意 16.2.x 及以下版本不支持此方法 88 | */ 89 | export const autoFixContext = (...configs) => { 90 | try { 91 | override(configs) 92 | } catch (err) { 93 | console.warn('activation override failed:', err) 94 | } 95 | } 96 | 97 | autoFixContext([React, 'createElement']) 98 | -------------------------------------------------------------------------------- /src/core/lifecycles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import React, { 3 | Component, 4 | forwardRef, 5 | useEffect, 6 | useRef, 7 | useContext, 8 | } from 'react' 9 | import hoistStatics from 'hoist-non-react-statics' 10 | import { 11 | get, 12 | run, 13 | nextTick, 14 | isObject, 15 | isFunction, 16 | isUndefined, 17 | } from 'szfe-tools' 18 | 19 | import { AliveNodeConsumer, aliveNodeContext } from './context' 20 | 21 | export const LIFECYCLE_ACTIVATE = 'componentDidActivate' 22 | export const LIFECYCLE_UNACTIVATE = 'componentWillUnactivate' 23 | 24 | export const withActivation = (WrappedComponent) => { 25 | class HOC extends Component { 26 | drop = null 27 | 28 | componentWillUnmount() { 29 | run(this.drop) 30 | } 31 | 32 | render() { 33 | const { forwardedRef, ...props } = this.props 34 | 35 | return ( 36 | 37 | {({ attach } = {}) => ( 38 | { 40 | if ( 41 | [LIFECYCLE_ACTIVATE, LIFECYCLE_UNACTIVATE].every( 42 | (lifecycleName) => !isFunction(get(ref, lifecycleName)) 43 | ) 44 | ) { 45 | return 46 | } 47 | this.drop = run(attach, undefined, ref) 48 | 49 | // 以下保持 ref 功能 50 | if (isUndefined(forwardedRef)) { 51 | return 52 | } 53 | 54 | if (isObject(forwardedRef) && 'current' in forwardedRef) { 55 | forwardedRef.current = ref 56 | return 57 | } 58 | 59 | run(forwardedRef, undefined, ref) 60 | }} 61 | {...props} 62 | /> 63 | )} 64 | 65 | ) 66 | } 67 | } 68 | 69 | // 由于 KeepAlive 内组件渲染与实际内容落后一个节拍 70 | // 将导致真实节点的 componentDidMount 无法及时获取到 KeepAlive 中内容的 ref 值 71 | // 此处对使用了 withActivation HOC 的组件 componentDidMount 做 nextTick 延时处理 72 | // 修复上述问题 73 | 74 | if (isFunction(WrappedComponent.prototype.componentDidMount)) { 75 | WrappedComponent.prototype._componentDidMount = 76 | WrappedComponent.prototype.componentDidMount 77 | WrappedComponent.prototype.componentDidMount = function componentDidMount() { 78 | nextTick(() => WrappedComponent.prototype._componentDidMount.call(this)) 79 | } 80 | } 81 | 82 | if (isFunction(forwardRef)) { 83 | const ForwardedRefHOC = forwardRef((props, ref) => ( 84 | 85 | )) 86 | 87 | return hoistStatics(ForwardedRefHOC, WrappedComponent) 88 | } else { 89 | return hoistStatics(HOC, WrappedComponent) 90 | } 91 | } 92 | 93 | const useActivation = (funcName, func) => { 94 | // 兼容性判断 95 | if ([useRef, useContext, useEffect].some((fn) => !isFunction(fn))) { 96 | return 97 | } 98 | 99 | const ctxValue = useContext(aliveNodeContext) 100 | 101 | // 未处于 KeepAlive 中 102 | if (!ctxValue) { 103 | return 104 | } 105 | 106 | const { current: ref } = useRef({}) 107 | const { attach } = ctxValue 108 | 109 | ref[funcName] = func 110 | ref.drop = attach(ref) 111 | 112 | useEffect(() => { 113 | return () => run(ref.drop) 114 | }, []) 115 | } 116 | 117 | export const useActivate = useActivation.bind(null, LIFECYCLE_ACTIVATE) 118 | export const useUnactivate = useActivation.bind(null, LIFECYCLE_UNACTIVATE) 119 | -------------------------------------------------------------------------------- /src/core/Bridge/Context/ConsumerBridge.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import React, { 3 | PureComponent, 4 | useContext, 5 | useRef, 6 | useEffect, 7 | useState, 8 | } from 'react' 9 | import { run, get, nextTick, isUndefined, isFunction, isExist } from 'szfe-tools' 10 | 11 | import ConsumerWrapper from './ConsumerWrapper' 12 | import { fixedContext, eventBus, updateListenerCache } from './fixContext' 13 | 14 | const fixedContextSnapshot = {} 15 | 16 | // 对 ConsumerWrapper 的递归结构,会在 devtool 中生成较深的嵌套结构,可用 hooks 消除嵌套结构 17 | class RecursiveConsumerBridge extends PureComponent { 18 | constructor(props) { 19 | super(props) 20 | const { id } = props 21 | 22 | if (!fixedContextSnapshot[id]) { 23 | fixedContextSnapshot[id] = [...fixedContext].filter(ctx => isExist(ctx.Consumer)) 24 | } 25 | } 26 | 27 | renderWrapper = (renderChildren) => { 28 | const { id } = this.props 29 | 30 | const renderWrapper = fixedContextSnapshot[id].reduce( 31 | (render, ctx) => { 32 | const { Consumer } = ctx 33 | 34 | const renderWrapper = (renderContent) => ( 35 | 36 | {(value) => ( 37 | 46 | )} 47 | 48 | ) 49 | 50 | return renderWrapper 51 | }, 52 | (renderContent) => renderContent([]) 53 | ) 54 | 55 | return renderWrapper(renderChildren) 56 | } 57 | 58 | render() { 59 | const { children: renderChildren } = this.props 60 | 61 | return this.renderWrapper(renderChildren) 62 | } 63 | } 64 | 65 | // 若支持 Hooks,就不需要递归了,相关实现解释可参考 ConsumerWrapper 66 | // function HooksConsumerBridge({ children: renderChildren, id }) { 67 | // const [, setRenderKey] = useState(Math.random) 68 | 69 | // useEffect(() => { 70 | // // 渲染时若 fixedContext 列表更新,则需强制刷新 71 | // const updateListener = () => setRenderKey(Math.random) 72 | // eventBus.on('update', updateListener) 73 | // return () => { 74 | // eventBus.off('update', updateListener) 75 | // } 76 | // }, []) 77 | 78 | // const context$$ = fixedContext 79 | // .map((ctx) => { 80 | // const value = useContext(ctx) 81 | // const prevValueRef = useRef(value) 82 | // const { current: updateListener } = useRef( 83 | // get(updateListenerCache.get(ctx), id, new Map()) 84 | // ) 85 | 86 | // // 尽可能早地进行更新 87 | // if (prevValueRef.current !== value) { 88 | // nextTick(() => run(updateListener, 'forEach', (fn) => fn(value))) 89 | // } 90 | // prevValueRef.current = value 91 | 92 | // useEffect(() => { 93 | // return () => { 94 | // if (isUndefined(value)) { 95 | // return 96 | // } 97 | 98 | // updateListenerCache.set(ctx, { 99 | // ...get(updateListenerCache.get(ctx), undefined, {}), 100 | // [id]: updateListener, 101 | // }) 102 | // } 103 | // }, []) 104 | 105 | // return { 106 | // ctx, 107 | // value, 108 | // onUpdate: (fn) => { 109 | // updateListener.set(fn, fn) 110 | 111 | // return () => updateListener.delete(fn) 112 | // }, 113 | // } 114 | // }) 115 | // .filter(({ value }) => !isUndefined(value)) 116 | 117 | // return renderChildren(context$$) 118 | // } 119 | 120 | // fix #99 121 | // const ConsumerBridge = [useContext, useRef, useEffect].every(isFunction) 122 | // ? HooksConsumerBridge 123 | // : RecursiveConsumerBridge 124 | // fix #99 125 | export default RecursiveConsumerBridge 126 | -------------------------------------------------------------------------------- /src/helpers/createReactContext.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/jamiebuilds/create-react-context/blob/master/src/implementation.js 2 | import React, { Component } from 'react' 3 | import { random } from 'szfe-tools' 4 | 5 | const MAX_SIGNED_31_BIT_INT = 1073741823 6 | 7 | // Inlined Object.is polyfill. 8 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 9 | function objectIs(x, y) {} 10 | 11 | function createEventEmitter(value) { 12 | let handlers = [] 13 | return { 14 | on(handler) { 15 | handlers.push(handler) 16 | }, 17 | 18 | off(handler) { 19 | handlers = handlers.filter((h) => h !== handler) 20 | }, 21 | 22 | get() { 23 | return value 24 | }, 25 | 26 | set(newValue, changedBits) { 27 | value = newValue 28 | handlers.forEach((handler) => handler(value, changedBits)) 29 | }, 30 | } 31 | } 32 | 33 | function onlyChild(children) { 34 | return Array.isArray(children) ? children[0] : children 35 | } 36 | 37 | function createReactContext(defaultValue, calculateChangedBits) { 38 | const contextProp = '__create-react-context-' + random() + '__' 39 | 40 | class Provider extends Component { 41 | emitter = createEventEmitter(this.props.value) 42 | 43 | getChildContext() { 44 | return { 45 | [contextProp]: this.emitter, 46 | } 47 | } 48 | 49 | componentWillReceiveProps(nextProps) { 50 | if (this.props.value !== nextProps.value) { 51 | let oldValue = this.props.value 52 | let newValue = nextProps.value 53 | let changedBits 54 | 55 | if (objectIs(oldValue, newValue)) { 56 | changedBits = 0 // No change 57 | } else { 58 | changedBits = 59 | typeof calculateChangedBits === 'function' 60 | ? calculateChangedBits(oldValue, newValue) 61 | : MAX_SIGNED_31_BIT_INT 62 | 63 | changedBits |= 0 64 | 65 | if (changedBits !== 0) { 66 | this.emitter.set(nextProps.value, changedBits) 67 | } 68 | } 69 | } 70 | } 71 | 72 | render() { 73 | return this.props.children 74 | } 75 | } 76 | 77 | class Consumer extends Component { 78 | observedBits 79 | 80 | state = { 81 | value: this.getValue(), 82 | } 83 | 84 | componentWillReceiveProps(nextProps) { 85 | let { observedBits } = nextProps 86 | this.observedBits = 87 | observedBits === undefined || observedBits === null 88 | ? MAX_SIGNED_31_BIT_INT // Subscribe to all changes by default 89 | : observedBits 90 | } 91 | 92 | componentDidMount() { 93 | if (this.context[contextProp]) { 94 | this.context[contextProp].on(this.onUpdate) 95 | } 96 | let { observedBits } = this.props 97 | this.observedBits = 98 | observedBits === undefined || observedBits === null 99 | ? MAX_SIGNED_31_BIT_INT // Subscribe to all changes by default 100 | : observedBits 101 | } 102 | 103 | componentWillUnmount() { 104 | if (this.context[contextProp]) { 105 | this.context[contextProp].off(this.onUpdate) 106 | } 107 | } 108 | 109 | getValue() { 110 | if (this.context[contextProp]) { 111 | return this.context[contextProp].get() 112 | } else { 113 | return defaultValue 114 | } 115 | } 116 | 117 | onUpdate = (newValue, changedBits) => { 118 | const observedBits = this.observedBits | 0 119 | if ((observedBits & changedBits) !== 0) { 120 | this.setState({ value: this.getValue() }) 121 | } 122 | } 123 | 124 | render() { 125 | return onlyChild(this.props.children)(this.state.value) 126 | } 127 | } 128 | 129 | return { 130 | Provider, 131 | Consumer, 132 | } 133 | } 134 | 135 | export default React.createContext || createReactContext 136 | -------------------------------------------------------------------------------- /src/core/withAliveScope.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import React, { forwardRef, useContext } from 'react' 3 | import hoistStatics from 'hoist-non-react-statics' 4 | import { get, isFunction, isUndefined } from 'szfe-tools' 5 | 6 | import { Acceptor } from './Bridge' 7 | import NodeKey from './NodeKey' 8 | import { AliveScopeConsumer, useScopeContext } from './context' 9 | 10 | function controllerCherryPick(controller) { 11 | const { 12 | drop, 13 | dropScope, 14 | refresh, 15 | refreshScope, 16 | clear, 17 | getCachingNodes, 18 | dropById, 19 | dropScopeByIds, 20 | refreshById, 21 | refreshScopeByIds, 22 | } = controller 23 | 24 | return { 25 | drop, 26 | dropScope, 27 | refresh, 28 | refreshScope, 29 | clear, 30 | getCachingNodes, 31 | dropById, 32 | dropScopeByIds, 33 | refreshById, 34 | refreshScopeByIds, 35 | } 36 | } 37 | 38 | export const expandKeepAlive = (KeepAlive) => { 39 | const renderContent = ({ idPrefix, helpers, props, forwardedRef }) => { 40 | const isOutsideAliveScope = isUndefined(helpers) 41 | 42 | if (isOutsideAliveScope) { 43 | console.error('You should not use outside a ') 44 | } 45 | 46 | return isOutsideAliveScope ? ( 47 | get(props, 'children', null) 48 | ) : ( 49 | 50 | {(nkId) => { 51 | const id = props.cacheKey || nkId 52 | 53 | return ( 54 | 55 | {(bridgeProps) => ( 56 | 64 | )} 65 | 66 | ) 67 | }} 68 | 69 | ) 70 | } 71 | const HookExpand = ({ id: idPrefix, forwardedRef, ...props }) => 72 | renderContent({ idPrefix, helpers: useScopeContext(), props, forwardedRef }) 73 | 74 | const WithExpand = ({ id: idPrefix, forwardedRef, ...props }) => ( 75 | 76 | {(helpers) => renderContent({ idPrefix, helpers, props, forwardedRef })} 77 | 78 | ) 79 | 80 | const ExpandKeepAlive = isFunction(useContext) ? HookExpand : WithExpand 81 | 82 | if (isFunction(forwardRef)) { 83 | const ForwardedRefHOC = forwardRef((props, ref) => ( 84 | 85 | )) 86 | 87 | return hoistStatics(ForwardedRefHOC, KeepAlive) 88 | } else { 89 | return hoistStatics(ExpandKeepAlive, KeepAlive) 90 | } 91 | } 92 | 93 | const withAliveScope = (WrappedComponent) => { 94 | const renderContent = ({ helpers, props, forwardedRef }) => ( 95 | 96 | ) 97 | 98 | const HookScope = ({ forwardedRef, ...props }) => 99 | renderContent({ 100 | helpers: controllerCherryPick(useScopeContext() || {}), 101 | props, 102 | forwardedRef, 103 | }) 104 | 105 | const WithScope = ({ forwardedRef, ...props }) => ( 106 | 107 | {(controller = {}) => 108 | renderContent({ 109 | helpers: controllerCherryPick(controller), 110 | props, 111 | forwardedRef, 112 | }) 113 | } 114 | 115 | ) 116 | 117 | const HOCWithAliveScope = isFunction(useContext) ? HookScope : WithScope 118 | 119 | if (isFunction(forwardRef)) { 120 | const ForwardedRefHOC = forwardRef((props, ref) => ( 121 | 122 | )) 123 | 124 | return hoistStatics(ForwardedRefHOC, WrappedComponent) 125 | } else { 126 | return hoistStatics(HOCWithAliveScope, WrappedComponent) 127 | } 128 | } 129 | 130 | export const useAliveController = () => { 131 | if (!isFunction(useContext)) { 132 | return {} 133 | } 134 | 135 | const ctxValue = useScopeContext() 136 | 137 | if (!ctxValue) { 138 | return {} 139 | } 140 | 141 | return controllerCherryPick(ctxValue) 142 | } 143 | 144 | export default withAliveScope 145 | -------------------------------------------------------------------------------- /src/core/AliveScope.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { get, run, flatten, debounce, globalThis as root } from 'szfe-tools' 3 | 4 | import { isRegExp } from '../helpers/is' 5 | import { AliveScopeProvider } from './context' 6 | import Keeper from './Keeper' 7 | 8 | const HANDLE_TYPE_DROP = 'drop' 9 | const HANDLE_TYPE_REFRESH = 'refresh' 10 | 11 | export default class AliveScope extends Component { 12 | store = new Map() 13 | nodes = new Map() 14 | keepers = new Map() 15 | 16 | debouncedForceUpdate = debounce((cb) => this.forceUpdate(cb)) 17 | updateCallbackList = [] 18 | smartForceUpdate = (cb) => { 19 | this.updateCallbackList.push(cb) 20 | this.debouncedForceUpdate(() => { 21 | this.updateCallbackList.forEach((cb) => run(cb)) 22 | this.updateCallbackList = [] 23 | }) 24 | } 25 | update = (id, params) => 26 | new Promise((resolve) => { 27 | const keeper = this.keepers.get(id) 28 | const isNew = !keeper 29 | const now = Date.now() 30 | const node = this.nodes.get(id) || null 31 | this.nodes.set(id, { 32 | id, 33 | createTime: now, 34 | updateTime: now, 35 | ...node, 36 | ...params, 37 | }) 38 | 39 | if (isNew) { 40 | this.helpers = { ...this.helpers } 41 | 42 | this.forceUpdate(resolve) 43 | } else { 44 | const { children, bridgeProps } = params 45 | keeper.setState({ children, bridgeProps }, resolve) 46 | } 47 | }) 48 | 49 | keep = (id, params) => 50 | new Promise((resolve) => { 51 | this.update(id, { 52 | id, 53 | ...params, 54 | }).then(() => { 55 | resolve(this.store.get(id)) 56 | }) 57 | }) 58 | 59 | getCachingNodesByName = (name) => 60 | this.getCachingNodes().filter((node) => 61 | isRegExp(name) ? name.test(node.name) : node.name === name 62 | ) 63 | 64 | getScopeIds = (ids) => { 65 | // 递归采集 scope alive nodes id 66 | const getCachingNodesId = (id) => { 67 | const aliveNodesId = get(this.getCache(id), 'aliveNodesId', []) 68 | 69 | if (aliveNodesId.size > 0) { 70 | return [id, [...aliveNodesId].map(getCachingNodesId)] 71 | } 72 | 73 | return [id, ...aliveNodesId] 74 | } 75 | 76 | return flatten(ids.map((id) => getCachingNodesId(id))) 77 | } 78 | 79 | dropById = (id, ...rest) => this.handleNodes([id], HANDLE_TYPE_DROP, ...rest) 80 | dropScopeByIds = (ids, ...rest) => 81 | this.handleNodes(this.getScopeIds(ids), HANDLE_TYPE_DROP, ...rest) 82 | 83 | drop = (name, ...rest) => 84 | this.handleNodes( 85 | this.getCachingNodesByName(name).map((node) => node.id), 86 | HANDLE_TYPE_DROP, 87 | ...rest 88 | ) 89 | 90 | dropScope = (name, ...rest) => 91 | this.dropScopeByIds( 92 | this.getCachingNodesByName(name).map(({ id }) => id), 93 | ...rest 94 | ) 95 | 96 | refreshById = (id, ...rest) => 97 | this.handleNodes([id], HANDLE_TYPE_REFRESH, ...rest) 98 | refreshScopeByIds = (ids, ...rest) => 99 | this.handleNodes(this.getScopeIds(ids), HANDLE_TYPE_REFRESH, ...rest) 100 | 101 | refresh = (name, ...rest) => 102 | this.handleNodes( 103 | this.getCachingNodesByName(name).map((node) => node.id), 104 | HANDLE_TYPE_REFRESH, 105 | ...rest 106 | ) 107 | 108 | refreshScope = (name, ...rest) => 109 | this.refreshScopeByIds( 110 | this.getCachingNodesByName(name).map(({ id }) => id), 111 | ...rest 112 | ) 113 | 114 | handleNodes = (nodesId, type = HANDLE_TYPE_DROP, ...rest) => 115 | new Promise((resolve) => { 116 | const handleKeepers = [] 117 | 118 | nodesId.forEach((id) => { 119 | const cache = this.store.get(id) 120 | 121 | if (!cache) { 122 | return 123 | } 124 | 125 | const keeper = this.keepers.get(id) 126 | handleKeepers.push(keeper) 127 | }) 128 | 129 | if (handleKeepers.length === 0) { 130 | resolve(false) 131 | return 132 | } 133 | 134 | Promise.all( 135 | handleKeepers.map((keeper) => run(keeper, type, ...rest)) 136 | ).then((responses) => resolve(responses.every(Boolean))) 137 | }) 138 | 139 | clear = (...rest) => 140 | this.handleNodes( 141 | this.getCachingNodes().map(({ id }) => id), 142 | HANDLE_TYPE_DROP, 143 | ...rest 144 | ) 145 | 146 | getCache = (id) => this.store.get(id) 147 | getNode = (id) => this.nodes.get(id) 148 | getCachingNodes = () => [...this.nodes.values()] 149 | 150 | // 静态化节点上下文内容,防止重复渲染 151 | helpers = { 152 | keep: this.keep, 153 | update: this.update, 154 | drop: this.drop, 155 | dropScope: this.dropScope, 156 | dropById: this.dropById, 157 | dropScopeByIds: this.dropScopeByIds, 158 | refresh: this.refresh, 159 | refreshScope: this.refreshScope, 160 | refreshById: this.refreshById, 161 | refreshScopeByIds: this.refreshScopeByIds, 162 | getScopeIds: this.getScopeIds, 163 | clear: this.clear, 164 | getCache: this.getCache, 165 | getNode: this.getNode, 166 | getCachingNodes: this.getCachingNodes, 167 | } 168 | 169 | render() { 170 | const { children = null } = this.props 171 | 172 | return ( 173 | 174 | {children} 175 |
176 | {[...this.nodes.values()].map(({ children, ...props }) => ( 177 | { 184 | this.keepers.set(props.id, keeper) 185 | }} 186 | > 187 | {children} 188 | 189 | ))} 190 |
191 |
192 | ) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/core/Keeper.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Suspense } from 'react' 2 | import { flushSync } from 'react-dom' 3 | import { get, run, nextTick, EventBus } from 'szfe-tools' 4 | 5 | import ReactFreeze from './Freeze' 6 | import Bridge from './Bridge' 7 | import { AliveNodeProvider } from './context' 8 | import { LIFECYCLE_ACTIVATE, LIFECYCLE_UNACTIVATE } from './lifecycles' 9 | 10 | const Freeze = !!Suspense ? ReactFreeze : ({ children }) => children 11 | 12 | export default class Keeper extends PureComponent { 13 | eventBus = new EventBus() 14 | listeners = new Map() 15 | wrapper = null 16 | 17 | constructor(props, ...rest) { 18 | super(props, ...rest) 19 | 20 | this.state = { 21 | children: props.children, 22 | bridgeProps: props.bridgeProps, 23 | key: Math.random(), 24 | freeze: false, 25 | } 26 | } 27 | 28 | cache = undefined 29 | componentDidMount() { 30 | const { store, id } = this.props 31 | const listeners = this.listeners 32 | const node = this.wrapper 33 | 34 | // 已存在检测,防止意外现象 35 | if (store.has(id)) { 36 | return 37 | } 38 | 39 | let nodes 40 | try { 41 | nodes = [...node.children] 42 | } catch (e) { 43 | nodes = [node.children] 44 | } 45 | 46 | this.cache = { 47 | listeners, 48 | aliveNodesId: [], 49 | inited: false, 50 | cached: false, 51 | wrapper: node, 52 | nodes, 53 | [LIFECYCLE_ACTIVATE]: () => this[LIFECYCLE_ACTIVATE](), 54 | [LIFECYCLE_UNACTIVATE]: () => this[LIFECYCLE_UNACTIVATE](), 55 | } 56 | 57 | store.set(id, this.cache) 58 | } 59 | 60 | unmounted = false 61 | safeSetState = (nextState, callback) => { 62 | // fix #170 63 | if (this.unmounted) { 64 | return 65 | } 66 | this.setState(nextState, callback) 67 | } 68 | componentWillUnmount() { 69 | const { store, keepers, id } = this.props 70 | // 卸载前尝试归位 DOM 节点 71 | try { 72 | const cache = store.get(id) 73 | cache.nodes.forEach((node) => { 74 | cache.wrapper.appendChild(node) 75 | }) 76 | } catch (error) { 77 | // console.error(error) // do nothing 78 | } 79 | store.delete(id) 80 | keepers.delete(id) 81 | this.unmounted = true 82 | } 83 | 84 | freezeTimeout = null; 85 | [LIFECYCLE_ACTIVATE]() { 86 | clearTimeout(this.freezeTimeout) 87 | // 激活后,立即解冻 88 | this.safeSetState({ 89 | freeze: false, 90 | }) 91 | this.eventBus.emit(LIFECYCLE_ACTIVATE) 92 | this.listeners.forEach((listener) => run(listener, [LIFECYCLE_ACTIVATE])) 93 | } 94 | 95 | [LIFECYCLE_UNACTIVATE]() { 96 | this.eventBus.emit(LIFECYCLE_UNACTIVATE) 97 | const listeners = [...this.listeners] 98 | 99 | listeners 100 | .reverse() 101 | .forEach(([, listener]) => run(listener, [LIFECYCLE_UNACTIVATE])) 102 | 103 | // 缓存后,延迟冻结,保证各项后续处理得以进行,如关闭弹窗等 104 | clearTimeout(this.freezeTimeout) 105 | this.freezeTimeout = setTimeout(() => { 106 | flushSync(() => { 107 | this.safeSetState({ 108 | freeze: true, 109 | }) 110 | }) 111 | }, 1000) 112 | } 113 | 114 | // // 原先打算更新过程中先重置 dom 节点状态,更新后恢复 dom 节点 115 | // // 但考虑到性能消耗可能过大,且可能因 dom 操作时机引发其他 react 渲染问题,故不使用 116 | // // 对应 KeepAlive 处 update 也注释起来不使用 117 | // // 组件更新后,更新 DOM 节点列表状态 118 | // componentDidUpdate() { 119 | // const { store, id } = this.props 120 | // const node = this.wrapper 121 | // if (get(node, 'children.length') > 0) { 122 | // store[id].nodes = [...node.children] 123 | // } 124 | 125 | // console.log(store[id].nodes) 126 | // } 127 | 128 | // 生命周期绑定 129 | attach = (ref) => { 130 | const listeners = this.listeners 131 | 132 | if (!ref) { 133 | return () => null 134 | } 135 | 136 | if (ref.isKeepAlive) { 137 | nextTick(() => { 138 | const { id, store } = this.props 139 | const cache = store.get(id) 140 | cache.aliveNodesId = new Set([...cache.aliveNodesId, ref.id]) 141 | }) 142 | } 143 | 144 | listeners.set(ref, { 145 | [LIFECYCLE_ACTIVATE]: () => run(ref, LIFECYCLE_ACTIVATE), 146 | [LIFECYCLE_UNACTIVATE]: () => run(ref, LIFECYCLE_UNACTIVATE), 147 | }) 148 | 149 | // 返回 listenerRemover 用以在对应组件卸载时解除监听 150 | return () => { 151 | listeners.delete(ref) 152 | } 153 | } 154 | 155 | // 静态化节点上下文内容,防止重复渲染 156 | contextValue = { 157 | id: this.props.id, 158 | attach: this.attach, 159 | } 160 | 161 | drop = ({ delay = 1200, refreshIfDropFailed = true } = {}) => 162 | new Promise((resolve) => { 163 | let timeout 164 | const { scope, id } = this.props 165 | const drop = () => { 166 | clearTimeout(timeout) 167 | this.eventBus.off(LIFECYCLE_UNACTIVATE, drop) 168 | // 用在多层 KeepAlive 同时触发 drop 时,避免触发深层 KeepAlive 节点的缓存生命周期 169 | this.cache.willDrop = true 170 | scope.nodes.delete(id) 171 | scope.helpers = { ...scope.helpers } 172 | scope.smartForceUpdate(() => resolve(true)) 173 | } 174 | 175 | const canDrop = get(this.cache, 'cached') || get(this.cache, 'willDrop') 176 | if (!canDrop) { 177 | this.eventBus.on(LIFECYCLE_UNACTIVATE, drop) 178 | timeout = setTimeout(() => { 179 | this.eventBus.off(LIFECYCLE_UNACTIVATE, drop) 180 | if (refreshIfDropFailed) { 181 | this.refresh().then((result) => resolve(result)) 182 | } else { 183 | resolve(false) 184 | } 185 | }, delay) 186 | return 187 | } 188 | 189 | drop() 190 | }) 191 | 192 | refresh = () => 193 | new Promise((resolve) => { 194 | const canRefresh = !get(this.cache, 'cached') 195 | if (!canRefresh) { 196 | resolve(false) 197 | } 198 | this.safeSetState( 199 | { 200 | key: Math.random(), 201 | }, 202 | () => resolve(true) 203 | ) 204 | }) 205 | 206 | render() { 207 | const { id, autoFreeze = true, contentProps = {}, ...props } = this.props 208 | const { children, bridgeProps, key, freeze } = this.state 209 | 210 | return ( 211 | 212 |
{ 214 | this.wrapper = node 215 | }} 216 | > 217 |
223 | 224 | 225 | {React.Children.map(children, (child, idx) => 226 | React.cloneElement(child, { 227 | key: `${child.key || ''}:${key}:${idx}`, 228 | }) 229 | )} 230 | 231 | 232 |
233 |
234 |
235 | ) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/core/KeepAlive.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | value, 4 | get, 5 | run, 6 | globalThis as root, 7 | nextTick, 8 | isFunction, 9 | isArray, 10 | debounce, 11 | flatten, 12 | } from 'szfe-tools' 13 | 14 | import { expandKeepAlive } from './withAliveScope' 15 | import { 16 | LIFECYCLE_ACTIVATE, 17 | LIFECYCLE_UNACTIVATE, 18 | withActivation, 19 | } from './lifecycles' 20 | import saveScrollPosition from '../helpers/saveScrollPosition' 21 | 22 | const body = get(root, 'document.body') 23 | const screenScrollingElement = get( 24 | root, 25 | 'document.scrollingElement', 26 | get(root, 'document.documentElement', {}) 27 | ) 28 | 29 | const parseWhenResult = (res) => { 30 | if (isArray(res)) { 31 | return res 32 | } 33 | 34 | return [res] 35 | } 36 | 37 | class KeepAlive extends Component { 38 | static defaultProps = { 39 | saveScrollPosition: true, 40 | } 41 | 42 | id = null // 用作 Keeper 识别 KeepAlive 43 | isKeepAlive = true // 用作 Keeper 识别 KeepAlive 44 | cached = false 45 | constructor(props) { 46 | super(props) 47 | this.id = props.id 48 | this.init() 49 | 50 | // 继承响应父级 KeepAlive 的生命周期 51 | ;[LIFECYCLE_ACTIVATE, LIFECYCLE_UNACTIVATE].forEach((lifecycleName) => { 52 | this[lifecycleName] = () => { 53 | const { id, _helpers } = this.props 54 | const cache = _helpers.getCache(id) 55 | const node = _helpers.getNode(id) 56 | if (node && lifecycleName === LIFECYCLE_ACTIVATE) { 57 | node.updateTime = Date.now() 58 | } 59 | 60 | const cached = lifecycleName === LIFECYCLE_UNACTIVATE 61 | 62 | // 若组件即将卸载则不再触发缓存生命周期 63 | if (!cache || cache.willDrop) { 64 | // 若组件在父 KeepAlive 缓存期间被卸载,后续恢复后需重新触发 init 65 | if (this.cached && !cached) { 66 | this.init() 67 | } 68 | return 69 | } 70 | 71 | run(cache, lifecycleName) 72 | cache.cached = cached 73 | this.cached = cached 74 | } 75 | }) 76 | } 77 | 78 | // DOM 操作将实际内容插入占位元素 79 | inject = (didActivate = true) => { 80 | const { id, saveScrollPosition, _helpers } = this.props 81 | const cache = _helpers.getCache(id) 82 | // DOM 操作有风险,try catch 护体 83 | try { 84 | // // 原计划不增加额外的节点,直接将 Keeper 中所有内容节点一一迁移 85 | // // 后发现缺乏统一 react 认可的外层包裹,可能会造成 react dom 操作的错误 86 | // // 且将导致 KeepAlive 进行 update 时需先恢复各 dom 节点的组件归属,成本过高 87 | // // 故此处增加统一的 div 外层,Keeper 中与 KeepAlive 中各一个且外层不做移除处理 88 | // this.parentNode = this.placeholder.parentNode 89 | // cache.nodes.forEach(node => { 90 | // this.parentNode.insertBefore(node, this.placeholder) 91 | // }) 92 | // this.parentNode.removeChild(this.placeholder) 93 | // 将 AliveScopeProvider 中的渲染内容通过 dom 操作置回当前 KeepAlive 94 | cache.nodes.forEach((node) => { 95 | this.placeholder.appendChild(node) 96 | }) 97 | 98 | if (didActivate && saveScrollPosition) { 99 | // 恢复该节点下各可滚动元素的滚动位置 100 | run(cache.revertScrollPos) 101 | } 102 | } catch (error) { 103 | // console.error(error) 104 | } 105 | } 106 | 107 | // DOM 操作将实际内容移出占位元素 108 | eject = (willUnactivate = true) => { 109 | const { id, _helpers } = this.props 110 | const cache = _helpers.getCache(id) 111 | const nodesNeedToSaveScrollPosition = flatten( 112 | flatten([this.props.saveScrollPosition]).map((flag) => { 113 | if (flag === true) { 114 | return cache.nodes 115 | } 116 | 117 | if (flag === 'screen') { 118 | return [screenScrollingElement, body] 119 | } 120 | 121 | return [...value(run(root, 'document.querySelectorAll', flag), [])] 122 | }) 123 | ).filter(Boolean) 124 | 125 | // DOM 操作有风险,try catch 护体 126 | try { 127 | if (willUnactivate && nodesNeedToSaveScrollPosition.length > 0) { 128 | // 保存该节点下各可滚动元素的滚动位置 129 | cache.revertScrollPos = saveScrollPosition( 130 | nodesNeedToSaveScrollPosition 131 | ) 132 | } 133 | 134 | // // 原计划不增加额外的节点,直接将 Keeper 中所有内容节点一一迁移 135 | // // 后发现缺乏统一 react 认可的外层包裹,可能会造成 react dom 操作的错误 136 | // // 且将导致 KeepAlive 进行 update 时需先恢复各 dom 节点的组件归属,成本过高 137 | // // 故此处增加统一的 div 外层,Keeper 中与 KeepAlive 中各一个且外层不做移除处理 138 | // this.parentNode.insertBefore(this.placeholder, cache.nodes[0]) 139 | // cache.nodes.forEach(node => { 140 | // if (willUnactivate) { 141 | // this.parentNode.removeChild(node) 142 | // } else { 143 | // cache.wrapper.appendChild(node) 144 | // } 145 | // }) 146 | // this.parentNode.insertBefore(this.placeholder, cache.nodes[0]) 147 | // 将 KeepAlive 中的节点恢复为原先的占位节点,保证卸载操作正常进行 148 | cache.nodes.forEach((node) => { 149 | if (willUnactivate) { 150 | this.placeholder.removeChild(node) 151 | } else { 152 | cache.wrapper.appendChild(node) 153 | } 154 | }) 155 | } catch (error) { 156 | // console.error(error) 157 | } 158 | } 159 | 160 | refresh = () => { 161 | const { _helpers, id } = this.props 162 | return _helpers.refreshById(id) 163 | } 164 | 165 | drop = (config) => { 166 | const { _helpers, id } = this.props 167 | return _helpers.dropById(id, config) 168 | } 169 | 170 | init = () => { 171 | const { _helpers, id, children, ...rest } = this.props 172 | 173 | // 将 children 渲染至 AliveScopeProvider 中 174 | _helpers 175 | .keep(id, { 176 | children, 177 | getInstance: () => this, 178 | ...rest, 179 | }) 180 | .then((cache) => { 181 | // fix #22 182 | if (!cache) { 183 | return 184 | } 185 | 186 | this.inject() 187 | 188 | // 触发 didActivate 生命周期 189 | if (cache.inited) { 190 | run(this, LIFECYCLE_ACTIVATE) 191 | } else { 192 | cache.inited = true 193 | } 194 | this.cached = false 195 | }) 196 | } 197 | 198 | update = ({ _helpers, id, name, ...rest } = {}) => { 199 | if (!_helpers || this.cached) { 200 | return 201 | } 202 | 203 | // // 原先打算更新过程中先重置 dom 节点状态,更新后恢复 dom 节点 204 | // // 但考虑到性能消耗可能过大,且可能因 dom 操作时机引发其他 react 渲染问题,故不使用 205 | // // 对应 Keeper 处 componentDidUpdate 也注释起来不使用 206 | // this.eject(false) 207 | _helpers.update(id, { 208 | name, 209 | getInstance: () => this, 210 | ...rest, 211 | }) 212 | // this.inject(false) 213 | } 214 | 215 | // 利用 shouldComponentUpdate 提前触发组件更新 216 | shouldComponentUpdate(nextProps) { 217 | this.update(nextProps) 218 | 219 | return false 220 | } 221 | 222 | // 组件卸载时重置 dom 状态,保证 react dom 操作正常进行,并触发 unactivate 生命周期 223 | componentWillUnmount() { 224 | const { id, _helpers, when: calcWhen = true } = this.props 225 | const cache = _helpers.getCache(id) 226 | const [when, isScope] = parseWhenResult(run(calcWhen)) 227 | 228 | if (!cache) { 229 | return 230 | } 231 | 232 | this.eject() 233 | delete cache.getInstance 234 | 235 | if (!when) { 236 | if (isScope) { 237 | const needToDrop = [ 238 | cache, 239 | ..._helpers.getScopeIds([id]).map((id) => _helpers.getCache(id)), 240 | ].filter(Boolean) 241 | 242 | needToDrop.forEach((cache) => { 243 | cache.willDrop = true 244 | }) 245 | nextTick(() => _helpers.dropScopeByIds([id])) 246 | } else { 247 | cache.willDrop = true 248 | nextTick(() => _helpers.dropById(id)) 249 | } 250 | } 251 | 252 | // 触发 willUnactivate 生命周期 253 | run(this, LIFECYCLE_UNACTIVATE) 254 | } 255 | 256 | render() { 257 | const { wrapperProps = {} } = this.props || {} 258 | return ( 259 |
{ 265 | this.placeholder = node 266 | }} 267 | /> 268 | ) 269 | } 270 | } 271 | 272 | // 兼容 SSR 服务端渲染 273 | function SSRKeepAlive({ children }) { 274 | const { wrapperProps = {}, contentProps = {} } = this.props || {} 275 | return ( 276 |
282 |
288 | {children} 289 |
290 |
291 | ) 292 | } 293 | 294 | export default isFunction(get(root, 'document.getElementById')) 295 | ? expandKeepAlive(withActivation(KeepAlive)) 296 | : SSRKeepAlive 297 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ⚠️ 注意 2 | 3 | - 不要使用 `` 严格模式 4 | - (React v18+) 不要使用 `ReactDOMClient.createRoot`, 而是使用 `ReactDOM.render`, https://github.com/CJY0208/react-activation/issues/225#issuecomment-1311136388 5 | 6 | 或者可以关闭 `autoFreeze` 来兼容 `createRoot`,但这会造成性能损失 7 | 8 | ```jsx 9 | import { KeepAlive } from 'react-activation' 10 | KeepAlive.defautProps.autoFreeze = false // default 'true' 11 | ``` 12 | 13 | # React Activation 14 | 15 | [![size](https://img.shields.io/bundlephobia/minzip/react-activation@latest.svg)](https://bundlephobia.com/result?p=react-activation@latest) 16 | [![dm](https://img.shields.io/npm/dm/react-activation.svg)](https://github.com/CJY0208/react-activation) 17 | ![](https://komarev.com/ghpvc/?username=cjy0208-react-activation&label=VIEWS) 18 | 19 | [English](./README.md) | 中文说明 20 | 21 | Vue 中 `` 功能在 React 中的黑客实现 22 | 23 | 建议关注 `React 18.x` 中的官方实现 [``](https://github.com/reactwg/react-18/discussions/19) 24 | 25 | --- 26 | 27 | 配合 babel 预编译实现更稳定的 KeepAlive 功能 28 | 29 | [实现原理说明 - 想读源码的同学关注这里~](https://github.com/CJY0208/react-activation/blob/master/README_CN.md#%E5%8E%9F%E7%90%86%E6%A6%82%E8%BF%B0) 30 | 31 | [在线 Demo](https://codesandbox.io/s/affectionate-beaver-solkt) 32 | 33 | 34 | 35 | --- 36 | 37 | ## 更多复杂示例 38 | 39 | - [可关闭的路由 tabs 示例](https://codesandbox.io/s/keguanbideyifangwenluyou-tab-shilikeanluyoucanshufenduofenhuancun-ewycx) 40 | - [可关闭的路由 tabs 示例(`umijs`)](https://codesandbox.io/s/umi-keep-alive-tabs-demo-knfxy) 41 | - [使用路由转场动画](https://codesandbox.io/s/luyouzhuanchangdonghuashili-jdhq1) 42 | 43 | --- 44 | 45 | ## 兼容性 46 | 47 | - React v16 / v17 / v18 48 | 49 | - Preact v10+ 50 | 51 | - 兼容 SSR 52 | 53 | --- 54 | 55 | ## 安装 56 | 57 | ```bash 58 | yarn add react-activation 59 | # 或者 60 | npm install react-activation 61 | ``` 62 | 63 | --- 64 | 65 | ## 使用方式 66 | 67 | #### 1. (可选,建议)babel 配置文件 `.babelrc` 中增加 `react-activation/babel` 插件 68 | 69 | [为什么需要它?](https://github.com/CJY0208/react-activation/issues/18#issuecomment-564360695) 70 | 71 | 该插件将借助 [`react-node-key`](https://github.com/CJY0208/react-node-key) 于编译阶段在各 JSX 元素上增加 `_nk` 属性,帮助 `react-activation` 在运行时**按渲染位置生成唯一的缓存 id 标识** 72 | 73 | ```javascript 74 | { 75 | "plugins": [ 76 | "react-activation/babel" 77 | ] 78 | } 79 | ``` 80 | 81 | (0.11.0+)如果不使用 babel,建议给每个 `` 声明全局唯一且不变的 `cacheKey` 属性,以确保缓存的稳定性,如下 82 | 83 | ```jsx 84 | 85 | ``` 86 | 87 | #### 2. 用 `` 包裹需要保持状态的组件 88 | 89 | 如例子中的 `` 组件 90 | 91 | ```javascript 92 | // App.js 93 | 94 | import React, { useState } from 'react' 95 | import KeepAlive from 'react-activation' 96 | 97 | function Counter() { 98 | const [count, setCount] = useState(0) 99 | 100 | return ( 101 |
102 |

count: {count}

103 | 104 |
105 | ) 106 | } 107 | 108 | function App() { 109 | const [show, setShow] = useState(true) 110 | 111 | return ( 112 |
113 | 114 | {show && ( 115 | 116 | 117 | 118 | )} 119 |
120 | ) 121 | } 122 | 123 | export default App 124 | ``` 125 | 126 | ##### 3. 在不会被销毁的位置放置 `` 外层,一般为应用入口处 127 | 128 | 注意:与 `react-router` 或 `react-redux` 配合使用时,建议将 `` 放置在 `` 或 `` 内部 129 | 130 | ```javascript 131 | // index.js 132 | 133 | import React from 'react' 134 | import ReactDOM from 'react-dom' 135 | import { AliveScope } from 'react-activation' 136 | 137 | import App from './App' 138 | 139 | ReactDOM.render( 140 | 141 | 142 | , 143 | document.getElementById('root') 144 | ) 145 | ``` 146 | 147 | --- 148 | 149 | ## 生命周期 150 | 151 | `ClassComponent` 可配合 `withActivation` 装饰器 152 | 153 | 使用 `componentDidActivate` 与 `componentWillUnactivate` 对应激活与缓存两种状态 154 | 155 | `FunctionComponent` 则分别使用 `useActivate` 与 `useUnactivate` hooks 钩子 156 | 157 | ```javascript 158 | ... 159 | import KeepAlive, { useActivate, useUnactivate, withActivation } from 'react-activation' 160 | 161 | @withActivation 162 | class TestClass extends Component { 163 | ... 164 | componentDidActivate() { 165 | console.log('TestClass: componentDidActivate') 166 | } 167 | 168 | componentWillUnactivate() { 169 | console.log('TestClass: componentWillUnactivate') 170 | } 171 | ... 172 | } 173 | ... 174 | function TestFunction() { 175 | useActivate(() => { 176 | console.log('TestFunction: didActivate') 177 | }) 178 | 179 | useUnactivate(() => { 180 | console.log('TestFunction: willUnactivate') 181 | }) 182 | ... 183 | } 184 | ... 185 | function App() { 186 | ... 187 | return ( 188 | {show && ( 189 | 190 | 191 | 192 | 193 | )} 194 | ) 195 | } 196 | ... 197 | ``` 198 | 199 | --- 200 | 201 | ## 缓存控制 202 | 203 | ### 手动控制缓存 204 | 205 | 1. 给需要控制缓存的 `` 标签增加 `name` 属性 206 | 207 | 2. 使用 `withAliveScope` 或 `useAliveController` 获取控制函数 208 | 209 | - **drop(name)**: ("卸载"仅可用于缓存状态下的节点,如果节点没有被缓存但需要清空缓存状态,请使用 “刷新” 控制) 210 | 211 | 按 name 卸载缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,注意,仅卸载命中 `` 的第一层内容,不会卸载 `` 中嵌套的、未命中的 `` 212 | 213 | - **dropScope(name)**: ("卸载"仅可用于缓存状态下的节点,如果节点没有被缓存但需要清空缓存状态,请使用 “刷新” 控制) 214 | 215 | 按 name 卸载缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,将卸载命中 `` 的所有内容,包括 `` 中嵌套的所有 `` 216 | 217 | - **refresh(name)**: 218 | 219 | 按 name 刷新缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,注意,仅刷新命中 `` 的第一层内容,不会刷新 `` 中嵌套的、未命中的 `` 220 | 221 | - **refreshScope(name)**: 222 | 223 | 按 name 刷新缓存状态下的 `` 节点,name 可选类型为 `String` 或 `RegExp`,将刷新命中 `` 的所有内容,包括 `` 中嵌套的所有 `` 224 | 225 | - **clear()**: 226 | 227 | 将清空所有缓存中的 KeepAlive 228 | 229 | 230 | - **getCachingNodes()**: 231 | 232 | 获取所有缓存中的节点 233 | 234 | ```javascript 235 | ... 236 | import KeepAlive, { withAliveScope, useAliveController } from 'react-activation' 237 | ... 238 | 239 | ... 240 | 241 | ... 242 | 243 | ... 244 | 245 | ... 246 | 247 | ... 248 | 249 | ... 250 | function App() { 251 | const { drop, dropScope, clear, getCachingNodes } = useAliveController() 252 | 253 | useEffect(() => { 254 | drop('Test') 255 | // or 256 | drop(/Test/) 257 | // or 258 | dropScope('Test') 259 | 260 | clear() 261 | }) 262 | 263 | return ( 264 | ... 265 | ) 266 | } 267 | // or 268 | @withAliveScope 269 | class App extends Component { 270 | render() { 271 | const { drop, dropScope, clear, getCachingNodes } = this.props 272 | 273 | return ( 274 | ... 275 | ) 276 | } 277 | } 278 | ... 279 | ``` 280 | 281 | --- 282 | 283 | ### 自动控制缓存 284 | 285 | 给需要控制缓存的 `` 标签增加 `when` 属性,取值如下 286 | 287 | #### 当 `when` 类型为 `Boolean` 时 288 | 289 | - **true**: 卸载时缓存 290 | - **false**: 卸载时不缓存 291 | 292 | ```javascript 293 | 294 | ``` 295 | 296 | #### 当 `when` 类型为 `Array` 时 297 | 298 | **第 1 位**参数表示是否需要在卸载时缓存 299 | 300 | **第 2 位**参数表示是否卸载 `` 的所有缓存内容,包括 `` 中嵌套的所有 `` 301 | 302 | ```javascript 303 | // 例如:以下表示卸载时不缓存,并卸载掉嵌套的所有 `` 304 | 305 | ... 306 | 307 | ... 308 | ... 309 | ... 310 | 311 | ... 312 | 313 | ``` 314 | 315 | #### 当 `when` 类型为 `Function` 时(**建议使用这种方式**) 316 | 317 | 返回值为上述 `Boolean` 或 `Array`,依照上述说明生效 318 | 319 | 但 `when` 的最终计算时机调整到 `` 组件 `componentWillUnmount` 时,可避免大部分 when 属性没有达到预期效果的问题 320 | 321 | ```jsx 322 | true}> 323 | [false, true]}> 324 | ``` 325 | 326 | --- 327 | 328 | ## 多份缓存 329 | 330 | 同一个父节点下,相同位置的 `` 默认会使用同一份缓存 331 | 332 | 例如下述的带参数路由场景,`/item` 路由会按 `id` 来做不同呈现,但只能保留同一份缓存 333 | 334 | ```javascript 335 | ( 338 | 339 | 340 | 341 | )} 342 | /> 343 | ``` 344 | 345 | --- 346 | 347 | 类似场景,可以使用 `` 的 `id` 属性,来实现按特定条件分成多份缓存 348 | 349 | ```javascript 350 | ( 353 | 354 | 355 | 356 | )} 357 | /> 358 | ``` 359 | 360 | ## 保存滚动位置(默认为 `true`) 361 | 362 | `` 会检测它的 `children` 属性中是否存在可滚动的元素,然后在 `componentWillUnactivate` 之前自动保存滚动位置,在 `componentDidActivate` 之后恢复保存的滚动位置 363 | 364 | 如果你不需要 `` 做这件事,可以将 `saveScrollPosition` 属性设置为 `false` 365 | 366 | ```javascript 367 | 368 | ``` 369 | 370 | 如果你的组件共享了屏幕滚动容器如 `document.body` 或 `document.documentElement`, 将 `saveScrollPosition` 属性设置为 `"screen"` 可以在 `componentWillUnactivate` 之前自动保存共享屏幕容器的滚动位置 371 | 372 | ```javascript 373 | 374 | ``` 375 | 376 | --- 377 | 378 | ## 原理概述 379 | 380 | 将 `` 的 `children` 属性传递到 `` 中,通过 `` 进行渲染 381 | 382 | `` 完成渲染后通过 `DOM` 操作,将内容转移到 `` 中 383 | 384 | 由于 `` 不会被卸载,故能实现缓存功能 385 | 386 | [最简实现示例](https://codesandbox.io/s/zuijian-react-keepalive-shixian-ovh90) 387 | 388 | 围绕最简实现,本仓库后续代码主要集中在 389 | 390 | 1. 借助 React 的上下文实现生命周期机制 391 | 392 | 2. 借助[桥接机制](https://github.com/StructureBuilder/react-keep-alive/issues/36#issuecomment-527490445)修复断层 393 | 394 | 3. 借助 babel 标记各节点,建立[渲染坐标系](https://github.com/CJY0208/react-node-key/issues/3)来增强[稳定性](https://github.com/CJY0208/react-activation/issues/18#issuecomment-564360695) 395 | 396 | **(源码欢迎微信讨论 375564567)** 397 | 398 | 399 | 400 | --- 401 | 402 | ## Breaking Change 由实现原理引发的额外问题 403 | 404 | 1. `` 中需要有一个将 children 传递到 `` 的动作,故真实内容的渲染会相较于正常情况**慢一拍** 405 | 406 | 将会对严格依赖生命周期顺序的功能造成一定影响,例如 `componentDidMount` 中 ref 的取值,如下 407 | 408 | ```javascript 409 | class Test extends Component { 410 | componentDidMount() { 411 | console.log(this.outside) // will log
instance 412 | console.log(this.inside) // will log undefined 413 | } 414 | 415 | render() { 416 | return ( 417 |
418 |
{ 420 | this.outside = ref 421 | }} 422 | > 423 | Outside KeepAlive 424 |
425 | 426 |
{ 428 | this.inside = ref 429 | }} 430 | > 431 | Inside KeepAlive 432 |
433 |
434 |
435 | ) 436 | } 437 | } 438 | ``` 439 | 440 | `ClassComponent` 中上述错误可通过利用 `withActivation` 高阶组件修复 441 | 442 | `FunctionComponent` 目前暂无处理方式,可使用 `setTimeout` 或 `nextTick` 延时获取 `ref` 443 | 444 | ```javascript 445 | @withActivation 446 | class Test extends Component { 447 | componentDidMount() { 448 | console.log(this.outside) // will log
instance 449 | console.log(this.inside) // will log
instance 450 | } 451 | 452 | render() { 453 | return ( 454 |
455 |
{ 457 | this.outside = ref 458 | }} 459 | > 460 | Outside KeepAlive 461 |
462 | 463 |
{ 465 | this.inside = ref 466 | }} 467 | > 468 | Inside KeepAlive 469 |
470 |
471 |
472 | ) 473 | } 474 | } 475 | ``` 476 | 477 | 2. 对 Context 的破坏性影响 478 | 479 | react-actication@0.8.0 版本后 React 16.3+ 版本中已自动修复 480 | 481 | react-actication@0.8.0 版本配合 React 17+ 需要做如下调整以达到自动修复效果 482 | 483 | ```jsx 484 | import { autoFixContext } from 'react-activation' 485 | 486 | autoFixContext( 487 | [require('react/jsx-runtime'), 'jsx', 'jsxs', 'jsxDEV'], 488 | [require('react/jsx-dev-runtime'), 'jsx', 'jsxs', 'jsxDEV'] 489 | ) 490 | ``` 491 | 492 | react-actication@0.8.0 以下版本需手动修复,参考以下 493 | 494 | 问题情景参考:https://github.com/StructureBuilder/react-keep-alive/issues/36 495 | 496 | ```jsx 497 | 498 | {show && ( 499 | 500 | 501 | {( 502 | context // 由于渲染层级被破坏,此处无法正常获取 context 503 | ) => } 504 | 505 | 506 | )} 507 | 508 | 509 | ``` 510 | 511 | 修复方式任选一种 512 | 513 | - 使用从 `react-activation` 导出的 `createContext` 创建上下文 514 | 515 | - 使用从 `react-activation` 导出的 `fixContext` 修复受影响的上下文 516 | 517 | ```javascript 518 | ... 519 | import { createContext } from 'react-activation' 520 | 521 | const { Provider, Consumer } = createContext() 522 | ... 523 | // or 524 | ... 525 | import { createContext } from 'react' 526 | import { fixContext } from 'react-activation' 527 | 528 | const Context = createContext() 529 | const { Provider, Consumer } = Context 530 | 531 | fixContext(Context) 532 | ... 533 | ``` 534 | 535 | 3. 对依赖于 React 层级的功能造成影响,如下 536 | 537 | - [x] [react-router 的 withRouter/hooks 功能异常修正](https://github.com/CJY0208/react-activation/issues/77) 538 | - [x] ~~Error Boundaries~~(已修复) 539 | - [x] ~~React.Suspense & React.lazy~~(已修复) 540 | - [ ] React 合成事件冒泡失效 541 | - [ ] 其他未发现的功能 542 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ NOTICE 2 | 3 | - DO NOT use `` 4 | - (React v18+) DO NOT use `ReactDOMClient.createRoot`, use `ReactDOM.render` instead, https://github.com/CJY0208/react-activation/issues/225#issuecomment-1311136388 5 | 6 | or You can disable `autoFreeze` to work with `createRoot` though this may result in performance loss 7 | 8 | ```jsx 9 | import { KeepAlive } from 'react-activation' 10 | KeepAlive.defautProps.autoFreeze = false // default 'true' 11 | ``` 12 | 13 | # React Activation 14 | 15 | [![size](https://img.shields.io/bundlephobia/minzip/react-activation@latest.svg)](https://bundlephobia.com/result?p=react-activation@latest) 16 | [![dm](https://img.shields.io/npm/dm/react-activation.svg)](https://github.com/CJY0208/react-activation) 17 | ![](https://komarev.com/ghpvc/?username=cjy0208-react-activation&label=VIEWS) 18 | 19 | English | [中文说明](./README_CN.md) 20 | 21 | **HACK Implementation** of the `` function in `Vue` For `React` 22 | 23 | Please also pay attention to official support [``](https://github.com/reactwg/react-18/discussions/19) in `React 18.x` 24 | 25 | --- 26 | 27 | More stable `` function with `babel` pre-compilation 28 | 29 | [Online Demo](https://codesandbox.io/s/affectionate-beaver-solkt) 30 | 31 | 32 | 33 | --- 34 | 35 | ## More examples 36 | 37 | - [Closable tabs with `react-router`](https://codesandbox.io/s/keguanbideyifangwenluyou-tab-shilikeanluyoucanshufenduofenhuancun-ewycx) 38 | - [Closable tabs with `umi`](https://codesandbox.io/s/umi-keep-alive-tabs-demo-knfxy) 39 | - [Using Animation with `react-router`](https://codesandbox.io/s/luyouzhuanchangdonghuashili-jdhq1) 40 | 41 | --- 42 | 43 | ## Compatibility 44 | 45 | - React v16 / v17 / v18 46 | 47 | - Preact v10+ 48 | 49 | - Compatible with SSR 50 | 51 | --- 52 | 53 | ## Install 54 | 55 | ```bash 56 | yarn add react-activation 57 | # or 58 | npm install react-activation 59 | ``` 60 | 61 | --- 62 | 63 | ## Usage 64 | 65 | #### 1. (Optional, Recommended) Add `react-activation/babel` plugins in `.babelrc` 66 | 67 | [Why is it needed?](https://github.com/CJY0208/react-activation/issues/18#issuecomment-564360695) 68 | 69 | The plugin adds a `_nk` attribute to each JSX element during compilation to help the `react-activation` runtime **generate an unique identifier by render location** base on [`react-node-key`](https://github.com/CJY0208/react-node-key). 70 | 71 | ```javascript 72 | { 73 | "plugins": [ 74 | "react-activation/babel" 75 | ] 76 | } 77 | ``` 78 | 79 | **(0.11.0+)** If you don't want to use Babel, it is recommended that each `` declare a globally unique and invariant `cacheKey` attribute to ensure the stability of the cache, as follows: 80 | 81 | ```jsx 82 | 83 | ``` 84 | 85 | #### 2. Wrap the components that need to keep states with `` 86 | 87 | Like the `` component in the example 88 | 89 | ```javascript 90 | // App.js 91 | 92 | import React, { useState } from 'react' 93 | import KeepAlive from 'react-activation' 94 | 95 | function Counter() { 96 | const [count, setCount] = useState(0) 97 | 98 | return ( 99 |
100 |

count: {count}

101 | 102 |
103 | ) 104 | } 105 | 106 | function App() { 107 | const [show, setShow] = useState(true) 108 | 109 | return ( 110 |
111 | 112 | {show && ( 113 | 114 | 115 | 116 | )} 117 |
118 | ) 119 | } 120 | 121 | export default App 122 | ``` 123 | 124 | #### 3. Place the `` outer layer at a location that will not be unmounted, usually at the application entrance 125 | 126 | Note: When used with `react-router` or `react-redux`, you need to place `` inside `` or `` 127 | 128 | ```javascript 129 | // index.js 130 | 131 | import React from 'react' 132 | import ReactDOM from 'react-dom' 133 | import { AliveScope } from 'react-activation' 134 | 135 | import App from './App' 136 | 137 | ReactDOM.render( 138 | 139 | 140 | , 141 | document.getElementById('root') 142 | ) 143 | ``` 144 | 145 | --- 146 | 147 | ## Lifecycle 148 | 149 | `ClassComponent` works with `withActivation` decorator 150 | 151 | Use `componentDidActivate` and `componentWillUnactivate` to correspond to the two states of "activate" and "unactivate" respectively. 152 | 153 | `FunctionComponent` uses the `useActivate` and `useUnactivate` hooks respectively 154 | 155 | ```javascript 156 | ... 157 | import KeepAlive, { useActivate, useUnactivate, withActivation } from 'react-activation' 158 | 159 | @withActivation 160 | class TestClass extends Component { 161 | ... 162 | componentDidActivate() { 163 | console.log('TestClass: componentDidActivate') 164 | } 165 | 166 | componentWillUnactivate() { 167 | console.log('TestClass: componentWillUnactivate') 168 | } 169 | ... 170 | } 171 | ... 172 | function TestFunction() { 173 | useActivate(() => { 174 | console.log('TestFunction: didActivate') 175 | }) 176 | 177 | useUnactivate(() => { 178 | console.log('TestFunction: willUnactivate') 179 | }) 180 | ... 181 | } 182 | ... 183 | function App() { 184 | ... 185 | return ( 186 | {show && ( 187 | 188 | 189 | 190 | 191 | )} 192 | ) 193 | } 194 | ... 195 | ``` 196 | 197 | --- 198 | 199 | ## Cache Control 200 | 201 | ### Manually control the cache 202 | 203 | 1. Add the `name` attribute to the `` tag that needs to control the cache. 204 | 205 | 2. Get control functions using `withAliveScope` or `useAliveController`. 206 | 207 | - **drop(name)**: (`drop` can only be used for nodes in the cache state. If the node is not cached but needs to clear the cache state, please use `refresh`) 208 | 209 | 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 ``. 210 | 211 | - **dropScope(name)**: (`dropScope` can only be used for nodes in the cache state. If the node is not cached but needs to clear the cache state, please use `refreshScope`) 212 | 213 | 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 ``. 214 | 215 | - **refresh(name)**: 216 | 217 | Refresh 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 refreshed and will not be uninstalled in ``. Would not refresh nested ``. 218 | 219 | - **refreshScope(name)**: 220 | 221 | Refresh the `` node in cache state by name. The name optional type is `String` or `RegExp`, which will refresh all content of ``, including all `` nested in ``. 222 | 223 | 224 | - **clear()**: 225 | 226 | will clear all `` in the cache 227 | 228 | - **getCachingNodes()**: 229 | 230 | Get all the nodes in the cache 231 | 232 | ```javascript 233 | ... 234 | import KeepAlive, { withAliveScope, useAliveController } from 'react-activation' 235 | ... 236 | 237 | ... 238 | 239 | ... 240 | 241 | ... 242 | 243 | ... 244 | 245 | ... 246 | 247 | ... 248 | function App() { 249 | const { drop, dropScope, clear, getCachingNodes } = useAliveController() 250 | 251 | useEffect(() => { 252 | drop('Test') 253 | // or 254 | drop(/Test/) 255 | // or 256 | dropScope('Test') 257 | 258 | clear() 259 | }) 260 | 261 | return ( 262 | ... 263 | ) 264 | } 265 | // or 266 | @withAliveScope 267 | class App extends Component { 268 | render() { 269 | const { drop, dropScope, clear, getCachingNodes } = this.props 270 | 271 | return ( 272 | ... 273 | ) 274 | } 275 | } 276 | ... 277 | ``` 278 | 279 | --- 280 | 281 | ### Automatic control cache 282 | 283 | Add the `when` attribute to the `` tag that needs to control the cache. The value is as follows 284 | 285 | #### When the `when` type is `Boolean` 286 | 287 | - **true**: Cache after uninstallation 288 | - **false**: Not cached after uninstallation 289 | 290 | ```javascript 291 | 292 | ``` 293 | 294 | #### When the `when` type is `Array` 295 | 296 | The **1th** parameter indicates whether it needs to be cached at the time of uninstallation. 297 | 298 | The **2th** parameter indicates whether to unload all cache contents of ``, including all `` nested in ``. 299 | 300 | ```javascript 301 | // For example: 302 | // The following indicates that it is not cached when uninstalling 303 | // And uninstalls all nested `` 304 | 305 | ... 306 | 307 | ... 308 | ... 309 | ... 310 | 311 | ... 312 | 313 | ``` 314 | 315 | #### When the `when` type is `Function` (**Recommended**) 316 | 317 | The return value is the above `Boolean` or `Array`, which takes effect as described above. 318 | 319 | The final calculation time of `when` is adjusted to `componentWillUnmount` lifecicle of ``, the problem that most of the `when` do not achieve the expected effect can be avoided. 320 | 321 | ```jsx 322 | true}> 323 | [false, true]}> 324 | ``` 325 | 326 | --- 327 | 328 | ## Multiple Cache 329 | 330 | Under the same parent node, `` in the same location will use the same cache by default. 331 | 332 | For example, with the following parameter routing scenario, the `/item` route will be rendered differently by `id`, but only the same cache can be kept. 333 | 334 | ```javascript 335 | ( 338 | 339 | 340 | 341 | )} 342 | /> 343 | ``` 344 | 345 | Similar scenarios, you can use the `id` attribute of `` to implement multiple caches according to specific conditions. 346 | 347 | ```javascript 348 | ( 351 | 352 | 353 | 354 | )} 355 | /> 356 | ``` 357 | 358 | --- 359 | 360 | ## Save Scroll Position (`true` by default) 361 | 362 | `` would try to detect scrollable nodes in its `children`, then, save their scroll position automaticlly before `componentWillUnactivate` and restore saving position after `componentDidActivate` 363 | 364 | If you don't want `` to do this thing, set `saveScrollPosition` prop to `false` 365 | 366 | ```javascript 367 | 368 | ``` 369 | 370 | If your components share screen scroll container, `document.body` or `document.documentElement`, set `saveScrollPosition` prop to `"screen"` can save sharing screen container's scroll position before `componentWillUnactivate` 371 | 372 | ```javascript 373 | 374 | ``` 375 | 376 | --- 377 | 378 | ## Principle 379 | 380 | Pass the `children` attribute of `` to `` and render it with `` 381 | 382 | After rendering ``, the content is transferred to `` through `DOM` operation. 383 | 384 | Since `` will not be uninstalled, caching can be implemented. 385 | 386 | [Simplest Implementation Demo](https://codesandbox.io/s/zuijian-react-keepalive-shixian-ovh90) 387 | 388 | 389 | 390 | --- 391 | 392 | ## Breaking Change 393 | 394 | 1. `` needs to pass children to `` , so the rendering of the real content will be **slower than the normal situation** 395 | 396 | Will have a certain impact on the function of strictly relying on the lifecycle order, such as getting the value of `ref` in `componentDidMount`, as follows 397 | 398 | ```javascript 399 | class Test extends Component { 400 | componentDidMount() { 401 | console.log(this.outside) // will log
instance 402 | console.log(this.inside) // will log undefined 403 | } 404 | 405 | render() { 406 | return ( 407 |
408 |
{ 410 | this.outside = ref 411 | }} 412 | > 413 | Outside KeepAlive 414 |
415 | 416 |
{ 418 | this.inside = ref 419 | }} 420 | > 421 | Inside KeepAlive 422 |
423 |
424 |
425 | ) 426 | } 427 | } 428 | ``` 429 | 430 | The above error in `ClassComponent` can be fixed by using the `withActivation` high-level component 431 | 432 | `FunctionComponent` currently has no processing method, you can use `setTimeout` or `nextTick` to delay ref getting behavior 433 | 434 | ```javascript 435 | @withActivation 436 | class Test extends Component { 437 | componentDidMount() { 438 | console.log(this.outside) // will log
instance 439 | console.log(this.inside) // will log
instance 440 | } 441 | 442 | render() { 443 | return ( 444 |
445 |
{ 447 | this.outside = ref 448 | }} 449 | > 450 | Outside KeepAlive 451 |
452 | 453 |
{ 455 | this.inside = ref 456 | }} 457 | > 458 | Inside KeepAlive 459 |
460 |
461 |
462 | ) 463 | } 464 | } 465 | ``` 466 | 467 | 2. Destructive impact on `Context` 468 | 469 | after `react-actication@0.8.0` with `react@16.3+`, this question has been automatic fixed 470 | 471 | `react-actication@0.8.0` with `react@17+` you Need to make the following changes to achieve automatic repair 472 | 473 | ```jsx 474 | import { autoFixContext } from 'react-activation' 475 | 476 | autoFixContext( 477 | [require('react/jsx-runtime'), 'jsx', 'jsxs', 'jsxDEV'], 478 | [require('react/jsx-dev-runtime'), 'jsx', 'jsxs', 'jsxDEV'] 479 | ) 480 | ``` 481 | 482 | Versions below `react-actication@0.8.0` need to be repaired manually, refer to the following 483 | 484 | Problem reference: https://github.com/StructureBuilder/react-keep-alive/issues/36 485 | 486 | ```javascript 487 | 488 | {show && ( 489 | 490 | 491 | {( 492 | context // Since the rendering level is broken, the context cannot be obtained here. 493 | ) => } 494 | 495 | 496 | )} 497 | 498 | 499 | ``` 500 | 501 | Choose a repair method 502 | 503 | - Create `Context` using `createContext` exported from `react-activation` 504 | 505 | - Fix the affected `Context` with `fixContext` exported from `react-activation` 506 | 507 | ```javascript 508 | ... 509 | import { createContext } from 'react-activation' 510 | 511 | const { Provider, Consumer } = createContext() 512 | ... 513 | // or 514 | ... 515 | import { createContext } from 'react' 516 | import { fixContext } from 'react-activation' 517 | 518 | const Context = createContext() 519 | const { Provider, Consumer } = Context 520 | 521 | fixContext(Context) 522 | ... 523 | ``` 524 | 525 | 3. Affects the functionality that depends on the level of the React component, as follows 526 | 527 | - [x] [Fix `withRouter/hooks` of react-router](https://github.com/CJY0208/react-activation/issues/77) 528 | - [x] ~~Error Boundaries~~ (Fixed) 529 | - [x] ~~React.Suspense & React.lazy~~ (Fixed) 530 | - [ ] React Synthetic Event Bubbling Failure 531 | - [ ] Other undiscovered features 532 | 533 | --- 534 | --------------------------------------------------------------------------------