如何仅使用单个d3.json().then Promise为多个柱状图加载数据
问题根因
获取不到抽离函数返回值的核心原因是d3.json()是异步Promise操作:如果在函数内用同步写法返回值,return执行时异步请求还未完成,拿到的结果永远是undefined。
要实现单文件多D3图表仅发起一次数据请求、同时符合DRY原则,按照下面的结构写即可。
实现方案
核心思路是:通用配置全局抽离复用、单个图表逻辑封装为独立作用域函数、全局仅发起1次数据请求,拿到格式化后的数据后统一传入所有图表函数完成渲染。
错误写法参考
这是绝大多数人第一次抽离逻辑会踩的坑:
// 错误:return只在then回调内部生效,外层函数拿不到异步结果 function loadData() { d3.json("data.json").then(res => { return res.map(d => ({...d, value: +d.value})) }) } const data = loadData() // 永远返回undefined
正确实现步骤
1. 抽离全局通用配置
把所有图表共用的边距、尺寸、通用比例尺生成逻辑抽离到全局,避免每个图表重复定义:
// 全局通用配置,所有图表可直接引用 const CHART_COMMON_CONFIG = { margin: {top: 20, right: 20, bottom: 30, left: 40}, baseWidth: 600, baseHeight: 400, // 通用比例尺生成方法,接收数据和尺寸动态返回实例 genXScale: (data, innerWidth) => d3.scaleBand() .domain(data.map(d => d.category)) .range([0, innerWidth]) .padding(0.2), genYScale: (data, innerHeight) => d3.scaleLinear() .domain([0, d3.max(data, d => d.value)]) .nice() .range([innerHeight, 0]) }
2. 封装独立图表渲染函数
每个图表单独封装为函数,所有函数统一接收挂载容器、格式化后的数据作为入参,函数内部变量为函数作用域,不会产生命名冲突:
/** * 渲染第一个柱状图 * @param {string} container 挂载选择器 * @param {Array} data 格式化后的通用数据 */ function renderSalesBar(container, data) { const {margin, baseWidth, baseHeight, genXScale, genYScale} = CHART_COMMON_CONFIG const innerWidth = baseWidth - margin.left - margin.right const innerHeight = baseHeight - margin.top - margin.bottom // 复用通用方法生成比例尺 const xScale = genXScale(data, innerWidth) const yScale = genYScale(data, innerHeight) // 正常编写绘图逻辑即可 const svg = d3.select(container) .append("svg") .attr("width", baseWidth) .attr("height", baseHeight) .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`) // 画坐标轴、柱子、交互逻辑省略... } /** * 渲染第二个柱状图 * @param {string} container 挂载选择器 * @param {Array} data 格式化后的通用数据 */ function renderProfitBar(container, data) { // 复用通用配置,仅编写该图表的差异化逻辑即可 const {margin, baseWidth, baseHeight, genXScale, genYScale} = CHART_COMMON_CONFIG const innerWidth = baseWidth - margin.left - margin.right const innerHeight = baseHeight - margin.top - margin.bottom const xScale = genXScale(data, innerWidth) const yScale = genYScale(data, innerHeight) // 后续绘图逻辑... }
3. 单次请求完成全量渲染
全局仅调用一次d3.json,在请求回调内统一完成数据格式化,再依次调用所有图表的渲染函数,传入同一份数据:
// 仅发起1次数据请求 d3.json("data.json") .then(rawData => { // 统一做一次数据格式化,所有图表共用结果 const formattedData = rawData.map(d => ({ category: d.type, value: +d.count, // 其他统一数据处理逻辑 })) // 依次渲染所有图表,传入同一份格式化后的数据 renderSalesBar("#chart-sales", formattedData) renderProfitBar("#chart-profit", formattedData) // 后续新增柱状图,只需要在这里加一行调用即可,不需要重复写请求 }) .catch(err => console.error("数据加载异常:", err))
抽离数据加载逻辑的写法
如果需要把数据加载、格式化逻辑也抽离为独立函数,直接返回整个Promise链即可,不要在Promise内部同步return:
// 抽离数据加载+格式化函数 function fetchChartData() { // 必须return整个Promise实例 return d3.json("data.json").then(rawData => { return rawData.map(d => ({ category: d.type, value: +d.count })) }) } // 调用时在then回调里拿处理好的数据,统一渲染 fetchChartData().then(formattedData => { renderSalesBar("#chart-sales", formattedData) renderProfitBar("#chart-profit", formattedData) })
注意事项
- 所有依赖数据的渲染逻辑必须写在Promise的
then回调内部,不要在回调外部读取数据变量,否则会拿到undefined - 不要在单个图表函数内部调用数据请求方法,否则会产生重复请求
- 每个图表函数内部的变量都是独立函数作用域,不需要担心命名冲突问题
内容的提问来源于stack exchange,提问作者Aloysia de Argenteuil
相关产品推荐
相关产品推荐

