threejs 加载两个场景_前端在搜索场景可以做哪些优化

这片文章想聊一聊搜索场景,前端可以做哪些事情,保证用户体验和性能。

先来明确一下搜索场景,本文想讨论的是一类场景,它包含以下特征,本文先以搜索场景命名。

1. 获取远程数据进行展示

2. 响应用户输入,重新获取数据

3. 用户输入可连续

典型的包括搜索词推荐,搜索结果,表格数据过滤加分页展示,远程搜索下拉框等,算是一类比较常见的场景。

在这些场景下除了正常的loading和展示,还可以做哪些事情?

随用户输入自动重新获取数据

这个可以减少用户的操作次数,不需要再去点一次查询按钮。但是对于有中间态的情况,如输入框,或者多选框,或者有多个表单过滤条件,会有多余的请求,可能会对用户造成一定的心里负担(笔者就会有浪费请求的心里负担)。

对于简单情况,比如翻页,单选,应该没有疑问,在用户输入后就主动重新获取数据。

对于输入框,响应回车键来进行数据的获取也很便捷,因为用户本来就在打字。有时候回车被占用,像百度搜索的场景,回车要用来搜索,所以在用户输入的时候自动变更搜索推荐词。如果要在输入框中响应输入自动获取数据,通过处理compositionstart和compositionend,在这两个事件之间不触发数据的获取,可以减少一些请求。

防抖debounce

这个是比较容易想到的优化,但也值得去思考,我们是不是真的需要它。先来看一下debounce的模型,Regular是常规的事件触发,下边两个是对应的debounce和throttle处理后的实际事件触发

d3e102f556c0e648166abba5561edf95.png

可以看到的是debounce确实能少触发一些操作,但是得等用户输入停止后等一小段时间再触发事件,这意味着你好不容易用fastclick挤出来的时间又被debounce还回去了。幸运的是这个时间是我们自己设置的,那debounce的时间到底设置为多少合适,如果设置大了用户等很久,如果设置小了,那防抖的实际效果不明显。所以这个值得综合考虑用户输入的速度、用户的抖动输入的概率到底有多大、一次查询的成本(时间和后端压力)来决定,然后尽可能的小来保证用户体验。

回到输入框的场景,是用户输入比较快的场景。事实上这个场景如果回车键没被占用,打完字然后回车一下不香么,为什么要等你一会你再自动获取数据。可能有一定的用户教育成本,但是笔者认为这样更为方便。

还有用户的点击操作,点击的操作一般不会太快,得考虑用户快速多次的点击概率有多大,收益能不能覆盖debounce带来的负面影响。这种时候的debounce很鸡肋。

不过debounce还是有用武之地的,在频繁的系统调用时,比如你监听了一些值的变化,去自动获取数据,但是你连续改变了多个过滤参数的值,就自动触发了多次请求。这种情况下debounce的时间可以设置的很小,用户很难察觉。

使用的话用lodash的debounce实现就好啦。

丢弃旧的数据

这是比较容易忽略的一个步骤,但其实是一个必不可少的操作。因为用户的操作可能连续,比如连续的翻页,即使你用了防抖,也会发出多个请求,如果前面的请求因为网络拥堵,比后面的请求后完成,会导致用户的输入和展示不符合的错误,如下图。

9fd0e99f9d0c8776e00e38df6a69ebe3.png

用户的输入最后是B,会先展示B的内容,最后可能屏幕一闪显示了A的内容。这中情况在测试或者使用中比较难复现,所以也容易被忽略。那我们要做的其实是在A请求完成的时候,发现用户输入了B,就不去更新数据。实际上怎么去操作呢?简单的,用一个全局变量去存下来最新的请求次数,每次请求后自增,每次请求结束,用当前请求次数对比全局变量中最新的请求次数,如果不一样就不更新数据。类似场景的处理是相似的,可以封装一个高阶函数去处理,大致代码如下:

