Commit a6c65439 authored by Shen Chang's avatar Shen Chang

refactor(KeepAlive): Asynchronous mount component

parent 1ed0cf2f
......@@ -247,9 +247,14 @@ ReactDOM.render(
**Note**: If you want to use the **lifecycle**, wrap the components in a `bindLifecycle` high-level component.
### `bindLifecycle`
Components that pass this high-level component wrap will have the **correct** lifecycle, entering the component must trigger the `componentDidMount` lifecycle, and leaving will also trigger the `componentWillUnmount` lifecycle. Refer to this [example] (https://codesandbox.io/s/q1xprn1qq) for a better understanding, pay attention to open the console.
Components that pass this high-level component wrap will have the **correct** lifecycle, and we have added two additional lifecycles, `componentDidActivate` and `componentWillUnactivate`.
The old version of ~~`componentDidActivate`~~ and ~~`componentWillUnactivate`~~ has been deleted, this is a component that is inevitably unaccustomed to the new life cycle, and was originally written with reference to Vue, but it is not entirely suitable for React.
Lifecycle after adding:
![Lifecycle after adding](https://github.com/Sam618/react-keep-alive/raw/master/assets/lifecycle.png)
`componentDidActivate` will be executed once after the initial mount or from the unactivated state to the active state. although we see `componentDidActivate` after `componentDidUpdate` in the `Updating` phase, this does not mean `componentDidActivate` Always triggered.
At the same time, only one of the lifecycles of `componentWillUnactivate` and `componentWillUnmount` is triggered. `componentWillUnactivate` is executed when caching is required; `componentWillUnmount` is executed without caching.
#### Example
```JavaScript
......
......@@ -250,9 +250,14 @@ ReactDOM.render(
**注意**:如果要使用 **生命周期**,请将组件包装在 `bindLifecycle` 高阶组件中。
### `bindLifecycle`
这个高阶组件包装的组件将具有 **正确的** 的生命周期,进入组件必定会触发 `componentDidMount` 生命周期,离开也必定会触发 `componentWillUnmount` 生命周期。参考这个 [例子](https://codesandbox.io/s/q1xprn1qq) 能够更好的理解,注意打开控制台
这个高阶组件包装的组件将具有 **正确的** 的生命周期,并且我们添加了两个额外的生命周期 `componentDidActivate``componentWillUnactivate`
旧版本新增的 ~~`componentDidActivate`~~ 和 ~~`componentWillUnactivate`~~ 生命周期已经删除,这是考虑到了新增生命周期难免会不习惯,并且原来是参照 Vue 来写的组件,但其实并不完全适合 React。
添加新的生命周期之后:
![Lifecycle after adding](https://github.com/Sam618/react-keep-alive/raw/master/assets/lifecycle.png)
`componentDidActivate` 将在组件刚挂载或从未激活状态变为激活状态时执行。虽然我们在 `Updating` 阶段的 `componentDidUpdate` 之后能够看到 `componentDidActivate`,但这并不意味着 `componentDidActivate` 总是被触发。
同时只能触发 `componentWillUnactivate``componentWillUnmount` 生命周期的其中之一。当需要缓存时执行 `componentWillUnactivate`,而 `componentWillUnmount` 在禁用缓存的情况下执行。
#### 例子
```JavaScript
......
import React, {useState, useEffect} from 'react';
import React, {useState, useEffect, useRef} from 'react';
import {useKeepAliveEffect} from '../../../es';
import B from './B';
function Test() {
const [index, setIndex] = useState(0);
const divRef = useRef();
useKeepAliveEffect(() => {
console.log('activated', index);
console.log(divRef.current.offsetWidth);
const i = 0;
return () => {
......@@ -13,7 +16,8 @@ function Test() {
});
return (
<div>
<div>This is a.</div>
<div ref={divRef}>This is a.</div>
<B />
<button onClick={() => setIndex(index + 1)}>click me({index})</button>
</div>
);
......
......@@ -8,6 +8,7 @@ class B extends React.Component {
}
componentDidMount() {
console.log(this.ref.offsetWidth);
console.log('B componentDidMount');
}
......@@ -20,6 +21,7 @@ class B extends React.Component {
}
componentDidUpdate() {
console.log(this.ref.offsetWidth);
console.log('B componentDidUpdate');
}
......@@ -34,7 +36,7 @@ class B extends React.Component {
render() {
console.log('B render');
return (
<div>This is b.</div>
<div ref={ref => this.ref = ref}>This is b.</div>
);
}
}
......
......@@ -3,11 +3,20 @@ import {bindLifecycle} from '../../../es';
@bindLifecycle
class C extends React.Component {
state = {
value: false,
};
componentWillMount() {
console.log('C componentWillMount');
}
componentDidMount() {
setTimeout(() => {
this.setState({
value: true,
});
}, 1000);
console.log('C componentDidMount');
}
......@@ -34,7 +43,9 @@ class C extends React.Component {
render() {
console.log('C render');
return (
<div>This is c.</div>
<div>
{this.state.value ? <div>This is c.</div> : null}
</div>
);
}
}
......
{
"name": "react-keep-alive",
"version": "0.4.3",
"version": "1.2.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -7413,6 +7413,11 @@
}
}
},
"react-deep-force-update": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-2.1.3.tgz",
"integrity": "sha512-lqD4eHKVuB65RyO/hGbEST53E2/GPbcIPcFYyeW/p4vNngtH4G7jnKGlU6u1OqrFo0uNfIvwuBOg98IbLHlNEA=="
},
"react-dom": {
"version": "16.8.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.5.tgz",
......
{
"name": "react-keep-alive",
"version": "1.2.2",
"version": "2.0.0.alpha.0",
"description": "Package will allow components to maintain their status, to avoid repeated re-rendering.",
"author": "Shen Chang",
"homepage": "https://github.com/Sam618/react-keep-alive",
......@@ -37,7 +37,8 @@
"dependencies": {
"@types/js-md5": "^0.4.2",
"hoist-non-react-statics": "^3.3.0",
"js-md5": "^0.7.3"
"js-md5": "^0.7.3",
"react-deep-force-update": "^2.1.3"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
......
import * as React from 'react';
import deepForceUpdate from 'react-deep-force-update';
interface IProps {
setMounted: any;
getMounted: any;
correctionPosition: any;
}
interface IState {
component: any;
}
export default class AsyncComponent extends React.Component<IProps, IState> {
public state = {
component: null,
};
public componentDidMount() {
const {children} = this.props;
Promise.resolve().then(() => this.setState({component: children}));
}
public componentDidUpdate() {
this.props.correctionPosition();
}
// Delayed update
// In order to be able to get real DOM data
public shouldComponentUpdate() {
if (!this.state.component) {
// If it is already mounted asynchronously, you don't need to do it again when you update it.
this.props.setMounted(false);
return true;
}
Promise.resolve().then(() => {
if (this.props.getMounted()) {
this.props.setMounted(false);
deepForceUpdate(this);
}
});
return false;
}
public render() {
return this.state.component;
}
}
import React from 'react';
import AsyncComponent from './AsyncComponent';
import {START_MOUNTING_DOM, LIFECYCLE} from './Provider';
import keepAlive, {COMMAND} from '../utils/keepAliveDecorator';
import changePositionByComment from '../utils/changePositionByComment';
......@@ -16,6 +17,18 @@ interface IKeepAliveInnerProps extends IKeepAliveProps {
class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
private bindUnmount: (() => void) | null = null;
private bindUnactivate: (() => void) | null = null;
private unmounted = false;
private mounted = false;
private ref: null | Element = null;
private refNextSibling: null | ChildNode = null;
private childNodes: ChildNode[] = [];
public componentDidMount() {
const {
_container,
......@@ -24,6 +37,7 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
notNeedActivate,
identification,
eventEmitter,
keepAlive,
} = _container;
notNeedActivate();
const cb = () => {
......@@ -32,6 +46,13 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
if (keepAlive) {
this.componentDidActivate();
}
}
public componentDidActivate() {
// tslint-disable
}
public componentDidUpdate() {
......@@ -46,13 +67,23 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
notNeedActivate();
this.mount();
this.listen();
this.unmounted = false;
this.componentDidActivate();
}
}
public componentWillUnactivate() {
this.unmount();
this.unlisten();
}
public componentWillUnmount() {
if (!this.unmounted) {
this.unmounted = true;
this.unmount();
this.unlisten();
}
}
private mount() {
const {
......@@ -63,11 +94,33 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
setLifecycle,
},
} = this.props;
this.setMounted(true);
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
private correctionPosition = () => {
if (this.ref && this.ref.parentNode && this.ref.nextSibling) {
const childNodes = this.ref.childNodes as any;
this.refNextSibling = this.ref.nextSibling;
for (const child of childNodes) {
this.childNodes.push(child);
this.ref.parentNode.insertBefore(child, this.ref.nextSibling);
}
this.ref.parentNode.removeChild(this.ref);
}
}
private retreatPosition = () => {
if (this.ref && this.refNextSibling && this.refNextSibling.parentNode) {
for (const child of this.childNodes) {
this.ref.appendChild(child);
}
this.refNextSibling.parentNode.insertBefore(this.ref, this.refNextSibling);
}
}
private unmount() {
const {
_container: {
......@@ -79,6 +132,7 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
this.retreatPosition();
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
......@@ -96,6 +150,10 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
[identification, COMMAND.CURRENT_UNMOUNT],
this.bindUnmount = this.componentWillUnmount.bind(this),
);
eventEmitter.on(
[identification, COMMAND.CURRENT_UNACTIVATE],
this.bindUnactivate = this.componentWillUnactivate.bind(this),
);
}
private unlisten() {
......@@ -106,10 +164,31 @@ class KeepAlive extends React.PureComponent<IKeepAliveInnerProps> {
},
} = this.props;
eventEmitter.off([identification, COMMAND.CURRENT_UNMOUNT], this.bindUnmount);
eventEmitter.off([identification, COMMAND.CURRENT_UNACTIVATE], this.bindUnactivate);
}
private setMounted = (value: boolean) => {
this.mounted = value;
}
private getMounted = () => {
return this.mounted;
}
public render() {
return this.props.children;
// The purpose of this div is to not report an error when moving the DOM,
// so you need to remove this div later.
return (
<div ref={ref => this.ref = ref}>
<AsyncComponent
setMounted={this.setMounted}
getMounted={this.getMounted}
correctionPosition={this.correctionPosition}
>
{this.props.children}
</AsyncComponent>
</div>
);
}
}
......
......@@ -12,7 +12,10 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
shouldComponentUpdate = noop,
} = WrappedComponent.prototype;
WrappedComponent.prototype.componentDidMount = function () {
......@@ -22,12 +25,25 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
_container: {
identification,
eventEmitter,
activated,
},
keepAlive,
} = this.props;
this._unmounted = false;
// Determine whether to execute the componentDidActivate life cycle of the current component based on the activation state of the KeepAlive components
if (!activated && keepAlive !== false) {
componentDidActivate.call(this);
}
eventEmitter.on(
[identification, COMMAND.ACTIVATE],
this._bindActivate = () => this._needActivate = true,
true,
);
eventEmitter.on(
[identification, COMMAND.MOUNT],
this._bindMount = () => this._needActivate = true,
[identification, COMMAND.UNACTIVATE],
this._bindUnactivate = () => {
componentWillUnactivate.call(this);
this._unmounted = false;
},
true,
);
eventEmitter.on(
......@@ -39,13 +55,20 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
true,
);
};
WrappedComponent.prototype.componentDidUpdate = function (...args: any) {
// In order to be able to re-update after transferring the DOM, we need to block the first update.
WrappedComponent.prototype.shouldComponentUpdate = function (...args: any) {
if (this._needActivate) {
return false;
}
return shouldComponentUpdate.call(this, ...args) || true;
};
WrappedComponent.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
if (this._needActivate) {
this._needActivate = false;
this._unmounted = false;
componentDidMount.call(this);
} else {
componentDidUpdate.apply(this, args);
componentDidActivate.call(this);
}
};
WrappedComponent.prototype.componentWillUnmount = function () {
......@@ -59,8 +82,12 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
},
} = this.props;
eventEmitter.off(
[identification, COMMAND.MOUNT],
this._bindMount,
[identification, COMMAND.ACTIVATE],
this._bindActivate,
);
eventEmitter.off(
[identification, COMMAND.UNACTIVATE],
this._bindUnactivate,
);
eventEmitter.off(
[identification, COMMAND.UNMOUNT],
......@@ -74,6 +101,8 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
_identificationContextProps: {
identification,
eventEmitter,
activated,
keepAlive,
},
...wrapperProps
}) => {
......@@ -88,6 +117,8 @@ export default function bindLifecycle<P = any>(Component: React.ComponentClass<P
_container={{
identification,
eventEmitter,
activated,
keepAlive,
}}
/>
);
......
......@@ -3,12 +3,7 @@ import {prefix} from './createUniqueIdentification';
export default function createStoreElement(): HTMLElement {
const keepAliveDOM = document.createElement('div');
keepAliveDOM.dataset.type = prefix;
keepAliveDOM.style.visibility = 'hidden';
keepAliveDOM.style.opacity = '0';
keepAliveDOM.style.position = 'absolute';
keepAliveDOM.style.top = '0';
keepAliveDOM.style.left = '0';
keepAliveDOM.style.zIndex = '-1';
keepAliveDOM.style.display = 'none';
document.body.appendChild(keepAliveDOM);
return keepAliveDOM;
}
......@@ -12,9 +12,11 @@ import shallowEqual from './shallowEqual';
import getKeepAlive from './getKeepAlive';
export enum COMMAND {
UNACTIVATE = 'unactivate',
UNMOUNT = 'unmount',
MOUNT = 'mount',
ACTIVATE = 'activate',
CURRENT_UNMOUNT = 'current_unmount',
CURRENT_UNACTIVATE = 'current_unactivate',
}
interface IListenUpperKeepAliveContainerProps extends IIdentificationContextConsumerComponentProps, IKeepAliveContextConsumerComponentProps {
......@@ -29,6 +31,7 @@ interface IListenUpperKeepAliveContainerState {
interface ITriggerLifecycleContainerProps extends IKeepAliveContextConsumerComponentProps {
propKey: string;
keepAlive: boolean;
getCombinedKeepAlive: () => boolean;
}
/**
......@@ -43,6 +46,8 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
class TriggerLifecycleContainer extends React.PureComponent<ITriggerLifecycleContainerProps> {
private identification: string;
private activated = false;
private ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
......@@ -63,6 +68,9 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
}
public componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {
keepAlive,
_keepAliveContextProps: {
......@@ -71,19 +79,40 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.MOUNT]);
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
public componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
public componentWillUnmount() {
const {
getCombinedKeepAlive,
_keepAliveContextProps: {
eventEmitter,
isExisted,
},
} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
eventEmitter.emit([this.identification, COMMAND.CURRENT_UNMOUNT]);
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()) {
eventEmitter.emit([this.identification, COMMAND.CURRENT_UNACTIVATE]);
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
private activate = () => {
this.activated = true;
}
private reactivate = () => {
this.ifStillActivate = false;
......@@ -110,6 +139,7 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
const {
propKey,
keepAlive,
getCombinedKeepAlive,
_keepAliveContextProps: {
isExisted,
storeElement,
......@@ -137,6 +167,7 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
identification,
......@@ -156,6 +187,7 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
identification,
eventEmitter,
keepAlive,
activated,
getLifecycle,
isExisted,
}}
......@@ -169,6 +201,7 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
eventEmitter,
identification,
storeElement,
keepAlive,
cache,
}}
/>
......@@ -180,11 +213,15 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
}
class ListenUpperKeepAliveContainer extends React.Component<IListenUpperKeepAliveContainerProps, IListenUpperKeepAliveContainerState> {
private combinedKeepAlive: boolean;
public state = {
activated: true,
};
private mount: () => void;
private activate: () => void;
private unactivate: () => void;
private unmount: () => void;
......@@ -228,8 +265,13 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
return;
}
eventEmitter.on(
[identification, COMMAND.MOUNT],
this.mount = () => this.setState({activated: true}),
[identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
eventEmitter.on(
......@@ -244,10 +286,15 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
if (!identification) {
return;
}
eventEmitter.off([identification, COMMAND.MOUNT], this.mount);
eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate);
eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate);
eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
}
private getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
}
public render() {
const {
_identificationContextProps: {
......@@ -274,7 +321,7 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
return null;
}
const newKeepAlive = getKeepAlive(propKey, include, exclude, disabled);
const combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: identification
? upperKeepAlive && newKeepAlive
......@@ -285,7 +332,8 @@ export default function keepAliveDecorator<P = any>(Component: React.ComponentTy
{...wrapperProps}
key={propKey}
propKey={propKey}
keepAlive={combinedKeepAlive}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
......
import React, {useEffect, useContext, useRef} from 'react';
import React, {useEffect, useContext, useRef, useState} from 'react';
import {warn} from './debug';
import {COMMAND} from './keepAliveDecorator';
import IdentificationContext, {IIdentificationContextProps} from '../contexts/IdentificationContext';
......@@ -14,18 +14,32 @@ export default function useKeepAliveEffect(effect: React.EffectCallback) {
const effectRef: React.MutableRefObject<React.EffectCallback> = useRef(effect);
effectRef.current = effect;
useEffect(() => {
let bindMount: (() => void) | null = null;
let bindActivate: (() => void) | null = null;
let bindUnactivate: (() => void) | null = null;
let bindUnmount: (() => void) | null = null;
let effectResult = effectRef.current();
let unmounted = false;
eventEmitter.on(
[identification, COMMAND.MOUNT],
bindMount = () => {
[identification, COMMAND.ACTIVATE],
bindActivate = () => {
// Delayed update
Promise.resolve().then(() => {
effectResult = effectRef.current();
});
unmounted = false;
},
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
bindUnactivate = () => {
if (effectResult) {
effectResult();
unmounted = true;
}
},
true,
);
eventEmitter.on(
[identification, COMMAND.UNMOUNT],
bindUnmount = () => {
......@@ -41,8 +55,12 @@ export default function useKeepAliveEffect(effect: React.EffectCallback) {
effectResult();
}
eventEmitter.off(
[identification, COMMAND.MOUNT],
bindMount,
[identification, COMMAND.ACTIVATE],
bindActivate,
);
eventEmitter.off(
[identification, COMMAND.UNACTIVATE],
bindUnactivate,
);
eventEmitter.off(
[identification, COMMAND.UNMOUNT],
......
......@@ -6,6 +6,7 @@
"rules": {
"max-line-length": false,
"no-console": false,
"no-debugger": false,
"quotemark": [true, "single", "jsx-double"],
"trailing-comma": [true, {"multiline": "ignore", "singleline": "never"}],
"ordered-imports": false,
......
declare module "react-deep-force-update" {
export default function deepForceUpdate(instance: any, shouldUpdate?: Function, onUpdate?: Function): void;
}
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