React&D3的零成本方式。
#javascript #网络开发人员 #react #d3

纯粹的反应解决方案规则非常简单,这不是秘密。我们只需要正确使用所有反应库存,例如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>
  );
}

每秒都有一个间隔刷新标记值,并将其作为聊天的道具传递。

first result

您可以使用完整的示例here

似乎没有任何预示的问题。我想修改上面的代码。目的是雄辩地展示该问题。

渲染数

首先,我将在public/index.html
中添加全局renderstimeStart变量

<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

second result

据我们所知,每个标记更改都会使LineChart组件呈现。如果上面的结果无法说服您,我已经准备了以下实验。我离开了工作示例几分钟,喝咖啡。

coffee

当我返回时,我看到了以下内容。

renders

每杯咖啡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>
  );
}

根据上述评论,有三个变化点。

  1. 提供LINECHART的参考
  2. 通过useImperativeHandle.的标记直接设置要注意以下事实:每个基于富有体验的呼叫都不会导致组件呈现。这是非常重要的!
  3. 备忘录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

final result

一直只有two renders。看起来很酷!

就像React开发人员的耳朵一样!

愉快的编码!

ps:如果您想知道为什么两个渲染器,请阅读有关React Strict Mode的有关