Commit 3fe0ceb4 authored by Shen Chang's avatar Shen Chang

refactor(src): Rewrite with typescript, Added include and exclude

parent a089fc4a
...@@ -892,6 +892,20 @@ ...@@ -892,6 +892,20 @@
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
"@types/hoist-non-react-statics": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz",
"integrity": "sha512-3wTz66vV+WatOAjMST+hKCmo01KYPFgnsu+QeLcn0FuwPCoymX6aj1a4RvFCdVsfh2m0hfTPhE/zTv4M28ho1Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/js-md5": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@types/js-md5/-/js-md5-0.4.2.tgz",
"integrity": "sha512-FUPoQkpQTzA5wz9ebrdVRjsjQsFehr+cW1CVhLcI2UwD/SO/4NHPO1esrXPPbx7ux762U0POmWFSrUjQq2ophw=="
},
"@types/node": { "@types/node": {
"version": "10.12.29", "version": "10.12.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.29.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.29.tgz",
...@@ -914,6 +928,15 @@ ...@@ -914,6 +928,15 @@
"csstype": "^2.2.0" "csstype": "^2.2.0"
} }
}, },
"@types/react-dom": {
"version": "16.8.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.2.tgz",
"integrity": "sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
"license": "MIT", "license": "MIT",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@types/js-md5": "^0.4.2",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
"js-md5": "^0.7.3" "js-md5": "^0.7.3"
}, },
...@@ -43,8 +44,10 @@ ...@@ -43,8 +44,10 @@
"@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/preset-env": "^7.3.4", "@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@types/hoist-non-react-statics": "^3.0.1",
"@types/node": "^10.12.21", "@types/node": "^10.12.21",
"@types/react": "^16.8.1", "@types/react": "^16.8.1",
"@types/react-dom": "^16.8.2",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"codecov": "^3.2.0", "codecov": "^3.2.0",
......
...@@ -2,42 +2,50 @@ import React from 'react'; ...@@ -2,42 +2,50 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import noop from '../utils/noop'; import noop from '../utils/noop';
export default class Comment extends React.PureComponent { interface IReactCommentProps {
parentNode = null; onLoaded: () => void;
currentNode = null; }
commentNode = null;
content = null; class ReactComment extends React.PureComponent<IReactCommentProps> {
public static defaultProps = {
componentDidMount() { onLoaded: noop,
const node = ReactDOM.findDOMNode(this); };
private parentNode: Node;
private currentNode: Element;
private commentNode: Comment;
private content: string;
public componentDidMount() {
const node = ReactDOM.findDOMNode(this) as Element;
const commentNode = this.createComment(); const commentNode = this.createComment();
this.commentNode = commentNode; this.commentNode = commentNode;
this.currentNode = node; this.currentNode = node;
this.parentNode = node.parentNode; this.parentNode = node.parentNode as Node;
this.parentNode.replaceChild(commentNode, node); this.parentNode.replaceChild(commentNode, node);
ReactDOM.unmountComponentAtNode(node); ReactDOM.unmountComponentAtNode(node);
this.props.onLoaded(); this.props.onLoaded();
} }
componentWillUnmount() { public componentWillUnmount() {
this.parentNode.replaceChild(this.currentNode, this.commentNode); this.parentNode.replaceChild(this.currentNode, this.commentNode);
} }
createComment() { private createComment() {
let content = this.props.children; let content = this.props.children;
if (typeof content !== 'string') { if (typeof content !== 'string') {
content = ''; content = '';
} }
content = content.trim(); this.content = (content as string).trim();
this.content = content; return document.createComment(this.content);
return document.createComment(content);
} }
render() { public render() {
return <div />; return <div />;
} }
} }
Comment.defaultProps = { export default ReactComment;
onLoaded: noop,
};
\ No newline at end of file
import React from 'react'; import React from 'react';
import Comment from './Comment'; import Comment from './Comment';
import {LIFECYCLE, ICache, ICacheItem} from './Provider';
import findDOMNodeByFiberNode from '../utils/findDOMNodeByFiberNode'; import findDOMNodeByFiberNode from '../utils/findDOMNodeByFiberNode';
import createUniqueIdentification from '../utils/createUniqueIdentification'; import createUniqueIdentification from '../utils/createUniqueIdentification';
export const LIFECYCLE = { interface IConsumerProps {
MOUNTED: 0, children: React.ReactNode;
UPDATING: 1, identification: string;
UNMOUNTED: 2, keepAlive: boolean;
}; cache: ICache;
setCache: (identification: string, value: ICacheItem) => void;
unactivate: (identification: string) => void;
}
class Consumer extends React.PureComponent<IConsumerProps> {
private renderElement: HTMLElement;
class Consumer extends React.PureComponent { private identification: string = this.props.identification;
renderElement = null;
identification = this.props.identification;
// This attribute is designed to prevent duplicates of the identification of KeepAlive components. // This attribute is designed to prevent duplicates of the identification of KeepAlive components.
key = createUniqueIdentification(); private key: string = createUniqueIdentification();
constructor(props) { constructor(props: IConsumerProps, ...args: any) {
super(props); super(props, ...args);
const {cache, setCache, children} = props; const {cache, setCache, children} = props;
if (!cache || !setCache) { if (!cache || !setCache) {
throw new Error('<KeepAlive> component must be in the <Provider> component.'); throw new Error('<KeepAlive> component must be in the <Provider> component.');
...@@ -26,14 +30,14 @@ class Consumer extends React.PureComponent { ...@@ -26,14 +30,14 @@ class Consumer extends React.PureComponent {
React.Children.only(children); React.Children.only(children);
} }
componentDidMount() { public componentDidMount() {
const { const {
setCache, setCache,
children, children,
keepAlive, keepAlive,
} = this.props; } = this.props;
const {_reactInternalFiber} = this; const {_reactInternalFiber} = this as any;
this.renderElement = findDOMNodeByFiberNode(_reactInternalFiber); this.renderElement = findDOMNodeByFiberNode(_reactInternalFiber) as HTMLElement;
setCache(this.identification, { setCache(this.identification, {
children, children,
keepAlive, keepAlive,
...@@ -44,9 +48,9 @@ class Consumer extends React.PureComponent { ...@@ -44,9 +48,9 @@ class Consumer extends React.PureComponent {
}); });
} }
componentDidUpdate() { public componentDidUpdate() {
const { const {
setCache, setCache,
children, children,
keepAlive, keepAlive,
} = this.props; } = this.props;
...@@ -57,15 +61,15 @@ class Consumer extends React.PureComponent { ...@@ -57,15 +61,15 @@ class Consumer extends React.PureComponent {
}); });
} }
componentWillUnmount() { public componentWillUnmount() {
const {unactivate} = this.props; const {unactivate} = this.props;
unactivate(this.identification); unactivate(this.identification);
} }
render() { public render() {
const {identification} = this; const {identification} = this;
return <Comment>{identification}</Comment>; return <Comment>{identification}</Comment>;
} }
} }
export default Consumer; export default Consumer;
\ No newline at end of file
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Comment from './Comment'; import Comment from './Comment';
import {LIFECYCLE} from './Consumer';
import KeepAliveContext from '../contexts/KeepAliveContext'; import KeepAliveContext from '../contexts/KeepAliveContext';
import createEventEmitter from '../utils/createEventEmitter'; import createEventEmitter from '../utils/createEventEmitter';
import createUniqueIdentification from '../utils/createUniqueIdentification'; import createUniqueIdentification from '../utils/createUniqueIdentification';
...@@ -10,79 +9,119 @@ import createStoreElement from '../utils/createStoreElement'; ...@@ -10,79 +9,119 @@ import createStoreElement from '../utils/createStoreElement';
export const keepAliveProviderTypeName = 'KeepAliveProvider'; export const keepAliveProviderTypeName = 'KeepAliveProvider';
export const START_MOUNTING_DOM = 'startMountingDOM'; export const START_MOUNTING_DOM = 'startMountingDOM';
export enum LIFECYCLE {
MOUNTED,
UPDATING,
UNMOUNTED,
}
export interface ICacheItem {
children: React.ReactNode;
keepAlive: boolean;
lifecycle: LIFECYCLE;
key?: string | null;
renderElement?: HTMLElement;
activated?: boolean;
ifStillActivate?: boolean;
reactivate?: () => void;
}
export interface ICache {
[key: string]: ICacheItem;
}
export interface IKeepAliveProviderImpl {
storeElement: HTMLElement;
cache: ICache;
keys: string[];
eventEmitter: any;
existed: boolean;
providerIdentification: string;
setCache: (identification: string, value: ICacheItem) => void;
unactivate: (identification: string) => void;
isExisted: () => boolean;
}
export interface IKeepAliveProviderProps {
include?: string | string[] | RegExp;
exclude?: string | string[] | RegExp;
}
// TODO: include max exclude // TODO: include max exclude
export default class KeepAliveProvider extends React.PureComponent { export default class KeepAliveProvider extends React.PureComponent<IKeepAliveProviderProps> implements IKeepAliveProviderImpl {
storeElement = createStoreElement(); public static displayName = keepAliveProviderTypeName;
public storeElement = createStoreElement();
// Sometimes data that changes with setState cannot be synchronized, so force refresh // Sometimes data that changes with setState cannot be synchronized, so force refresh
cache = Object.create(null); public cache: ICache = Object.create(null);
keys = []; public keys: string[] = [];
eventEmitter = createEventEmitter(); public eventEmitter = createEventEmitter();
existed = true; public existed: boolean = true;
needRerender = false; private needRerender: boolean = false;
providerIdentification = createUniqueIdentification(); public providerIdentification: string = createUniqueIdentification();
componentDidUpdate() { public componentDidUpdate() {
if (this.needRerender) { if (this.needRerender) {
this.needRerender = false; this.needRerender = false;
this.forceUpdate(); this.forceUpdate();
} }
} }
componentWillUnmount() { public componentWillUnmount() {
this.eventEmitter.clear(); this.eventEmitter.clear();
this.existed = false; this.existed = false;
document.body.removeChild(this.storeElement); document.body.removeChild(this.storeElement);
} }
isExisted = () => { public isExisted = () => {
return this.existed; return this.existed;
}; }
setCache = (identification, value) => { public setCache = (identification: string, value: ICacheItem) => {
const {cache, keys} = this; const {cache, keys} = this;
const {key} = cache[identification] || {}; const currentCache = cache[identification];
if (!key) { const key = currentCache && currentCache.key;
keys.push(identification); if (key && value.key && key !== (value.key as unknown)) {
// this.shiftKey();
}
if (key && value.key && key !== value.key) {
throw new Error('Cached components have duplicates.'); throw new Error('Cached components have duplicates.');
} }
if (!currentCache) {
keys.push(identification);
}
this.cache[identification] = { this.cache[identification] = {
...cache[identification], ...currentCache,
...value, ...value,
}; };
this.forceUpdate(); this.forceUpdate();
};
getMax = () => {
return this.props.max ? parseInt(this.props.max) : null;
};
shiftKey = () => {
const max = this.getMax();
const {keys, cache} = this;
if (!max || keys.length <= max) {
return;
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const currentCache = cache[key];
if (currentCache && !currentCache.activated) {
keys.splice(i, 1);
delete cache[key];
return;
}
}
} }
unactivate = identification => { // private getMax = () => {
// return this.props.max ? parseInt(this.props.max) : null;
// }
// private shiftKey = () => {
// const max = this.getMax();
// const {keys, cache} = this;
// if (!max || keys.length <= max) {
// return;
// }
// for (let i = 0; i < keys.length; i++) {
// const key = keys[i];
// const currentCache = cache[key];
// if (currentCache && !currentCache.activated) {
// keys.splice(i, 1);
// delete cache[key];
// return;
// }
// }
// }
public unactivate = (identification: string) => {
const {cache} = this; const {cache} = this;
this.cache[identification] = { this.cache[identification] = {
...cache[identification], ...cache[identification],
...@@ -91,36 +130,35 @@ export default class KeepAliveProvider extends React.PureComponent { ...@@ -91,36 +130,35 @@ export default class KeepAliveProvider extends React.PureComponent {
lifecycle: LIFECYCLE.UNMOUNTED, lifecycle: LIFECYCLE.UNMOUNTED,
}; };
this.forceUpdate(); this.forceUpdate();
}; }
startMountingDOM = identification => { private startMountingDOM = (identification: string) => {
this.eventEmitter.emit([identification, START_MOUNTING_DOM]); this.eventEmitter.emit([identification, START_MOUNTING_DOM]);
}; }
render() { public render() {
const { const {
cache, cache,
keys,
providerIdentification, providerIdentification,
isExisted, isExisted,
setCache, setCache,
unactivate, unactivate,
shiftKey,
storeElement, storeElement,
eventEmitter, eventEmitter,
} = this; } = this;
const { const {
children, children: innerChildren,
include, include,
exclude, exclude,
} = this.props; } = this.props;
return ( return (
<KeepAliveContext.Provider <KeepAliveContext.Provider
value={{ value={{
cache, cache,
providerIdentification, providerIdentification,
isExisted, isExisted,
setCache, setCache,
shiftKey,
unactivate, unactivate,
storeElement, storeElement,
eventEmitter, eventEmitter,
...@@ -129,19 +167,15 @@ export default class KeepAliveProvider extends React.PureComponent { ...@@ -129,19 +167,15 @@ export default class KeepAliveProvider extends React.PureComponent {
}} }}
> >
<React.Fragment> <React.Fragment>
{children} {innerChildren}
{ {
Object.entries(cache).map(( keys.map(identification => {
[
identification,
{
keepAlive,
children,
lifecycle,
},
],
) => {
const currentCache = cache[identification]; const currentCache = cache[identification];
const {
keepAlive,
children,
lifecycle,
} = currentCache;
let cacheChildren = children; let cacheChildren = children;
if (lifecycle === LIFECYCLE.MOUNTED && !keepAlive) { if (lifecycle === LIFECYCLE.MOUNTED && !keepAlive) {
// If the cache was last enabled, then the components of this keepAlive package are used, // If the cache was last enabled, then the components of this keepAlive package are used,
...@@ -162,7 +196,7 @@ export default class KeepAliveProvider extends React.PureComponent { ...@@ -162,7 +196,7 @@ export default class KeepAliveProvider extends React.PureComponent {
<React.Fragment> <React.Fragment>
<Comment>{identification}</Comment> <Comment>{identification}</Comment>
{cacheChildren} {cacheChildren}
<Comment <Comment
onLoaded={() => this.startMountingDOM(identification)} onLoaded={() => this.startMountingDOM(identification)}
>{identification}</Comment> >{identification}</Comment>
</React.Fragment> </React.Fragment>
...@@ -177,4 +211,4 @@ export default class KeepAliveProvider extends React.PureComponent { ...@@ -177,4 +211,4 @@ export default class KeepAliveProvider extends React.PureComponent {
</KeepAliveContext.Provider> </KeepAliveContext.Provider>
); );
} }
} }
\ No newline at end of file
import React from 'react';
const WithKeepAliveContext = React.createContext();
export default WithKeepAliveContext;
\ No newline at end of file
import React from 'react';
const WithKeepAliveContext = React.createContext({});
export default WithKeepAliveContext;
import React from 'react';
const KeepAliveContext = React.createContext();
export default KeepAliveContext;
\ No newline at end of file
import React from 'react';
const KeepAliveContext = React.createContext({});
export default KeepAliveContext;
...@@ -6,4 +6,4 @@ export { ...@@ -6,4 +6,4 @@ export {
Provider, Provider,
keepAlive, keepAlive,
bindLifecycle, bindLifecycle,
}; };
\ No newline at end of file
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider';
import md5 from './md5';
import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationConsumer from './withIdentificationContextConsumer';
import withKeepAliveConsumer from './withKeepAliveContextConsumer';
import changePositionByComment from './changePositionByComment';
import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive';
export const COMMAND = {
UNACTIVATE: 'unactivate',
UNMOUNT: 'unmount',
ACTIVATE: 'activate',
};
export default function keepAlive({
name,
forwardRef = false,
} = {}) {
return Component => {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
const displayName = name || getDisplayName(Component);
if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
}
Component.prototype.componentDidMount = function () {
const {
_container,
keepAlive,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
} = _container;
notNeedActivate();
const cb = () => {
mount.call(this);
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
componentDidMount.call(this);
if (keepAlive) {
componentDidActivate.call(this);
}
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
const {
_container,
} = this.props;
const {
notNeedActivate,
isNeedActivate,
} = _container;
if (isNeedActivate()) {
notNeedActivate();
mount.call(this);
this._unmounted = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnactivate = function () {
componentWillUnactivate.call(this);
unmount.call(this);
};
Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times
if (!this._unmounted) {
this._unmounted = true;
componentWillUnmount.call(this);
unmount.call(this);
}
};
function mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
function unmount() {
const {
_container: {
identification,
storeElement,
cache,
setLifecycle,
},
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
}
}
@withKeepAliveConsumer
class TriggerLifecycleContainer extends React.PureComponent {
identification = null;
ref = null;
activated = false;
ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
needActivate = true;
lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {eventEmitter, keepAlive} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
componentWillUnmount() {
const {eventEmitter, isExisted, getCombinedKeepAlive} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
if (this.ref) {
this.ref.componentWillUnmount();
}
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
}
// When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) {
if (this.ref) {
this.ref.componentWillUnactivate();
}
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
activate = () => {
this.activated = true;
};
reactivate = () => {
this.ifStillActivate = false;
this.forceUpdate();
};
isNeedActivate = () => {
return this.needActivate;
};
notNeedActivate = () => {
this.needActivate = false;
};
getLifecycle = () => {
return this.lifecycle;
};
setLifecycle = lifecycle => {
this.lifecycle = lifecycle;
};
setRef = ref => {
this.ref = ref;
const {
forwardedRef,
} = this.props;
if (forwardedRef) {
forwardedRef(ref);
}
};
render() {
if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props;
const {
paths,
globalKey,
typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber);
this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
);
// The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification];
if (currentCache) {
this.ifStillActivate = currentCache.activated;
currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate;
}
}
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
setRef,
identification,
ifStillActivate,
} = this;
const {
eventEmitter,
keepAlive,
unactivate,
setCache,
forwardedRef,
isExisted,
storeElement,
cache,
providerIdentification,
...wrapperProps
} = this.props;
return !ifStillActivate
? (
<Consumer
identification={identification}
keepAlive={keepAlive}
>
<IdentificationContext.Provider
value={{
_identification: identification,
_eventEmitter: eventEmitter,
_keepAlive: keepAlive,
_activated: activated,
_getLifecycle: getLifecycle,
_isExisted: isExisted,
}}
>
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={setRef}
_container={{
isNeedActivate,
notNeedActivate,
setLifecycle,
eventEmitter,
identification,
storeElement,
cache,
}}
/>
</IdentificationContext.Provider>
</Consumer>
)
: null;
}
}
@withIdentificationConsumer
class ListenUpperKeepAliveContainer extends React.PureComponent {
combinedKeepAlive = this.props.keepAlive;
state = {
activated: true,
};
componentDidMount() {
this.listenUpperKeepAlive();
}
componentWillUnmount() {
this.unlistenUpperKeepAlive();
}
listenUpperKeepAlive() {
const {_identification, _eventEmitter} = this.props;
if (!_identification) {
return;
}
_eventEmitter.on(
[_identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
_eventEmitter.on(
[_identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
_eventEmitter.on(
[_identification, COMMAND.UNMOUNT],
this.unmount = () => this.setState({activated: false}),
true,
);
}
unlistenUpperKeepAlive() {
const {_identification, _eventEmitter} = this.props;
if (!_identification) {
return;
}
_eventEmitter.off([_identification, COMMAND.ACTIVATE], this.activate);
_eventEmitter.off([_identification, COMMAND.UNACTIVATE], this.unactivate);
_eventEmitter.off([_identification, COMMAND.UNMOUNT], this.unmount);
}
getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
};
render() {
const {
_identification,
_eventEmitter,
_keepAlive,
_activated,
_getLifecycle,
keepAlive,
...wrapperProps
} = this.props;
const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive);
this.combinedKeepAlive = _getLifecycle === undefined || _getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: _identification
? _keepAlive && newKeepAlive
: newKeepAlive;
return activated
? (
<TriggerLifecycleContainer
{...wrapperProps}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
}
}
let NewComponent = ListenUpperKeepAliveContainer;
if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer
{...props}
forwardedRef={ref}
/>
));
}
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component);
};
}
\ No newline at end of file
...@@ -5,7 +5,7 @@ import {COMMAND} from './keepAlive'; ...@@ -5,7 +5,7 @@ import {COMMAND} from './keepAlive';
import withIdentificationContextConsumer from './withIdentificationContextConsumer'; import withIdentificationContextConsumer from './withIdentificationContextConsumer';
import getDisplayName from './getDisplayName'; import getDisplayName from './getDisplayName';
export default function bindLifecycle(Component) { export default function bindLifecycle<P = any>(Component: React.ComponentType<P>) {
const { const {
componentDidMount = noop, componentDidMount = noop,
componentDidUpdate = noop, componentDidUpdate = noop,
...@@ -30,8 +30,8 @@ export default function bindLifecycle(Component) { ...@@ -30,8 +30,8 @@ export default function bindLifecycle(Component) {
componentDidActivate.call(this); componentDidActivate.call(this);
} }
eventEmitter.on( eventEmitter.on(
[identification, COMMAND.ACTIVATE], [identification, COMMAND.ACTIVATE],
this._bindActivate = () => this._needActivate = true, this._bindActivate = () => this._needActivate = true,
true, true,
); );
eventEmitter.on( eventEmitter.on(
...@@ -84,11 +84,11 @@ export default function bindLifecycle(Component) { ...@@ -84,11 +84,11 @@ export default function bindLifecycle(Component) {
const NewComponent = withIdentificationContextConsumer( const NewComponent = withIdentificationContextConsumer(
({ ({
forwardRef, forwardRef,
_identificationContextProps: { _identificationContextProps: {
identification, identification,
eventEmitter, eventEmitter,
activated, activated,
keepAlive, keepAlive,
}, },
...wrapperProps ...wrapperProps
...@@ -105,11 +105,11 @@ export default function bindLifecycle(Component) { ...@@ -105,11 +105,11 @@ export default function bindLifecycle(Component) {
activated, activated,
}} }}
/> />
) )
: null : null
), ),
); );
NewComponent.displayName = `bindLifecycle(${getDisplayName(Component)})`; NewComponent.displayName = `bindLifecycle(${getDisplayName(Component)})`;
return hoistNonReactStatics( return hoistNonReactStatics(
React.forwardRef((props, ref) => ( React.forwardRef((props, ref) => (
...@@ -117,4 +117,4 @@ export default function bindLifecycle(Component) { ...@@ -117,4 +117,4 @@ export default function bindLifecycle(Component) {
)), )),
Component, Component,
); );
}; }
const NODE_TYPES = { enum NODE_TYPES {
ELEMENT: 1, ELEMENT = 1,
COMMENT: 8, COMMENT = 8,
}; }
function findElementsBetweenComments(node, identification) { function findElementsBetweenComments(node: Node, identification: string): Node[] {
const elements = []; const elements = [];
const childNodes = node.childNodes; const childNodes = node.childNodes as any;
let startCommentExist = false; let startCommentExist = false;
for (let i = 0; i < childNodes.length; i++) { for (const child of childNodes) {
const child = childNodes[i];
if ( if (
child.nodeType === NODE_TYPES.COMMENT && child.nodeType === NODE_TYPES.COMMENT &&
child.nodeValue.trim() === identification && child.nodeValue.trim() === identification &&
!startCommentExist !startCommentExist
) { ) {
startCommentExist = true; startCommentExist = true;
...@@ -24,10 +23,9 @@ function findElementsBetweenComments(node, identification) { ...@@ -24,10 +23,9 @@ function findElementsBetweenComments(node, identification) {
return elements; return elements;
} }
function findComment(node, identification) { function findComment(node: Node, identification: string): Node | undefined {
const childNodes = node.childNodes; const childNodes = node.childNodes as any;
for (let i = 0; i < childNodes.length; i++) { for (const child of childNodes) {
const child = childNodes[i];
if ( if (
child.nodeType === NODE_TYPES.COMMENT && child.nodeType === NODE_TYPES.COMMENT &&
child.nodeValue.trim() === identification child.nodeValue.trim() === identification
...@@ -37,7 +35,7 @@ function findComment(node, identification) { ...@@ -37,7 +35,7 @@ function findComment(node, identification) {
} }
} }
export default function changePositionByComment(identification, presentParentNode, originalParentNode) { export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) {
if (!presentParentNode || !originalParentNode) { if (!presentParentNode || !originalParentNode) {
return; return;
} }
...@@ -46,11 +44,11 @@ export default function changePositionByComment(identification, presentParentNod ...@@ -46,11 +44,11 @@ export default function changePositionByComment(identification, presentParentNod
if (!elementNodes.length || !commentNode) { if (!elementNodes.length || !commentNode) {
return; return;
} }
elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling); elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node);
elementNodes.unshift(elementNodes[0].previousSibling); elementNodes.unshift(elementNodes[0].previousSibling as Node);
// Deleting comment elements when using commet components will result in component uninstallation errors // Deleting comment elements when using commet components will result in component uninstallation errors
for (let i = elementNodes.length - 1; i >= 0; i--) { for (let i = elementNodes.length - 1; i >= 0; i--) {
presentParentNode.insertBefore(elementNodes[i], commentNode); presentParentNode.insertBefore(elementNodes[i], commentNode);
} }
originalParentNode.appendChild(commentNode); originalParentNode.appendChild(commentNode);
} }
\ No newline at end of file
type EventNames = string | string[];
type Listener = (...args: any) => void;
export default function createEventEmitter() { export default function createEventEmitter() {
let events = Object.create(null); let events = Object.create(null);
function on(eventNames, listener, direction = false) { function on(eventNames: EventNames, listener: Listener, direction = false) {
eventNames = getEventNames(eventNames); eventNames = getEventNames(eventNames);
let current = events; let current = events;
const maxIndex = eventNames.length - 1; const maxIndex = eventNames.length - 1;
...@@ -9,7 +13,7 @@ export default function createEventEmitter() { ...@@ -9,7 +13,7 @@ export default function createEventEmitter() {
const key = eventNames[i]; const key = eventNames[i];
if (!current[key]) { if (!current[key]) {
current[key] = i === maxIndex ? [] : {}; current[key] = i === maxIndex ? [] : {};
}; }
current = current[key]; current = current[key];
} }
if (!Array.isArray(current)) { if (!Array.isArray(current)) {
...@@ -22,41 +26,43 @@ export default function createEventEmitter() { ...@@ -22,41 +26,43 @@ export default function createEventEmitter() {
} }
} }
function off(eventNames, listener) { function off(eventNames: EventNames, listener: Listener) {
const listeners = getListeners(eventNames); const listeners = getListeners(eventNames);
if (!listeners) { if (!listeners) {
return; return;
} }
const matchIndex = listeners.findIndex(v => v === listener); const matchIndex = listeners.findIndex((v: Listener) => v === listener);
if (matchIndex !== -1) { if (matchIndex !== -1) {
listeners.splice(matchIndex, 1); listeners.splice(matchIndex, 1);
} }
} }
function removeAllListeners(eventNames) { function removeAllListeners(eventNames: EventNames) {
const listeners = getListeners(eventNames); const listeners = getListeners(eventNames);
if (!listeners) { if (!listeners) {
return; return;
} }
eventNames = getEventNames(eventNames); eventNames = getEventNames(eventNames);
const lastEventName = eventNames.pop(); const lastEventName = eventNames.pop();
const event = eventNames.reduce((obj, key) => obj[key], events); if (lastEventName) {
event[lastEventName] = []; const event = eventNames.reduce((obj, key) => obj[key], events);
event[lastEventName] = [];
}
} }
function emit(eventNames, ...args) { function emit(eventNames: EventNames, ...args: any) {
const listeners = getListeners(eventNames); const listeners = getListeners(eventNames);
if (!listeners) { if (!listeners) {
return; return;
} }
for (let i = 0; i < listeners.length; i++) { for (const listener of listeners) {
if (listeners[i]) { if (listener) {
listeners[i](...args); listener(...args);
} }
} }
} }
function listenerCount(eventNames) { function listenerCount(eventNames: EventNames) {
const listeners = getListeners(eventNames); const listeners = getListeners(eventNames);
return listeners ? listeners.length : 0; return listeners ? listeners.length : 0;
} }
...@@ -65,14 +71,16 @@ export default function createEventEmitter() { ...@@ -65,14 +71,16 @@ export default function createEventEmitter() {
events = Object.create(null); events = Object.create(null);
} }
function getListeners(eventNames) { function getListeners(eventNames: EventNames): Listener[] | undefined {
eventNames = getEventNames(eventNames); eventNames = getEventNames(eventNames);
try { try {
return eventNames.reduce((obj, key) => obj[key], events); return eventNames.reduce((obj, key) => obj[key], events);
} catch (e) {} } catch (e) {
return;
}
} }
function getEventNames(eventNames) { function getEventNames(eventNames: EventNames): string[] {
if (!eventNames) { if (!eventNames) {
throw new Error('Must exist event name.'); throw new Error('Must exist event name.');
} }
...@@ -90,4 +98,4 @@ export default function createEventEmitter() { ...@@ -90,4 +98,4 @@ export default function createEventEmitter() {
listenerCount, listenerCount,
removeAllListeners, removeAllListeners,
}; };
}; }
\ No newline at end of file
import {prefix} from './createUniqueIdentification'; import {prefix} from './createUniqueIdentification';
export default function createStoreElement() { export default function createStoreElement(): HTMLElement {
const keepAliveDOM = document.createElement('div'); const keepAliveDOM = document.createElement('div');
keepAliveDOM.dataset.type = prefix; keepAliveDOM.dataset.type = prefix;
keepAliveDOM.style.display = 'none'; keepAliveDOM.style.display = 'none';
document.body.appendChild(keepAliveDOM); document.body.appendChild(keepAliveDOM);
return keepAliveDOM; return keepAliveDOM;
} }
\ No newline at end of file
...@@ -10,8 +10,8 @@ export const prefix = 'keep-alive'; ...@@ -10,8 +10,8 @@ export const prefix = 'keep-alive';
*/ */
export default function createUniqueIdentification(length = 6) { export default function createUniqueIdentification(length = 6) {
const strings = []; const strings = [];
for (var i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
strings[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); strings[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
} }
return `${prefix}-${strings.join('')}`; return `${prefix}-${strings.join('')}`;
} }
\ No newline at end of file
export default function findDOMNodeByFiberNode(fiberNode) { export default function findDOMNodeByFiberNode(fiberNode: any): HTMLElement | null {
if (!fiberNode) { if (!fiberNode) {
return null; return null;
} }
const { const {
stateNode, stateNode,
return: parent, return: parent,
} = fiberNode; } = fiberNode;
if (!parent) { if (!parent) {
...@@ -13,4 +13,4 @@ export default function findDOMNodeByFiberNode(fiberNode) { ...@@ -13,4 +13,4 @@ export default function findDOMNodeByFiberNode(fiberNode) {
return stateNode; return stateNode;
} }
return findDOMNodeByFiberNode(parent); return findDOMNodeByFiberNode(parent);
} }
\ No newline at end of file
import React from 'react';
import {keepAliveProviderTypeName} from '../components/Provider'; import {keepAliveProviderTypeName} from '../components/Provider';
import {keepAliveDisplayName} from './keepAlive';
export default function getContextIdentificationByFiberNode(fiberNode) { export default function getContextIdentificationByFiberNode(fiberNode: any) {
let globalKey = null; let globalKey: React.Key | null = null;
let typeNames = ''; let typeNames = '';
function getPathsByFiberNode(fiberNode) { function getPathsByFiberNode(fiberNode: any) {
if (!fiberNode) { if (!fiberNode) {
return ''; return '';
} }
...@@ -12,12 +14,12 @@ export default function getContextIdentificationByFiberNode(fiberNode) { ...@@ -12,12 +14,12 @@ export default function getContextIdentificationByFiberNode(fiberNode) {
key, key,
index, index,
} = fiberNode; } = fiberNode;
let typeName = type && type.name ? type.name : ''; const typeName = type && type.displayName;
if (typeName === keepAliveProviderTypeName) { if (typeName === keepAliveProviderTypeName) {
return ''; return '';
} }
const joinName = getPathsByFiberNode(fiberNode.return); const joinName: string = getPathsByFiberNode(fiberNode.return);
if (type && type.displayName && type.displayName.indexOf('keepAlive') !== -1) { if (type && type.displayName && type.displayName.indexOf(keepAliveDisplayName) !== -1) {
if (!globalKey) { if (!globalKey) {
globalKey = key; globalKey = key;
} }
...@@ -31,4 +33,4 @@ export default function getContextIdentificationByFiberNode(fiberNode) { ...@@ -31,4 +33,4 @@ export default function getContextIdentificationByFiberNode(fiberNode) {
globalKey, globalKey,
typeNames, typeNames,
}; };
} }
\ No newline at end of file
export default function getDisplayName(Component) { import React from 'react';
export default function getDisplayName(Component: React.ComponentType) {
return Component.displayName || Component.name || null; return Component.displayName || Component.name || null;
} }
\ No newline at end of file
export default function getKeepAlive(keepAlive) {
return keepAlive === undefined ? true : keepAlive;
}
\ No newline at end of file
import isRegExp from './isRegExp';
type Pattern = string | string[] | RegExp;
function matches (pattern: Pattern, name: string) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1;
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1;
} else if (isRegExp(pattern)) {
return pattern.test(name);
}
return false;
}
export default function getKeepAlive(
name: string,
include?: Pattern,
exclude?: Pattern,
keepAlive?: boolean
) {
if (keepAlive !== undefined) {
return keepAlive;
}
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return false;
}
return true;
}
export default function isRegExp(value: RegExp) {
return value && Object.prototype.toString.call(value) === '[object RegExp]';
}
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider';
import md5 from './md5';
import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationConsumer from './withIdentificationConsumer';
import withKeepAliveConsumer from './withKeepAliveConsumer';
import changePositionByComment from './changePositionByComment';
import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive';
export const COMMAND = {
UNACTIVATE: 'unactivate',
UNMOUNT: 'unmount',
ACTIVATE: 'activate',
};
export default function keepAlive({
name,
forwardRef = false,
} = {}) {
return Component => {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
const displayName = name || getDisplayName(Component);
if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
}
Component.prototype.componentDidMount = function () {
const {
_container,
keepAlive,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
} = _container;
notNeedActivate();
const cb = () => {
mount.call(this);
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
componentDidMount.call(this);
if (keepAlive) {
componentDidActivate.call(this);
}
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
const {
_container,
} = this.props;
const {
notNeedActivate,
isNeedActivate,
} = _container;
if (isNeedActivate()) {
notNeedActivate();
mount.call(this);
this._unmounted = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnactivate = function () {
componentWillUnactivate.call(this);
unmount.call(this);
};
Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times
if (!this._unmounted) {
this._unmounted = true;
componentWillUnmount.call(this);
unmount.call(this);
}
};
function mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
function unmount() {
const {
_container: {
identification,
storeElement,
cache,
setLifecycle,
},
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
}
}
class TriggerLifecycleContainer extends React.PureComponent {
identification = null;
ref = null;
activated = false;
ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
needActivate = true;
lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {
keepAlive,
_keepAliveContextProps: {
eventEmitter,
},
} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
componentWillUnmount() {
const {
getCombinedKeepAlive,
_keepAliveContextProps: {
eventEmitter,
isExisted,
},
} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
if (this.ref) {
this.ref.componentWillUnmount();
}
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
}
// When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) {
if (this.ref) {
this.ref.componentWillUnactivate();
}
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
activate = () => {
this.activated = true;
};
reactivate = () => {
this.ifStillActivate = false;
this.forceUpdate();
};
isNeedActivate = () => {
return this.needActivate;
};
notNeedActivate = () => {
this.needActivate = false;
};
getLifecycle = () => {
return this.lifecycle;
};
setLifecycle = lifecycle => {
this.lifecycle = lifecycle;
};
setRef = ref => {
this.ref = ref;
const {
forwardedRef,
} = this.props;
if (forwardedRef) {
forwardedRef(ref);
}
};
render() {
if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props._keepAliveContextProps;
const {
paths,
globalKey,
typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber);
this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
);
// The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification];
if (currentCache) {
this.ifStillActivate = currentCache.activated;
currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate;
}
}
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
setRef,
identification,
ifStillActivate,
} = this;
const {
keepAlive,
forwardedRef,
_keepAliveContextProps: {
isExisted,
storeElement,
cache,
eventEmitter,
},
...wrapperProps
} = this.props;
return !ifStillActivate
? (
<Consumer
identification={identification}
keepAlive={keepAlive}
>
<IdentificationContext.Provider
value={{
identification,
eventEmitter,
keepAlive,
activated,
getLifecycle,
isExisted,
}}
>
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={setRef}
_container={{
isNeedActivate,
notNeedActivate,
setLifecycle,
eventEmitter,
identification,
storeElement,
cache,
}}
/>
</IdentificationContext.Provider>
</Consumer>
)
: null;
}
}
@withIdentificationConsumer
@withKeepAliveConsumer
class ListenUpperKeepAliveContainer extends React.PureComponent {
combinedKeepAlive = this.props.keepAlive;
state = {
activated: true,
};
componentDidMount() {
this.listenUpperKeepAlive();
}
componentWillUnmount() {
this.unlistenUpperKeepAlive();
}
listenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.on(
[identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNMOUNT],
this.unmount = () => this.setState({activated: false}),
true,
);
}
unlistenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate);
eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate);
eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
}
getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
};
render() {
const {
_identificationContextProps: {
identification,
keepAlive: _keepAlive,
getLifecycle,
},
keepAlive,
...wrapperProps
} = this.props;
const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive);
this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: identification
? _keepAlive && newKeepAlive
: newKeepAlive;
return activated
? (
<TriggerLifecycleContainer
{...wrapperProps}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
}
}
let NewComponent = ListenUpperKeepAliveContainer;
if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer
{...props}
forwardedRef={ref}
/>
));
}
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component);
};
}
\ No newline at end of file
import React from 'react'; import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics'; import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext'; import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer'; import Consumer from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider'; import {START_MOUNTING_DOM, LIFECYCLE} from '../components/Provider';
import md5 from './md5'; import md5 from './md5';
import noop from './noop'; import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode'; import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationContextConsumer from './withIdentificationContextConsumer'; import withIdentificationContextConsumer, {IIdentificationContextComponentProps} from './withIdentificationContextConsumer';
import withKeepAliveContextConsumer from './withKeepAliveContextConsumer'; import withKeepAliveContextConsumer, {IKeepAliveContextComponentProps} from './withKeepAliveContextConsumer';
import changePositionByComment from './changePositionByComment'; import changePositionByComment from './changePositionByComment';
import shallowEqual from './shallowEqual'; import shallowEqual from './shallowEqual';
import getDisplayName from './getDisplayName'; import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive'; import getKeepAlive from './getKeepAlive';
export const COMMAND = { export enum COMMAND {
UNACTIVATE: 'unactivate', UNACTIVATE = 'unactivate',
UNMOUNT: 'unmount', UNMOUNT = 'unmount',
ACTIVATE: 'activate', ACTIVATE = 'activate',
}; }
export default function keepAlive({ export interface IOptions {
name?: string;
forwardRef?: boolean;
}
export const keepAliveDisplayName = 'keepAlive';
interface IKeepAliveDecorativeComponentProps {
keepAlive?: boolean;
}
interface IListenUpperKeepAliveContainerProps extends IKeepAliveDecorativeComponentProps, IIdentificationContextComponentProps, IKeepAliveContextComponentProps {
forwardedRef?: React.Ref<{}>;
}
interface IListenUpperKeepAliveContainerState {
activated: boolean;
}
interface ITriggerLifecycleContainerProps extends IKeepAliveContextComponentProps {
forwardedRef?: React.Ref<{}>;
keepAlive: boolean;
getCombinedKeepAlive: () => boolean;
}
interface IComponentProps {
_container: object;
keepAlive: boolean;
ref: React.Ref<{}>;
}
export default function keepAliveDecorator({
name, name,
forwardRef = false, forwardRef = false,
} = {}) { }: IOptions = {}) {
return Component => { return (Component: React.ComponentType<IComponentProps>) => {
const { const {
componentDidMount = noop, componentDidMount = noop,
componentDidUpdate = noop, componentDidUpdate = noop,
...@@ -31,7 +62,7 @@ export default function keepAlive({ ...@@ -31,7 +62,7 @@ export default function keepAlive({
componentWillUnactivate = noop, componentWillUnactivate = noop,
componentWillUnmount = noop, componentWillUnmount = noop,
} = Component.prototype; } = Component.prototype;
const displayName = name || getDisplayName(Component); const displayName = (name || getDisplayName(Component)) as any;
if (!displayName) { if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.'); throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
...@@ -79,7 +110,7 @@ export default function keepAlive({ ...@@ -79,7 +110,7 @@ export default function keepAlive({
unmount.call(this); unmount.call(this);
}; };
Component.prototype.componentWillUnmount = function () { Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle // Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times // so we need to prevent it from firing multiple times
if (!this._unmounted) { if (!this._unmounted) {
this._unmounted = true; this._unmounted = true;
...@@ -88,7 +119,7 @@ export default function keepAlive({ ...@@ -88,7 +119,7 @@ export default function keepAlive({
} }
}; };
function mount() { function mount(this: any) {
const { const {
_container: { _container: {
cache, cache,
...@@ -102,7 +133,7 @@ export default function keepAlive({ ...@@ -102,7 +133,7 @@ export default function keepAlive({
changePositionByComment(identification, renderElement, storeElement); changePositionByComment(identification, renderElement, storeElement);
} }
function unmount() { function unmount(this: any) {
const { const {
_container: { _container: {
identification, identification,
...@@ -119,21 +150,21 @@ export default function keepAlive({ ...@@ -119,21 +150,21 @@ export default function keepAlive({
} }
} }
class TriggerLifecycleContainer extends React.PureComponent { class TriggerLifecycleContainer extends React.PureComponent<ITriggerLifecycleContainerProps> {
identification = null; private identification: string;
ref = null; private ref: any;
activated = false; private activated = false;
ifStillActivate = false; private ifStillActivate = false;
// Let the lifecycle of the cached component be called normally. // Let the lifecycle of the cached component be called normally.
needActivate = true; private needActivate = true;
lifecycle = LIFECYCLE.MOUNTED; private lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() { public componentDidMount() {
if (!this.ifStillActivate) { if (!this.ifStillActivate) {
this.activate(); this.activate();
} }
...@@ -141,7 +172,7 @@ export default function keepAlive({ ...@@ -141,7 +172,7 @@ export default function keepAlive({
keepAlive, keepAlive,
_keepAliveContextProps: { _keepAliveContextProps: {
eventEmitter, eventEmitter,
}, },
} = this.props; } = this.props;
if (keepAlive) { if (keepAlive) {
this.needActivate = true; this.needActivate = true;
...@@ -149,18 +180,18 @@ export default function keepAlive({ ...@@ -149,18 +180,18 @@ export default function keepAlive({
} }
} }
componentDidCatch() { public componentDidCatch() {
if (!this.activated) { if (!this.activated) {
this.activate(); this.activate();
} }
} }
componentWillUnmount() { public componentWillUnmount() {
const { const {
getCombinedKeepAlive, getCombinedKeepAlive,
_keepAliveContextProps: { _keepAliveContextProps: {
eventEmitter, eventEmitter,
isExisted, isExisted,
}, },
} = this.props; } = this.props;
const keepAlive = getCombinedKeepAlive(); const keepAlive = getCombinedKeepAlive();
...@@ -170,7 +201,7 @@ export default function keepAlive({ ...@@ -170,7 +201,7 @@ export default function keepAlive({
} }
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]); eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
} }
// When the Provider components are unmounted, the cache is not needed, // When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle. // so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) { if (keepAlive && isExisted()) {
if (this.ref) { if (this.ref) {
...@@ -180,57 +211,60 @@ export default function keepAlive({ ...@@ -180,57 +211,60 @@ export default function keepAlive({
} }
} }
activate = () => { private activate = () => {
this.activated = true; this.activated = true;
}; }
reactivate = () => { private reactivate = () => {
this.ifStillActivate = false; this.ifStillActivate = false;
this.forceUpdate(); this.forceUpdate();
}; }
isNeedActivate = () => { private isNeedActivate = () => {
return this.needActivate; return this.needActivate;
}; }
notNeedActivate = () => { private notNeedActivate = () => {
this.needActivate = false; this.needActivate = false;
}; }
getLifecycle = () => { private getLifecycle = () => {
return this.lifecycle; return this.lifecycle;
}; }
setLifecycle = lifecycle => { private setLifecycle = (lifecycle: LIFECYCLE) => {
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
}; }
setRef = ref => { private setRef = (ref: any) => {
this.ref = ref; this.ref = ref;
const { const {
forwardedRef, forwardedRef,
} = this.props; } = this.props;
if (forwardedRef) { if (forwardedRef) {
forwardedRef(ref); (forwardedRef as any)(ref);
} }
}; }
render() { public render() {
if (!this.identification) { if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component. // We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props._keepAliveContextProps; const {
providerIdentification,
cache,
} = this.props._keepAliveContextProps;
const { const {
paths, paths,
globalKey, globalKey,
typeNames, typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber); } = getContextIdentificationByFiberNode((this as any)._reactInternalFiber);
this.identification = md5( this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`, `${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
); );
// The last activated component must be unactivated before it can be activated again. // The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification]; const currentCache = cache[this.identification];
if (currentCache) { if (currentCache) {
this.ifStillActivate = currentCache.activated; this.ifStillActivate = currentCache.activated as boolean;
currentCache.ifStillActivate = this.ifStillActivate; currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate; currentCache.reactivate = this.reactivate;
} }
...@@ -248,6 +282,7 @@ export default function keepAlive({ ...@@ -248,6 +282,7 @@ export default function keepAlive({
const { const {
keepAlive, keepAlive,
forwardedRef, forwardedRef,
getCombinedKeepAlive,
_keepAliveContextProps: { _keepAliveContextProps: {
isExisted, isExisted,
storeElement, storeElement,
...@@ -277,7 +312,7 @@ export default function keepAlive({ ...@@ -277,7 +312,7 @@ export default function keepAlive({
isExisted, isExisted,
}} }}
> >
<Component <Component
{...wrapperProps} {...wrapperProps}
keepAlive={keepAlive} keepAlive={keepAlive}
ref={setRef} ref={setRef}
...@@ -293,61 +328,67 @@ export default function keepAlive({ ...@@ -293,61 +328,67 @@ export default function keepAlive({
/> />
</IdentificationContext.Provider> </IdentificationContext.Provider>
</Consumer> </Consumer>
) )
: null; : null;
} }
} }
@withKeepAliveContextConsumer class ListenUpperKeepAliveContainer extends React.Component<IListenUpperKeepAliveContainerProps, IListenUpperKeepAliveContainerState> {
@withIdentificationContextConsumer public static displayName = `${keepAliveDisplayName}(${displayName})`;
class ListenUpperKeepAliveContainer extends React.Component {
combinedKeepAlive = this.props.keepAlive; private combinedKeepAlive: boolean;
state = { public state = {
activated: true, activated: true,
}; };
shouldComponentUpdate(nextProps, nextState) { private activate: () => void;
private unactivate: () => void;
private unmount: () => void;
public shouldComponentUpdate(nextProps: IListenUpperKeepAliveContainerProps, nextState: IListenUpperKeepAliveContainerState) {
if (this.state.activated !== nextState.activated) { if (this.state.activated !== nextState.activated) {
return true; return true;
} }
const { const {
_keepAliveContextProps, _keepAliveContextProps,
_identificationContextProps, _identificationContextProps,
...rest ...rest
} = this.props; } = this.props;
const { const {
_keepAliveContextProps: _nextKeepAliveContextProps, _keepAliveContextProps: nextKeepAliveContextProps,
_identificationContextProps: _nextIdentificationContextProps, _identificationContextProps: nextIdentificationContextProps,
...nextRest ...nextRest
} = nextProps; } = nextProps;
if (!shallowEqual(rest, nextRest)) { if (!shallowEqual(rest, nextRest)) {
return true; return true;
} }
if ( if (
!shallowEqual(_keepAliveContextProps, _nextKeepAliveContextProps) || !shallowEqual(_keepAliveContextProps, nextKeepAliveContextProps) ||
!shallowEqual(_identificationContextProps, _nextIdentificationContextProps) !shallowEqual(_identificationContextProps, nextIdentificationContextProps)
) { ) {
return true; return true;
} }
return false; return false;
} }
componentDidMount() { public componentDidMount() {
this.listenUpperKeepAlive(); this.listenUpperKeepAlive();
} }
componentWillUnmount() { public componentWillUnmount() {
this.unlistenUpperKeepAlive(); this.unlistenUpperKeepAlive();
} }
listenUpperKeepAlive() { private listenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps; const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) { if (!identification) {
return; return;
} }
eventEmitter.on( eventEmitter.on(
[identification, COMMAND.ACTIVATE], [identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}), this.activate = () => this.setState({activated: true}),
true, true,
); );
...@@ -363,7 +404,7 @@ export default function keepAlive({ ...@@ -363,7 +404,7 @@ export default function keepAlive({
); );
} }
unlistenUpperKeepAlive() { private unlistenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps; const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) { if (!identification) {
return; return;
...@@ -373,28 +414,34 @@ export default function keepAlive({ ...@@ -373,28 +414,34 @@ export default function keepAlive({
eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount); eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
} }
getCombinedKeepAlive = () => { private getCombinedKeepAlive = () => {
return this.combinedKeepAlive; return this.combinedKeepAlive;
}; }
render() { public render() {
const { const {
_identificationContextProps: { _identificationContextProps: {
identification, identification,
keepAlive: _keepAlive, keepAlive: upperKeepAlive,
getLifecycle, getLifecycle,
}, },
keepAlive, keepAlive,
...wrapperProps ...wrapperProps
} = this.props; } = this.props;
const {activated} = this.state; const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted, const {
_keepAliveContextProps: {
include,
exclude,
},
} = wrapperProps;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component. // use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive); const newKeepAlive = getKeepAlive(displayName, include, exclude, keepAlive);
this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive ? newKeepAlive
: identification : identification
? _keepAlive && newKeepAlive ? upperKeepAlive && newKeepAlive
: newKeepAlive; : newKeepAlive;
return activated return activated
? ( ? (
...@@ -408,16 +455,19 @@ export default function keepAlive({ ...@@ -408,16 +455,19 @@ export default function keepAlive({
} }
} }
let NewComponent = ListenUpperKeepAliveContainer; const ListenUpperKeepAliveContainerHOC: any = withKeepAliveContextConsumer(
withIdentificationContextConsumer(ListenUpperKeepAliveContainer)
);
let NewComponent: React.ComponentType<IKeepAliveDecorativeComponentProps> = ListenUpperKeepAliveContainerHOC;
if (forwardRef) { if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => ( NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer <ListenUpperKeepAliveContainerHOC
{...props} {...props}
forwardedRef={ref} forwardedRef={ref}
/> />
)); ));
} }
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component); return hoistNonReactStatics(NewComponent, Component);
}; };
} }
\ No newline at end of file
import md5 from 'js-md5'; import md5 from 'js-md5';
import {prefix} from './createUniqueIdentification'; import {prefix} from './createUniqueIdentification';
export default function createMD5(string = '', length = 6) { export default function createMD5(value: string = '', length = 6) {
return `${prefix}-${md5(string).substr(0, length)}`; return `${prefix}-${md5(value).substr(0, length)}`;
} }
\ No newline at end of file
export default function noop() {};
\ No newline at end of file
const noop = () => undefined;
export default noop;
/** /**
* From react * From react
*/ */
function is(x, y) { function is(x: any, y: any) {
return ( return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
); );
...@@ -9,7 +9,7 @@ function is(x, y) { ...@@ -9,7 +9,7 @@ function is(x, y) {
const hasOwnProperty = Object.prototype.hasOwnProperty; const hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(objA, objB) { function shallowEqual(objA: object, objB: object) {
if (is(objA, objB)) { if (is(objA, objB)) {
return true; return true;
} }
...@@ -31,10 +31,10 @@ function shallowEqual(objA, objB) { ...@@ -31,10 +31,10 @@ function shallowEqual(objA, objB) {
} }
// Test for A's keys different from B. // Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) { for (const key of keysA) {
if ( if (
!hasOwnProperty.call(objB, keysA[i]) || !hasOwnProperty.call(objB, key) ||
!is(objA[keysA[i]], objB[keysA[i]]) !is(objA[key], objB[key])
) { ) {
return false; return false;
} }
...@@ -43,4 +43,4 @@ function shallowEqual(objA, objB) { ...@@ -43,4 +43,4 @@ function shallowEqual(objA, objB) {
return true; return true;
} }
export default shallowEqual; export default shallowEqual;
\ No newline at end of file
...@@ -2,13 +2,26 @@ import React from 'react'; ...@@ -2,13 +2,26 @@ import React from 'react';
import IdentificationContext from '../contexts/IdentificationContext'; import IdentificationContext from '../contexts/IdentificationContext';
import getDisplayName from './getDisplayName'; import getDisplayName from './getDisplayName';
export default function withIdentificationContextConsumer(Component) { export interface IIdentificationContextProps {
const NewComponent = props => ( identification: string;
eventEmitter: any;
keepAlive: boolean;
activated: boolean;
getLifecycle: () => number;
isExisted: () => boolean;
}
export interface IIdentificationContextComponentProps {
_identificationContextProps: IIdentificationContextProps;
}
export default function withIdentificationContextConsumer<P = any>(Component: React.ComponentType<IIdentificationContextComponentProps & P>) {
const NewComponent = (props: P) => (
<IdentificationContext.Consumer> <IdentificationContext.Consumer>
{contextProps => <Component _identificationContextProps={contextProps || {}} {...props} />} {(contextProps: IIdentificationContextProps) => <Component _identificationContextProps={contextProps || {}} {...props} />}
</IdentificationContext.Consumer> </IdentificationContext.Consumer>
); );
NewComponent.displayName = `withIdentificationContextConsumer(${getDisplayName(Component)})`; NewComponent.displayName = `withIdentificationContextConsumer(${getDisplayName(Component)})`;
return NewComponent; return NewComponent;
} }
\ No newline at end of file
import React from 'react'; import React from 'react';
import {IKeepAliveProviderImpl, IKeepAliveProviderProps} from '../components/Provider';
import KeepAliveContext from '../contexts/KeepAliveContext'; import KeepAliveContext from '../contexts/KeepAliveContext';
import getDisplayName from './getDisplayName'; import getDisplayName from './getDisplayName';
export default function withKeepAliveContextConsumer(Component) { type IKeepAliveContextProps = IKeepAliveProviderImpl & IKeepAliveProviderProps;
const NewComponent = (props, ref) => (
export interface IKeepAliveContextComponentProps {
_keepAliveContextProps: IKeepAliveContextProps;
}
export default function withKeepAliveContextConsumer<P = any>(Component: React.ComponentType<IKeepAliveContextComponentProps & P>) {
const NewComponent = (props: P) => (
<KeepAliveContext.Consumer> <KeepAliveContext.Consumer>
{contextProps => <Component _keepAliveContextProps={contextProps || {}} {...props} ref={ref} />} {(contextProps: IKeepAliveContextProps) => <Component _keepAliveContextProps={contextProps || {}} {...props} />}
</KeepAliveContext.Consumer> </KeepAliveContext.Consumer>
); );
NewComponent.displayName = `withKeepAliveContextConsumer(${getDisplayName(Component)})`; NewComponent.displayName = `withKeepAliveContextConsumer(${getDisplayName(Component)})`;
return React.forwardRef(NewComponent); return NewComponent;
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"target": "es5", "target": "es5",
"lib": ["es6", "dom"], "lib": ["es6", "dom"],
"allowJs": true, "declaration": true,
"sourceMap": false, "sourceMap": false,
"jsx": "react", "jsx": "react",
"moduleResolution": "node", "moduleResolution": "node",
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
"max-classes-per-file": false, "max-classes-per-file": false,
"no-unsafe-finally": false, "no-unsafe-finally": false,
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"space-before-function-paren": false,
"no-shadowed-variable": false,
"arrow-parens": [true, "ban-single-arg-parens"] "arrow-parens": [true, "ban-single-arg-parens"]
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment