runAfterInteractions(2022-09-24)
React Web 版
下面这段代码,是一段典型的React Web版代码,主组件App里,有一个子组件Ball,当App里的一些交互完成后,Ball才可以开始做一些动作。换句话说,Ball里的一些行为,是依赖于App里的一些交互完成的。一个典型的场景,就是App里要发送一些API拿数据,当数据都拿到了,Ball才能开始做一些动作。这里简化了API的抓取,用setTimeout来替代,本质上都是放在useEffect里的一些副作用。执行效果可以参看下面的动画截图,当三个时间延迟都完成后,App里的finished这个state就由false变成了true,于是Ball就做了它要做的动作,alert一个message。
import React, { useEffect, useState } from 'react';
import {
Alert,
StyleSheet,
View,
} from 'react-native';
const Ball = ({ onInteractionIsDone, finished }) => {
useEffect(() => {
if (finished) {
onInteractionIsDone()
}
}, [finished])
return <View style={[styles.ball]} />;
};
const App = () => {
const [finished1, setFinished1] = useState(false)
const [finished2, setFinished2] = useState(false)
const [finished3, setFinished3] = useState(false)
useEffect(() => {
setTimeout(() => {
setFinished1(true)
}, 2000);
}, []);
useEffect(() => {
setTimeout(() => {
setFinished2(true)
}, 1500);
}, []);
useEffect(() => {
setTimeout(() => {
setFinished3(true)
}, 3000);
}, []);
return (
<View style={styles.container}>
<Ball onInteractionIsDone={() => Alert.alert('done by normal way')}
finished={finished1 && finished2 && finished3} />
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
ball: {
width: 100,
height: 100,
backgroundColor: 'salmon',
borderRadius: 100,
},
});
export default App;
React Native 单组件版
在React Native中,有一个在Web中无法使用的API - InteractionManager。其中最常用的,是runAfterInteractions方法。这个方法的意思是说:当用户的交互(包括API返回,timeout,动画)完成后,才开始调用作为参数传递给该方法的那个方法。说白了,就是传递一个回调函数给runAfterInteractions,等待交互完成后,runAfterInteractions会自动调用那个回调函数。这样一来,我就不需要主动通过state来控制Ball这个子组件了,把控制权,交给了InteractionManager,它是知道什么时候交互完成的。
听起来挺高大上的,但是以我浅薄的认知来看,这不就是用一个全局的事件监控跨组件来同步状态么?和jQuery里发一个全局事件,或者Rxjs里多播一条消息,是不是本质上都一样呢?
遵循着这一类设计范式共有的特点,首先要告诉InteractionManager,哪儿些东西,是需要它来监控的,这里用
const handle = InteractionManager.createInteractionHandle()
来创建。
同理,还需要在不需要监控的时候,告诉InteractionManager,不用再监控了。这里用
InteractionManager.clearInteractionHandle(handle)
来销毁。
详细代码请参考下面。这段代码里,除了用InteractionManager替代state同步状态外,还封装了所有的交互到一个自定义hook里,在Ball里使用这个hook,就等于把交互这件事情和Ball本身建立了联系。
import React, { useEffect, useState } from 'react';
import {
Alert,
StyleSheet,
View,
InteractionManager
} from 'react-native';
const useCustomInteraction = () => {
useEffect(() => {
const handle1 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle1),
1500
);
return () => InteractionManager.clearInteractionHandle(handle1);
}, []);
useEffect(() => {
const handle2 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle2),
2000
);
return () => InteractionManager.clearInteractionHandle(handle2);
}, []);
useEffect(() => {
const handle3 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle3),
3000
);
return () => InteractionManager.clearInteractionHandle(handle3);
}, []);
};
const Ball = ({ onInteractionIsDone }) => {
useCustomInteraction()
useEffect(() => {
InteractionManager.runAfterInteractions(() => onInteractionIsDone());
}, [])
return <View style={[styles.ball]} />;
};
const App = () => {
return (
<View style={styles.container}>
<Ball onInteractionIsDone={() => Alert.alert('done by interaction way')}/>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
ball: {
width: 100,
height: 100,
backgroundColor: 'salmon',
borderRadius: 100,
},
});
export default App;
以下是演示效果
感兴趣的话,可以把第一个用来销毁handle1的clearInteractionHandle注释掉,那样就会发现,alert永远不会弹出来了,因为InteractionManager一直没有等到交互完成。
React Native 多组件版
下面这个例子,在上一个例子的基础上,又多加了2个Components - Square和Triangle,这两个Components也和Ball一样,必须等到交互完成后,才开始做一些动作。此时就看出来我们封装了useCustomInteraction和使用InteractionManager的好处,复用性大大提高了,而且还不用每一个组件设置一个state来同步状态。
import React, { useEffect, useState } from 'react';
import {
Alert,
StyleSheet,
View,
InteractionManager
} from 'react-native';
const useCustomInteraction = () => {
useEffect(() => {
const handle1 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle1),
1500
);
return () => InteractionManager.clearInteractionHandle(handle1);
}, []);
useEffect(() => {
const handle2 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle2),
2000
);
return () => InteractionManager.clearInteractionHandle(handle2);
}, []);
useEffect(() => {
const handle3 = InteractionManager.createInteractionHandle();
setTimeout(
() => InteractionManager.clearInteractionHandle(handle3),
3000
);
return () => InteractionManager.clearInteractionHandle(handle3);
}, []);
};
const Ball = ({ onInteractionIsDone }) => {
useCustomInteraction()
useEffect(() => {
InteractionManager.runAfterInteractions(() => onInteractionIsDone());
}, [])
return <View style={[styles.ball]} />;
};
const Square = ({ onInteractionIsDone }) => {
useCustomInteraction()
useEffect(() => {
InteractionManager.runAfterInteractions(() => onInteractionIsDone());
}, [])
return <View style={[styles.square]} />;
};
const Triangle = ({ onInteractionIsDone }) => {
useCustomInteraction()
useEffect(() => {
InteractionManager.runAfterInteractions(() => onInteractionIsDone());
}, [])
return <View style={[styles.triangle]} />;
};
const App = () => {
return (
<View style={styles.container}>
<Ball onInteractionIsDone={() => Alert.alert('Ball done')}/>
<Square onInteractionIsDone={() => Alert.alert('Square done')}/>
<Triangle onInteractionIsDone={() => Alert.alert('Triangle done')}/>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
ball: {
width: 100,
height: 100,
backgroundColor: 'red',
borderRadius: 100,
},
square: {
width: 100,
height: 100,
backgroundColor: 'green',
borderRadius: 0,
},
triangle: {
width: 0,
height: 0,
borderTopWidth: 50,
borderTopColor: 'transparent',
borderRightWidth: 100,
borderRightColor: 'transparent',
borderLeftWidth: 100,
borderLeftColor: 'transparent',
borderBottomWidth: 100,
borderBottomColor: 'blue'
},
});
export default App;