自從React Native出世,雖然官方一直儘可能的優化其性能,爲了能讓其媲美原生App的速度,但是現實感覺有點不盡人意。接下來介紹下實踐中遇到的一些性能問題以及優化方案。
一、StackNavigator頁面切換動畫優化
場景:在navigation還沒出來時,導航路由使用NavigatorIOS來實現,頁面切換是很流暢的,但是用了StackNavigator navigation發現頁面切換會使JS線程出現嚴重的掉幀(卡頓現象);
原因:
NavigatorIOS的切換動畫是跑在UI主線程上,而不是JS線程上的,所以不受JS線程上的掉幀影響;但是NavigatorIOS是但平臺專用,在我們業務開發中是不能使用的;而react-navigation是跨平臺的,也是官方推薦使用的;
問題:
react-navigation在從A頁面push到B頁面時,如果B頁面render存在大量可操作的視圖結構,或者componentDidMount方法裏面做了耗時的操作,會發現丟幀的現在,也就是頁面頁面會卡頓2s左右,從用戶體驗上來講是非常不友好的。
react-navigation動畫是由JS線程控制的
想象一下“從右邊推入”這個場景的切換:每一幀中,新的場景從右向左移動,從屏幕右邊緣開始(不妨認爲是320單位寬的的x軸偏移),最終移動到x軸偏移爲0的屏幕位置。切換過程中的每一幀,JavaScript線程都需要發送一個新的x軸偏移量給主線程。
由於JS就是一單線程的,他會優先處理自頁面的dom構建,以及componentDidMount中的事件處理,這是JS線程就無法處理轉場動畫了,知道處理完自頁面釋放之後在來處理動畫,這就導致切換頁面出現卡頓現象
解決方案:
使用API InteractionManager,它的作用就是可以使本來JS的一些操作在動畫完成之後執行,這樣就可確保動畫的流程性
InteractionManager簡介
1、可以提升用戶體驗和交互效果的模塊InteractionMnager(交互管理器)
2、基本內容
使用InteractionManager可以讓一些耗時的任務在交互操作或者動畫完成之後進行執行,這樣使用可以保證我們的JavaScript的動畫效果可以平滑流暢的執行。可以大大提升用戶體驗。
在應用開發中我們可以如下進行執行任務
InteractionManager.runAfterInteractions(() => {
//執行耗時的同步任務
});
該模塊和其他相關的調度方法對比:
- requestAnimationFrame():執行控制動畫效果的代碼
- setImmediate/setTimeout():設置延遲執行任務的時間,該可能會影響到正在執行的動畫
- runAfterInteractions():延遲執行任務,該不會影響到正在執行的動畫效果
觸摸系統中的單點或者多點觸控都是交互動作,耗時任務會在這些觸摸交互動作執行完成之後或者取消以後回調runAfterInteractions()方法進行執行。
runAfterInteractions任務也可以接收一個普通的回調函數或者一個帶有gen方法並且返回一個Promise的PromiseTask對象。如果參數是PromiseTask對象,那麼任務是異步執行的,也會阻塞。該會等着當前任務執行完畢以後才能執行下一個任務。
默認情況下,隊列任務會一次性在setImmediate方法中批量執行。如果你通過setDeadline方法設置一個時間值,那麼任務會在延遲該設定值時間進行執行。這時候會調用setTimeout方法進行掛起任務並且阻塞其他任務的執行。這樣可以給觸摸交互等操作留出時間更好的相應用戶操作。
3、方法與屬性
- runAfterInteractions(task) 靜態方法,在用戶交互和動畫結束以後執行任務
- createInteractionHandle() 靜態方法,創建一個句柄(處理器),通知管理器,某個動畫或者交互開始了
- clearInteractionHandle(handler:Handle) 靜態方法,進行清除句柄,通知管理器,某個動畫或者交互結束了。
- setDeadline(deadline:number) 靜態方法, 設置延遲時間,該會調用setTimeout方法掛起並且阻塞所有沒有完成的任務,然後在eventLoopRunningTime到設定的延遲時間後,然後執行setImmediate方法進行批量執行任務
- Events:CallExpression
- addListener:CallExpression
4、具體使用
開發中關於卡頓的解決:
先在render中渲染一個空視圖,等轉場動畫完成以後,再去渲染世紀的視圖
代碼如下:
//頁面引入InteractionManager
import {
AppRegistry,
StyleSheet,
Text,
ScrollView,
Image,
Alert,
TouchableOpacity,
InteractionManager,
View,
ImageBackground,
DeviceEventEmitte
} from "react-native";
class Redeem extends Component {
constructor(props) {
super(props);
this.state = {
renderPlaceholderOnly: true//交互管理器延時控制標識
};
}
/**
* 頁面初始化先渲染空視圖減少頁面轉場時間
*/
_renderPlaceholderView = () => {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}>
<Image
style={{ width: 150, height: 150, backgroundColor: "#f5f5f5" }}
source={require("./../../img/task/loading.gif")}
/>
</View>
);
};
render() {
//首先渲染空視圖
if (this.state.renderPlaceholderOnly) {
return this._renderPlaceholderView();
}
return(
//頁面dom
...
)
}
componentDidMount() {
//轉場動畫完成之後改變標記值,重新渲染dom
InteractionManager.runAfterInteractions(() => {
this.setState({ renderPlaceholderOnly: false });
});
}
}
二、事件監聽
場景:在頁面跳轉是如從A頁面路由跳轉到B頁面,需要在B頁面更新了數據,返回A頁面是需要更新A頁面數據
問題:
由於react路由是基於hash來實現的,從A頁面路由跳轉到B頁面,會在URL上拼接頁面對應的hash值,從B頁面在返回A頁面時,去除URL上的hash值,相當於改變了URL,此時A頁面會重新加載,從而達到刷新頁面的效果;
但是react-navigation的路由跳轉是基於棧來實現的,A路由跳轉B,就是一個壓棧的過程,就是把頁面B push到棧頂,從B返回到A是就是pop彈棧,此時對於頁面A來說是無感知的,就是把B頁面從棧中pop出去,此時對於開發者來說就無法感知B頁面的返回操作了;
解決方案:
使用RN神器發送和接收事件DeviceEventEmitter
具體使用如下:
頁面A:
//首 先導入DeviceEventEmitter組件
import {
StyleSheet,
Text,
ScrollView,
Image,
Alert,
TouchableOpacity,
TouchableWithoutFeedback,
TouchableHighlight,
InteractionManager,
View,
FlatList,
SectionList,
DeviceEventEmitter
} from "react-native";
//要在A頁面寫一個接收消息的方法
//在didmount方法中寫好監聽/接收方法,當有消息發送時,這就會接收到,並執行相應方法
componentDidMount() {
//收到監聽 事件監聽
//從新手任務頁面或者積分兌換頁面返回需要監聽 然後更新任務首頁總積分
//原因:新手任務頁面和積分兌換頁面都會設計積分的變更,但是返回任務首頁時不會刷新頁面,
//所以需要監聽返回狀態,在主動調用接口
this.listener = DeviceEventEmitter.addListener("left", (e) => {
//e就是從頁面B發送過來的數據
if (e) {
this.getAllTntegral();
}
Toast.info(e);
});
}
}
//當然,別忘了要卸載
componentWillUnmount() {
// 移除監聽
this.listener.remove();
}
頁面B
//首 先導入DeviceEventEmitter組件
import {
StyleSheet,
Text,
ScrollView,
Image,
Alert,
TouchableOpacity,
View,
ImageBackground,
InteractionManager,
FlatList,
DeviceEventEmitter
} from "react-native";
//可根據自己的業務場景來實現消息發送的時機,本人用法是在B頁面返回時,給A頁面發消息通知
class NewTask extends Component {
static navigationOptions = ({ navigation }) => ({
headerTitle: `${navigation.state.params.title}`,
headerLeft: (
<TouchableOpacity
style={styles.action}
onPress={() => {
//給監聽事件賦值
DeviceEventEmitter.emit("left", "back");
navigation.goBack();
}}>
<View style={styles.headerLeft} />
</TouchableOpacity>
)
// headerBackTitle: `${navigation.state.params.title}`
});
}