携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
记录一次bug调试的漫长旅程。
bug体现,切换屏幕大小时,屏幕宽度到指定大小1000px时,会造成组件刷新,由于组件中有视频,导致播放中视频停止,并回到开头
故事的起源是一个老项目针对少数用户维护(老,是相对的,不是真的老,技术是新的技术,架构比较老而已)。由于升级新的架构,有点风险,所有保留了老架构的的部分业务。 在日常中,人们也希望使用一些新的东西,有些新的组件想在老架构里使用,有幸由我进行了适配(control c v),由于老的项目的数据结构和新的数据结构存在略微差异,所以,我采用了适配器的设计模式(哈哈哈),有个中间层,把老数据格式包裹一下,包装成新组件可识别的格式,因此写下了类似下面的代码,因为出现了bug,所有重新读了一下代码,发现问题颇多。
import { useEffect, useState } from "react";
import useMedia from "use-media";
import "./App.css";
// 这数据是来自网络请求,这里模拟一下
var myData = {
title: "题目",
content: "内容",
};
var mobileData = {
title: "题目1",
content: "内容1",
};
// 这个是老架构里为了适配移动端
function App() {
const isWide = useMedia({ minWidth: 1000 });
console.log("APP更新了", isWide);
let data = myData;
if (window.isMock && isWide) {
// 某种业务情况下,这里会执行,但是当前业务不会执行,移动端和pc使用同一份数据
data = mobileData;
}
return (
<div className="App">
<header className="App-header">{renderData(data)}</header>
</div>
);
}
export default App;
function Content(props) {
const { contentObj, source } = props;
const [renderSource,setRenderSource]=useState(source);
console.log("content更新了");
useEffect(()=>{
setRenderSource({
fruits:source.fruits.map(item=>{
item.id=Math.random();
return item;
})
})
},[source])
return (
<div>
<h1>{contentObj.title}</h1>
<p>{contentObj.content}</p>
{renderSource.fruits.map((item) => {
return (
<div key={item.type} id={item.id}>{item.type + "的价格为:" + item.price}</div>
);
})}
</div>
);
}
// renderData里面逻辑是我新加的,目的就是为了使用新组件Content,
// 由于Content需要的属性是固定格式,新的架构里这样用的,不可更改,所以我包裹了一下,
// 其实不考虑上下文时,个人感觉这个逻辑没问题,因为是无状态,组件属性不更新,组件不更新,
// 所以 ... 哈哈哈
function RenderData(props) {
console.log("renderData 更新了");
const { title, content } = props;
const contentObj = { title, content };
// 这个是个插件配置,业务需要,写死即可
const source = {
fruits: [
{
price: 22,
type: "苹果1",
},
{
price: 30,
type: "香蕉1",
},
{
price: 100,
type: "葡萄1",
},
],
};
return <Content contentObj={contentObj} source={source}></Content>;
}
function renderData(data) {
return <RenderData {...data}></RenderData>;
}
这段代码在线上效果就是这样
排查问题时排查了很久。因为忽略了useMedia这个hooks,惭愧啊
有几次定位到问题出在这里了,但是麻木的以为这里没执行,data 没变,react就不更新了,react 都替我处理了。我想react内心一定是气炸了,每次赋值都是新对象,并且你的render(data)本身就是动态的东西,每次都不一样。你不改,我就一直更新。 试着做如下更改,把动态渲染的东西用useMome缓存起来,react再更新时,假设data没变,这个值就一直复用之前的,假设data变了,这个函数重新执行一下得到一个新值,道理都懂,擦,就是不好好干
// 这个是老架构里为了适配移动端
function App() {
const isWide = useMedia({ minWidth: 1000 });
console.log("APP更新了", isWide);
let data = myData;
if (window.isMock && isWide) {
// 某种业务情况下,这里会执行,但是当前业务不会执行,移动端和pc使用同一份数据
data = mobileData;
}
+ const renderDataDom=useMemo(()=>renderData(data),[data])
return (
<div className="App">
- <header className="App-header">{renderData(data)}</header>
+ <header className="App-header">{renderDataDom}</header>
</div>
);
}
得,破案了,提前下班吧。 问题还没完啊,因为老的组件也是在这种条件下生存的,老组件为啥没问题呢?而且这个老架构已经运行很久,这个App,在架构里也是App级别的,不能随便改,容易带来风险,如果要改,就要充分测试(项目太大,鬼才愿意测试)。还是要在自己写的适配组件上入手,解决这个问题,回过头冷静看一下。自己的代码,确实有漏洞啊,
function RenderData(props) {
console.log("renderData 更新了");
const { title, content } = props;
const contentObj = useMemo(()=>{
return { title, content };
},[title,content]);
// const contentObj={ title, content }
const source =useMemo(()=> {
return {
fruits: [
{
price: 22,
type: "苹果1",
},
{
price: 30,
type: "香蕉1",
},
{
price: 100,
type: "葡萄1",
},
],
}
},[])
// const source = {
// fruits: [
// {
// price: 22,
// type: "苹果1",
// },
// {
// price: 30,
// type: "香蕉1",
// },
// {
// price: 100,
// type: "葡萄1",
// },
// ],
// };
return <Content contentObj={contentObj} source={source}></Content>;
}
这样一改,虽然没能阻止祖先级组件的雪崩渲染,但是避免了子组件的无脑渲染。所有引用类型,不用hooks包裹,都可能给子孙带来灾难, 所以爱护地球从你我做起