前言
各位看官,注意了。
- 这是篇技术文
- 小说手法只是陪衬,最重要的还是技术。目的是让技术能更深入人心。
- 这篇文原本的标题是(萌新入门之 —— 无痕上拉刷新组件(面试篇))
没办法,掘金都把作者逼成啥样了。为了能被更多人看到,无chi之级。
0. 我告诉他要做到 “ 没有痕迹
”
哎呀,我真的脸红了。因为今晚的米其林相亲会,我穿了件特别简单的粉色纱裙,emmm,我还画了点淡妆,5分钟,就5分钟结束了化妆的战斗。
哎。咱不能瞧不起相亲,更不能有抵触心理。万一我撞大运了不是?万一真的就是Mr.Right?
相亲,从见第一面就开始交锋了。 这人看起来真的年轻,是什么职业?
“小姐姐,堤拉米苏。”
“小姐姐,甜脆泡芙。”
“小姐姐,原浆椰汁。”
我裂开了。一口一个小姐姐管什么用,我饿得要死,晚餐是打算靠点心来约会吗?米其林餐厅什么最贵?这些分量少得可怜的点心好好了解一下?我心里在滴血,我狠狠拿着叉子戳我的泡芙。
我愤怒了。这要是AA,我得好好亏一顿钱包。他的手机来电话了,直接个按掉了。
“妹纸电话?”
“啊?不,不是。”
“手机给我看下。”
“啊!这…… 这不合适吧?”
“我漂亮嚒?”
“额。。 挺漂亮的。”
“我可爱嚒?”
“可……可爱。”
“有件事情你要做到无痕,懂吗?”
“什么?你说的什么意思?”
“晚上你睡觉嚒?”
“啊……”
“啊什么啊,你丫快把手机给我看看。”
一顿鸡飞狗跳。我拿着他手机,打开天猫App,精选首页。
“这个无痕浏览能做嚒?”
我看着眼前已经满脸懵逼的“难”孩纸,我一脸笑。我前几天面试,面试官比较残忍,问了我这样一个问题。
1. 我们来梳理一下什么叫无痕浏览
那天,面试官问我如何做一个能无限滚动,不仅要流畅还要让用户感觉不到内容延迟加载的组件
。Oh my god, 面试官大大,你就不能说简单点。例如:如何写一个高性能的上拉加载下拉刷新的List组件?
可是,我不会,我嘴瓢了。写个List我会,写个高性能的我也可以,但什么叫用户感觉不到内容延迟加载?听起来也不像是懒加载啊!我懵了,面试官看我可怜(长得可爱,眼神无辜。因为我闺蜜们都这么说。?
),又换了个方式问我。
Q: 加入页面要渲染10000条数据,而接口每次返回数据量有限,最多不超过30条。你咋办?
噢,我好像有点明白了。说白了就是用户看不到 加载更多
和 加载中
呗,然后就无限往下滑呗。
说得容易,也想明白了,但还是不会……
“喂,喂,小姐姐,你喜欢吃甜脆泡芙,正好天猫有卖呢。”
气氛开始尴尬。我的思绪也被拉了回来,相亲的时候开小差。没事,颜值即正义,可爱即正义,脸皮厚无敌。
“小哥哥,我看你年纪也不大,只要你告诉我如何做到无痕,我就答应你一个条件。怎样,前端小哥哥?”
唉。这年头,为了工作,到处出卖尊严。
你听,某前端小弟弟心碎的声音。
2. 瓶颈在哪
- 用户手势交互,因为用户可以无脑快速往下滑
- 可视区域内容更换频繁,不能使用骨架屏,至少不能用普通的骨架屏。从骨架屏到页面真实内容,只能存在首次加载。
- 接口请求问题。假如每次请求时间为
300ms
,那要拿完10000
条数据的时间位10000/30 * 300 = 100000ms
。 所以,如果100000ms
的时间必须拆分开,拆成足够细。 我就不信用户这能滑倒第10000
条数据。 - DOM渲染瓶颈。随着DOM数量越来越多,切记不要渲染重复或者不变的数据,不然一定会越来越卡顿。
先上一段伪代码:
import React, { Fragment, useRef, useState,useEffect } from 'react';
import styles from './style.module.less';
let scrollTop = -1;
let touchX = 0;
let touchY = 0;
let time = 0;
let type = '';
let pullDownDoneBacking = false; // 下拉刷新完成后,正在回弹至初始位置的状态
let init = true;
export default ((props) => {
const [translateY, setTranslateY] = useState(0);
const [pullUpHide, setPullUpHide] = useState(0);
const [pullUpStatus, setPullUpStatus] = useState(0);
const [pullDownStatus, setPullDownStatus] = useState(0);
const wrapperEl = useRef(null);
const innerEl = useRef(null);
useEffect(() => {
if (init) {
//解决ios和安卓的页面自带滚动回弹
wrapperEl.current.addEventListener('touchmove', touchMoveHandle, { passive: false });
init = false;
}
//内容发生变化时重新计算 pullUpHide
const pullUpHide = innerEl.clientHeight < wrapperEl.current.clientHeight;
setPullUpHide(pullUpHide);
})
const scrollHandle = (e) => {
};
const touchStartHandle = (e) => {
};
const touchMoveHandle = (e) => {
};
const touchEndHandle = (e) => {
};
/*
* 可在父组件中通过ref调用该方法,传入自定义异步函数组件会根据异步函数的状态去处理下拉的状态,自动下拉刷新
* */
//下拉刷新
const pullDownRefresh = async (customAsyncFn) => {
};
//上拉加载
const pullUpLoad = async () => {
};
const { pullDownRefresh, pullUpLoad, children, noMore, backTop, noMoreTip = '我到底了' } = props;
return (
<Fragment>
<div className={styles.wrapper}
ref={wrapperEl}
onScroll={scrollHandle}
onTouchStart={touchStartHandle}
onTouchEnd={touchEndHandle}
>
<div
style={{
transform: `translateY(${translateY}px)`,
transition: 'transform .3s ease'
}}
>
{
<div className={styles.pullDownTip}>
{pullDownStatus === 1 && '正在刷新'}
{pullDownStatus === 2 && '刷新成功'}
{pullDownStatus === 3 && '刷新失败'}
</div>
}
<div ref={innerEl}>
<Items />
</div>
<div>我真的到底了</div>
</div>
</div>
</Fragment>
)
}
)
复制代码
我们先来考虑下组件的模式
- 下拉刷新,动画效果还是要的。
(留下了不会制作gif图的泪水????)
- 核心,Items
// 我们要做到重复数据的不要重复render
const Item = React.memo((info)=>{
return <div>
{info.price}
....
</div>
});
const Items = ((list)=>{
return list.map((item,index)=> <Item info={item}/>)
})
复制代码
- 下拉刷新不要更改list长度。只修改第一页或者前1-3页的数据。
const pullDownRefresh = async () => {
// 之更新前1-3页的数据,不删除任何DOM
const freshData = await API();
list[0] = freshData[0];
list[1] = freshData[1];
list[2] = freshData[2];
setList(list);
}
复制代码
-
API返回第几页的数据,就覆盖数组对应index的数据。
-
调用接口API获取数据交于浏览器空闲时间去做,绝对不能阻塞用户操作。
requestIdleCallback
requestIdleCallback(myWork); // 如window.requestIdleCallback is undefined ,可以使用setTimeout模拟。
// 一个任务队列
let tasks = [
function task1() {
// 获取第1页数据
API.fetchData(1);
console.log('调用接口API1')
},
function task2() {
// 获取第2页数据
API.fetchData(2);
console.log('调用接口API2')
},
function task3() {
// 获取第3页数据
API.fetchData(3);
console.log('调用接口API3')
},
...,
function taskN(){
// 获取第N页数据
API.fetchData(N);
console.log('调用接口API(N)')
}
]
// deadline是requestIdleCallback返回的一个对象
function myWork(deadline) {
console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`)
// 查看当前帧的剩余时间是否大于0 && 是否还有剩余任务
if (deadline.timeRemaining() > 0 && tasks.length) {
// 在这里做一些事情
const task = tasks.shift()
task()
}
// 如果还有任务没有被执行,那就放到下一帧调度中去继续执行,类似递归
if (tasks.length) {
requestIdleCallback(myWork)
}
}
复制代码
所以,我们是不是只需要在把我们的 task
任务栈你交给 requestIdleCallback
来处理?
- 最后,优化你的渲染组件,图片记得做缓存。
END
“小姐姐,你是如何知道我是一枚前端的?”
“我不知道啊!”
“那你怎么问我这样的问题?”
“回答不上来就可以愉快的结束用餐了啊!”
气氛再度陷入极度尴尬中。
“噢,对了。谁知道你还真是前端,然后还真的辣么菜。”
“那…… 我们还能留个微信吗?”
“饭钱多少,AA吧。”
“小姐姐,一顿饭我还是请得起的……”
我用眼神挪了挪账单,3780.
所以哇,年少的时候容易爱一个人,但不要爱太满。如果你是男生,也请不要和我这样的“老人”谈恋爱。你问为啥?那我就矫情的回答一句。
年少还是不要遇见太惊艳的人了,误终生。