React Native 中的多级界面缓存实现

Navigator 缓存界面

Navigator 是 React Native 提供的导航器组件用于界面场景间的切换,这里也是借助它来实现一个界面缓存,我们知道 Navigator 实现了一个路由栈,通常使用 push() 方法来装载界面,例如:假设当前界面为 V1,我们想打开一个 V2 界面:

navigator.push({
    view: 'V2'
});


这时候可以通过 Navigator getCurrentRoutes() 方法查看当前路由栈:

[
     {view: ‘V1’},
     {view: ‘V2’},
]

然后想返回 A1 时,再用 pop() 方法来卸载界面:

navigator.pop()

这就是最常用的方法,实现简单但弊端也很明显:就是再次打开 V2 时,必须再次 push 并装载 V2,而当 V2 是一个构建复杂且访问频率很高的界面时(如主栏目的列表),就导致大量性能消耗,界面不断 loading,并且由于每次都是重新装载,无法保存原有的访问状态(列表滑条位置等),用户体验差。

所以为了优化上述问题,我们希望这些界面只在第一次的访问时进行加载,之后都访问缓存。

翻了翻官方文档,发现 Navigator 有个 jumpTo() 方法,它的特点是可以跳转到已有场景而不卸载当前场景,似乎就是我们想要的效果,按照上面的例子场景,我们需要加一些条件判断,就是在加载界面时先判断下路由栈中是否已有该界面场景,如果有就调用 jumpTo(),没有就调用 push(),返回时也做相同的判断:

const currentRouteStack = navigator.getCurrentRoutes();
currentRouteStack.some((item, index) => {
    if (item.view === prevRouteView) {
        prevRoutePosition = index;
        return true;
    }
});
navigator.jumpTo(currentRouteStack[prevRoutePosition]);

这样就实现了已加载界面间的快速切换,好像很简单,但多操作一下就会发现问题,当有三个以上界面时,返回的顺序并不是我们预想的那样,原因是 jumpTo() 方法并不会改变 Navigator 路由栈的顺序,所以当点击返回的时候它会从当前路由所在位置执行出栈操作。

再看看文档,试了下几个方法,并不能实现我们期望的效果,所以这里就干脆自己维护一个简单的 迷你路由栈——给每个路由一个唯一的 id(代码示例中称为 vid),然后记录访问的顺序:

const currentRouteStack = navigator.getCurrentRoutes();
let historyPosition = 0;
let histroyRoute;
let stack = currentRouteStack[0].stack;

currentRouteStack.some((item, index) => {
    // 根据 vid 查找在路由栈中是否已经存在
    if (vid === item.vid) {
        historyPosition = index;
        histroyRoute = currentRouteStack[index];
        return true;
    }
});

if (vid !== stack[stack.length - 1]) {
    // 避免 vid 重复记录的情况
    stack.push(vid);
}

if (histroyRoute) {
    let nextRoute = currentRouteStack[historyPosition];
    navigator.jumpTo(nextRoute);
} else {
    navigator.push({
        vid: vid,
    });
}

当进行返回操作时就根据迷你路由栈中的顺序读取 Navigator 中的已有路由:

const currentRouteStack = navigator.getCurrentRoutes();
const unmountRoute = currentRouteStack[currentRouteStack.length - 1];
const stack = navigator.getCurrentRoutes()[0].stack;

stack.pop();
const prevRouteVid = stack[stack.length - 1];
let prevRoutePosition;

currentRouteStack.some((item, index) => {
    if (item.vid === prevRouteVid) {
        prevRoutePosition = index;
        return true;
    }
});

try {
    navigator.jumpTo(currentRouteStack[prevRoutePosition]);
} catch (err) {
    navigator.pop();
}

最后需要注意的就是迷你路由栈的储存了,其实在上面的代码中就已经可以看出,我这里是直接把它存储在 Navigator 路由栈的第一条记录中:

let stack = currentRouteStack[0].stack;

因为只要涉及路由操作都会传递 Navigator 对象,所以存在 Navigator 路由栈的第一条记录中就很好的解决了迷你路由栈跨界面读写的问题。

好了,经过一些改造我们就实现了一个能读取缓存的导航器,不管是界面切换还是返回,速度和体验都提升了很多。

二级界面缓存

上面的方法实现了一级界面的缓存,但在实际开发过程中经常会遇到包含选项卡(二级栏目)的界面:

rn1-4

那么实现这样的界面缓存也很简单,就是在已实现缓存的界面里直接使用 React Native 提供的 ViewPagerAndroid、 TabBarIOS 组件实现一个选项卡的功能,因为 ViewPagerAndroid、 TabBarIOS 组件会自动缓存界面状态,所以我们并不需要多特别处理,这里就不列举具体的实现代码了,可以直接参考官方文档。

唯一需要注意的是:不要一次性将所有选项卡中的内容都渲染出来,如果内容复杂就会造成界面顿卡或僵死,比较好的做法是只加载当前用户正在浏览界面的选项卡内容,然后借助 onPageScrollStateChanged、onPageSelected 等事件动态加载其它选项卡的内容。

三级界面缓存

通过对 Navigator 小改造和 ViewPagerAndroid、 TabBarIOS 组件的运用我们构建出了一个二级界面的缓存方案,这个方案可以满足绝大部分的需求,但也不排除会遇到三级界面的情况,对于这种界面的缓存方案是通过 Navigator 嵌套来实现:

Navigator -> SubNavigator -> ViewPagerAndroid

其核心就是在 SubNavigator 中也实现一套迷你路由栈功能,只是在处理返回操作时要优先处理 SubNavigator 的迷你路由栈,值得注意的是硬件返回事件会先传到 SubNavigator 而后传到 Navigator,这个过程类似冒泡,但无法中断或阻止,所以需要自定义一些标记来避免一个返回事件被重复处理。

关于性能

目前我们应用主要界面大概如上面的截图所示,包含四个主栏目,每个主栏目都有 4 个以上的二级栏目,每个二级栏目都是可以无限加载的列表。

在做 Web SPA 时面对这种大数据界面时都会着重关注性能问题,尽量减少 DOM,减少重绘,减少 GC 触发,而在 React Native 中做这种界面的性能又如何呢?这里做了下简单的测试——测试机型为 Nexus5,Android4.4,我让每个列表都加载 300 条数据,然后进行界面切换和列表滑动操作,UI的渲染帧数都保持在 40 帧以上,可见 Native 性能非常不错,并不需要做特别的优化工作,看来我们担心是多余的了。

3 comments

  1. 博主有没有 例子可以共享看一下呢。如果我的navigator对象带了参数,比如链接uri,一些其他的字段,这个时候是重写之前的navigator对象的参数还是再从新push呢?

发表评论

电子邮件地址不会被公开。 必填项已用*标注