纯粹的反应解决方案规则非常简单,这不是秘密。我们只需要正确使用所有反应库存,例如USESTATE,使用效果,USEMEMO和USECALLBACK。关于该主题的文章,指南和示例很多。但是让我们回答以下问题。
您处理了多少个“纯”项目?
纯反应(Angular,nodejs等)项目在现实生活中看起来像是胡说八道。客户期望复杂的解决方案,包括支付系统,图形库,CRM集成,跟踪工具等不同的3-5派对的东西。显然,并非所有这些都友好地反应,因此我们应该在大多数情况下计算这些库的功能,并且尝试同时完善反应代码。
今天,我想告诉您一些跨React和D3的性能细节。
我之前撰写了几篇关于D3主题的文章,我想这对您来说也会很有趣。有以下相关文章。
,但让我们专注于当前主题。
D3.js是一个基于数据操纵文档的JavaScript库。 D3可以帮助您使用HTML,SVG和CSS将数据栩栩如生。 D3对Web标准的强调为您提供了现代浏览器的全部功能,而无需将自己与专有框架联系在一起,结合了强大的可视化组件和数据驱动的DOM操纵方法。
D3很棒!我喜欢这个美丽的图书馆。但是它过着自己的生活。这就是为什么我们需要记住这一事实时,当我们与D3 Vanilla JS合作时,在React中说
目标
目标是一个简单的D3线图,每秒都会使用动态指南运动实现。
我很确定了解什么是解释问题的最佳方法。这就是为什么我将从最糟糕的例子开始解决方案的原因。我还将解释为什么这个示例如此错误,之后,我将向您提出最佳实施方式。
最糟糕的解决方案
有以下组件代表一个line chart。
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const transform = "translate(50,50)";
export default function LineChart({ data, width, height, marker }) {
const svgRef = useRef();
const renderSvg = () => {
const chartWidth = width - 200;
const chartHeight = height - 200;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const xScale = d3.scaleLinear().domain([0, 100]).range([0, chartWidth]);
const yScale = d3.scaleLinear().domain([0, 200]).range([chartHeight, 0]);
const g = svg.append("g").attr("transform", transform);
g.append("g")
.attr("transform", "translate(0," + chartHeight + ")")
.call(d3.axisBottom(xScale));
g.append("g").call(d3.axisLeft(yScale));
svg
.append("g")
.selectAll("dot")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) {
return xScale(d[0]);
})
.attr("cy", function (d) {
return yScale(d[1]);
})
.attr("r", 3)
.attr("transform", transform)
.style("fill", "#CC0000");
const line = d3
.line()
.x(function (d) {
return xScale(d[0]);
})
.y(function (d) {
return yScale(d[1]);
})
.curve(d3.curveMonotoneX);
svg
.append("path")
.datum(data)
.attr("class", "line")
.attr("transform", transform)
.attr("d", line)
.style("fill", "none")
.style("stroke", "#CC0000")
.style("stroke-width", "2");
if (marker) {
svg
.append("svg:line")
.attr("transform", transform)
.attr("stroke", "#00ff00")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2)
.attr("x1", xScale(marker))
.attr("y1", 200)
.attr("x2", xScale(marker))
.attr("y2", 0);
}
};
useEffect(() => {
renderSvg();
}, [width, height, data, marker]);
if (!width || !height || !data) {
return <></>;
}
return <svg ref={svgRef} width={width} height={height} />;
}
此组件采用以下道具。
-
data
-图表数据作为X和y 的二维数组
-
width
-与图表 -
height
-图表的高度 -
marker
-x指南的轴
和一个相关的parent component。
import React, { useState, useEffect } from "react";
import LineChart from "./LineChart";
import "./style.css";
const data = [
[1, 1],
[12, 20],
[24, 36],
[32, 50],
[40, 70],
[50, 100],
[55, 106],
[65, 123],
[73, 130],
[78, 134],
[83, 136],
[89, 138],
[100, 140],
];
export default function App() {
const [marker, setMarker] = useState(10);
useEffect(() => {
const intervalId = setInterval(() => {
setMarker((prevMarker) => (prevMarker + 10 > 100 ? 10 : prevMarker + 10));
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<div id="root-container">
<LineChart data={data} width={500} height={400} marker={marker} />
</div>
);
}
每秒都有一个间隔刷新标记值,并将其作为聊天的道具传递。
您可以使用完整的示例here。
似乎没有任何预示的问题。我想修改上面的代码。目的是雄辩地展示该问题。
渲染数
首先,我将在public/index.html
中添加全局renders
和timeStart
变量
<script>
var renders = 0;
var timeStart = new Date().toISOString();
</script>
<div id="root"></div>
第二,我增加renders
每个LineChart
渲染。
export default function LineChart({ data, width, height, marker }) {
// no changes here ...
renders++;
useEffect(() => {
renderSvg();
}, [width, height, data, marker]);
if (!width || !height || !data) {
return <></>;
}
return <svg ref={svgRef} width={width} height={height} />;
}
最后,我以以下方式更改了父组件。
export default function App() {
const [marker, setMarker] = useState(10);
useEffect(() => {
const intervalId = setInterval(() => {
setMarker((prevMarker) => (prevMarker + 10 > 100 ? 10 : prevMarker + 10));
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
const currentTime = new Date().toISOString();
return (
<div id="root-container">
<div style={{ marginTop: 20, marginLeft: 20 }}>
renders: {renders}
<br />
start: {timeStart}
<br />
now: {currentTime}
</div>
<LineChart data={data} width={500} height={400} marker={marker} />
</div>
);
}
主要目标是显示三个指标:渲染数,开始时间和当前时间。
让我们运行modified example。
据我们所知,每个标记更改都会使LineChart
组件呈现。如果上面的结果无法说服您,我已经准备了以下实验。我离开了工作示例几分钟,喝咖啡。
当我返回时,我看到了以下内容。
每杯咖啡948
渲染!看起来很糟糕...
此外,一堆D3重量级操作涵盖了每个渲染!
最好的解决方案
是时候解决上述问题了。
首先,让我为您提供最后的LineChart version,并逐步解释发生了什么。
import React, {
useEffect,
forwardRef,
useImperativeHandle,
useRef,
} from "react";
import * as d3 from "d3";
const transform = "translate(50,50)";
const LineChart = forwardRef(({ data, width, height }, ref) => {
const svgRef = useRef();
let svg;
let xScale;
useImperativeHandle(ref, () => ({
setMarker: (value) => {
if (isNaN(value)) {
return;
}
svg.selectAll(".marker").remove();
svg
.append("svg:line")
.attr("transform", transform)
.attr("class", "marker")
.attr("stroke", "#00ff00")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2)
.attr("x1", xScale(value))
.attr("y1", 200)
.attr("x2", xScale(value))
.attr("y2", 0);
},
}));
const renderSvg = () => {
const chartWidth = width - 200;
const chartHeight = height - 200;
svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
xScale = d3.scaleLinear().domain([0, 100]).range([0, chartWidth]);
const yScale = d3.scaleLinear().domain([0, 200]).range([chartHeight, 0]);
const g = svg.append("g").attr("transform", transform);
g.append("g")
.attr("transform", "translate(0," + chartHeight + ")")
.call(d3.axisBottom(xScale));
g.append("g").call(d3.axisLeft(yScale));
svg
.append("g")
.selectAll("dot")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) {
return xScale(d[0]);
})
.attr("cy", function (d) {
return yScale(d[1]);
})
.attr("r", 3)
.attr("transform", transform)
.style("fill", "#CC0000");
const line = d3
.line()
.x(function (d) {
return xScale(d[0]);
})
.y(function (d) {
return yScale(d[1]);
})
.curve(d3.curveMonotoneX);
svg
.append("path")
.datum(data)
.attr("class", "line")
.attr("transform", transform)
.attr("d", line)
.style("fill", "none")
.style("stroke", "#CC0000")
.style("stroke-width", "2");
};
renders++;
useEffect(() => {
renderSvg();
}, [width, height, data]);
if (!width || !height || !data) {
return <></>;
}
return <svg ref={svgRef} width={width} height={height} />;
});
export default LineChart;
ForwardRef
现在,Linechart的父可以与相关的组件参考一起工作。
const LineChart = forwardRef(({ data, width, height }, ref) => {
使用刺激性
在一些采访中,我问我的受访者这个问题。我很惊讶,因为他们中的大多数无法回答。我认为,this hook与Usestate和Usestate和使用效应一样重要,因为它使您的代码更加灵活和性能。
这是裸露的代码。
useImperativeHandle(ref, () => ({
setMarker: (value) => {
if (isNaN(value)) {
return;
}
svg.selectAll(".marker").remove();
svg
.append("svg:line")
.attr("transform", transform)
.attr("class", "marker")
.attr("stroke", "#00ff00")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2)
.attr("x1", xScale(value))
.attr("y1", 200)
.attr("x2", xScale(value))
.attr("y2", 0);
},
}));
我将其从renderSvg
函数的末端移动。参见the previous example。
让我们专注于parent component。请在那里阅读评论。
import React, { useState, useEffect, useMemo, useRef } from 'react';
import LineChart from './LineChart';
import './style.css';
const data = [
// no changes
];
export default function App() {
const [marker, setMarker] = useState(10);
// Provide a reference for LineChart
const chartRef = useRef();
useEffect(() => {
// If the marker has been changed set it on LineChart directly, see useImperativeHandle
chartRef.current.setMarker(marker);
}, [marker]);
useEffect(() => {
const intervalId = setInterval(() => {
setMarker((prevMarker) => (prevMarker + 10 > 100 ? 10 : prevMarker + 10));
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
const currentTime = new Date().toISOString();
// There is a trick because we don't need to render LineChart after every App state variable change
// As you can see we don't pass the marker here.
const chart = useMemo(() => {
return <LineChart ref={chartRef} data={data} width={500} height={400} />;
}, [data]);
return (
<div id="root-container">
<div style={{ marginTop: 20, marginLeft: 20 }}>
renders: {renders}
<br />
start: {timeStart}
<br />
now: {currentTime}
</div>
{chart}
</div>
);
}
根据上述评论,有三个变化点。
- 提供LINECHART的参考
- 通过
useImperativeHandle.
的标记直接设置要注意以下事实:每个基于富有体验的呼叫都不会导致组件呈现。这是非常重要的! - 备忘录
LineChart
组件。我们不需要在每个App
变化中刷新它。
最后,最棘手的东西仍然存在。
注意力查看上面的代码后,您可以提出一个问题。
一方面,现在不应重新渲染组件。另一方面,指南从A点移动到B。当然,
点删除上一个准则chartRef.current.setMarker(marker);
直接呼叫使我们能够将指南设置为新位置。但是,哪种方法使我们可以从A?
在文章开始时,我的意思是我们需要计算D3库功能。在这种情况下,我们应该知道下面的两个事实。
- D3对象是有状态的,因此我们可以随时操作它们。在这种情况下,请查看以下代码。
let svg;
const renderSvg = () => {
// ...
svg = d3.select(svgRef.current);
//All futures results of modifications will be present persistently in SVG object
};
- 根据上述功能,我们可以每次都不用重新渲染而更改D3对象。此外,我们可以通过假CSS课程操纵不同的图表零件。
查看以下代码。
setMarker: (value) => {
if (isNaN(value)) {
return;
}
svg.selectAll('.marker').remove();
svg
.append('svg:line')
.attr('transform', transform)
.attr('class', 'marker')
.attr('stroke', '#00ff00')
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('stroke-width', 2)
.attr('x1', xScale(value))
.attr('y1', 200)
.attr('x2', xScale(value))
.attr('y2', 0);
},
}));
当我们添加指南时,我们会在其中添加一个特殊的假类:
.attr('class', 'marker')
,但是在我们通过
删除之前的指南之前
svg.selectAll('.marker').remove();
今天就D3的秘密而言,这就是
的秘密是时候运行最后一个示例了!您可以使用完整的最终示例here。
一直只有two renders
。看起来很酷!
就像React开发人员的耳朵一样!
愉快的编码!
ps:如果您想知道为什么两个渲染器,请阅读有关React Strict Mode的有关