function refresh(fn, _thisArg) {
  let lastRes = null
  return function (...args) {
    const res = fn.apply(_thisArg || this, args)
    if (lastRes) {
      lastRes.reject(new Error('data has expired'))
    }
    if (Object.prototype.toString.call(res) === '[Object Promise]' || (res && res.then && res.catch)) {
      return Promise.race([
        res, 
        new Promise((resolve, reject) => {
          lastRes = {resolve, reject}
        })
      ])
    } else {
      lastRes = null
      return res
    }
  }
}

在上述代码没有使用自增数来判断,自增数主要的功能是让当前请求知道自己是不是最新的。通过使用Promise.race和缓存lastRes能达到同样的效果。refresh的意思是让你的数据保持新鲜,命名真让人头秃。

来测试一下吧

const table = {data: 0}
const getData = refresh(
    (msTimer, data) => new Promise(resolve => setTimeout(resolve, msTimer, data))
)
const setTableData = async (msTimer, data) => {
    try {
        table.data = await getData(msTimer, data)
    } catch (e) {
        console.log(e.message)
    }
}
setTableData(200, 1)
setTableData(100, 2)
setTimeout(() => {
    console.log(table.data)
}, 300)
// log data has expired
// log 2

如果不用refresh,最后table.data的结果会是1。使用的时候记得单独处理data has expired的报错。getData的职能应该单一,单纯的根据参数获取远程数据并返回,不要进行其他副作用操作。

缓存

对于数据实时性要求不高的,可以考虑把数据缓存在内存中,节省请求,让用户更快看到之前看过的数据。实际操作的时候要考虑以下三点。

1、根据参数生成key来应用缓存的请求的Promise,就算请求没完成,也不会重复发请求,而是等上一个请求结束。

2、然后对于失败的请求考虑清除缓存。

3、如果你对数据有实时性要求,但是也想用缓存,也没问题,那就给缓存的数据加一个过期时间吧。

满足以上条件的高阶函数实现如下

function cache(fn, _thisArg, {resetReject = true, keyFn, msMaxAge = 0} = {}) {
  let res = {}
  let isCached = {}
  return function (...args) {
    const key = typeof keyFn === 'function' ? keyFn(...args) : keyFn
    if (isCached[key] && (msMaxAge <=0 || Date.now() - isCached[key] <= msMaxAge )) {
      return res[key]
    }
    res[key] = fn.apply(_thisArg || this, args)
    isCached[key] = Date.now()
    if (resetReject && (Object.prototype.toString.call(res[key]) === '[Object Promise]' || (res[key] && res[key].then && res[key].catch))) {
      res[key].catch(e => {
        delete res[key]
        delete isCached[key]
      })
    }
    return res[key]
  }
}

这样这个函数可以满足大部分的缓存需求,包括复杂计算。如果你的异步函数是callback的形式,那先用promisfy转一下。

当cache和refresh想同时用的时候,先做cache操作,再做refresh操作,refresh(cache(fn)),这样能保证数据没被使用的情况,也能进行缓存。

预加载

有了缓存的情况下,我们就可以考虑预加载了。预加载多少和预加载什么可以根据业务的用户使用习惯来定,但是应该保证当前请求完成后再去做预加载。比如在用户翻页的时候,当前页加载完成后去预加载下一页的内容。也可以打点看一下缓存的命中率来调整预加载的策略。

记录用户的输入方便下一次访问,或根据用户输入生成分享链接

适用于一些有很多很复杂过滤条件的报表,便于分享报表,或者通过浏览器前进和后退查看之前的选择。首次进入页面后,去读取url上的信息,进行查询。当用户再输入的时候,通过pushState不刷新页面更改url,这样用户可以通过浏览器的前进后退,也很方便的复制url分享给别人,这种情况常见的处理有分页的页码和分页大小,其他过滤条件也适用。这样稍微复杂一些,如果只为分享,可以提供按钮生成链接并复制到粘贴板。

以上讨论是搜索场景下一些可以做的事情,不适用所有场景,欢迎大家拍砖讨论。部分提到的源代码可以在guji项目中找到,是我自己在根据之前业务中提炼出来的工具函数,满足我当时的业务需求,如有问题,欢迎提issue呀。

祝大家撸码愉快

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值