RN中優化堆棧下棧頂模式時頁面更新問題(A>B>C>D>C)

自從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、方法與屬性

  1. runAfterInteractions(task) 靜態方法,在用戶交互和動畫結束以後執行任務
  2. createInteractionHandle() 靜態方法,創建一個句柄(處理器),通知管理器,某個動畫或者交互開始了
  3. clearInteractionHandle(handler:Handle) 靜態方法,進行清除句柄,通知管理器,某個動畫或者交互結束了。
  4. setDeadline(deadline:number) 靜態方法, 設置延遲時間,該會調用setTimeout方法掛起並且阻塞所有沒有完成的任務,然後在eventLoopRunningTime到設定的延遲時間後,然後執行setImmediate方法進行批量執行任務
  5. Events:CallExpression
  6. 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}`
    });
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章