【问题标题】:JavaScript, looping, and functional approachJavaScript、循环和函数式方法
【发布时间】:2021-03-01 06:28:57
【问题描述】:

从服务器返回的数据结构

[
  {
    id: 1,
    type: "Pickup",
    items: [
      { 
        id: 1, 
        description: "Item 1"
      }
    ]
  },
  {
    id: 2,
    type: "Drop",
    items: [
      { 
        id: 0, 
        description: "Item 0"
      }
    ]
  },
  {
    id: 3,
    type: "Drop",
    items: [
      { 
        id: 1, 
        description: "Item 1"
      },
      { 
        id: 2, 
        description: "Item 2"
      }
    ]
  },
  {
    id: 0,
    type: "Pickup",
    items: [
      { 
        id: 0, 
        description: "Item 0"
      },
      { 
        id: 2, 
        description: "Item 2"
      }
    ]
  }
];
  • 每个元素代表一个事件。
  • 每个事件只是一个拾取或丢弃。
  • 每个事件可以有一个或多个项目。

初始状态

在初始加载时,循环来自服务器的响应,并为每个事件、每个项目添加一个名为 isSelected 的额外属性,并将其默认设置为 false。 -- 完成。

这个 isSelected 属性仅用于 UI 目的,它告诉用户哪些事件和/或项目已/已被选择。

// shove the response coming from the server here and add extra property called isSelected and set it to default value (false)
const initialState = {
  events: []
}

moveEvent 方法:

const moveEvent = ({ events }, selectedEventId) => {
  // de-dupe selected items 
  const selectedItemIds = {};
  
  // grab and find the selected event by id
  let foundSelectedEvent = events.find(event => event.id === selectedEventId);
  
  // update the found event and all its items' isSelected property to true
  foundSelectedEvent = {
    ...foundSelectedEvent, 
    isSelected: true,
    items: foundSelectedEvent.items.map(item => {
      item = { ...item, isSelected: true };
      // Keep track of the selected items to update the other events.
      selectedItemIds[item.id] = item.id;
      return item;
    }) 
  };

  events = events.map(event => {
    // update events array to have the found selected event
    if(event.id === foundSelectedEvent.id) {
      return foundSelectedEvent;
    }
    
    // Loop over the rest of the non selected events
    event.items = event.items.map(item => {
      // if the same item exists in the selected event's items, then set item's isSelected to true.
      const foundItem = selectedItemIds[item.id];
      // foundItem is the id of an item, so 0 is valid
      if(foundItem >= 0) {
        return { ...item, isSelected: true };
      }
      return item;  
    });
    
    const itemCount = event.items.length;
    const selectedItemCount = event.items.filter(item => item.isSelected).length;
    
    // If all items in the event are set to isSelected true, then mark the event to isSelected true as well.
    if(itemCount === selectedItemCount) {
      event = { ...event, isSelected: true };
    }

    return event;
  });

  return { events }
}

就我个人而言,我不喜欢我实现 moveEvent 方法的方式,即使我使用的是 find、filter 和 map,它似乎也是一种命令式的方法。 这个 moveEvent 方法所做的就是翻转 isSelected 标志。

  1. 有更好的解决方案吗?
  2. 有没有办法减少循环次数?也许事件应该是一个对象,甚至是它的项目。至少,查找事件的查找速度会很快,而且我最初不必使用 Array.find。但是,我仍然必须循环遍历其他非选定事件的属性,或者使用 Object.entries 和/或 Object.values 来回转换它们。
  3. 还有更多功能性方法吗?递归可以解决这个问题吗?

使用和结果

// found the event with id 0
const newState = moveEvent(initialState, 0);

// Expected results
[
  {
    id: 1,
    type: 'Pickup',
    isSelected: false,
    items: [ { id: 1, isSelected: false, description: 'Item 1' } ]
  }
  {
    id: 2,
    type: 'Drop',
    // becasue all items' isSelected properties are set to true (even though it is just one), then set this event's isSelected to true
    isSelected: true,
    // set this to true because event id 0 has the same item (id 1)
    items: [ { id: 0, isSelected: true, description: 'Item 0' } ]
  }
  {
    id: 3,
    type: 'Drop',
    // since all items' isSelected properties are not set to true, then this should remain false.
    isSelected: false,
    items: [
      { id: 1, isSelected: false, description: 'Item 1' },
      // set this to true because event id 0 has the same item (id 2)
      { id: 2, isSelected: true, description: 'Item 2' }
   ]
  }
  {
    id: 0,
    type: 'Pickup',
    // set isSelected to true because the selected event id is 0
    isSelected: true,
    items: [
      // since this belongs to the selected event id of 0, then set all items' isSelected to true
      { id: 0, isSelected: true, description: 'Item 0' },
      { id: 2, isSelected: true, description: 'Item 2' }
    ]
  }
]

【问题讨论】:

  • 这可能更适合codereview.stackexchange.com
  • 谢谢。我已经在其他网站上发布了。
  • 我认为您的代码过于复杂是因为您将 selectedEventIdisSelected inside 存储在事件对象中。我们很难在没有看到您的 UI 代码的情况下推荐解决方案,但我建议将 selectedEventId 移动到状态的根目录(与 events: [] 处于同一级别)。这将大大减少状态排列的数量,因此减少错误的机会。然后可以通过查找 selectedEventId 来增强组件以显示正确的 UI。始终尝试使状态尽可能简单:)
  • 是的,UI 太复杂了,无法解释,我在这里过度简化了。我不明白为什么我需要在 state 中添加 selectedEventId。 moveEvent 只是一个接收状态和选定事件 id 并返回新状态的函数。
  • 用户可以选择的不仅仅是一个事件。因此,每次用户选择特定事件并更新状态时,都会调用 moveEvent 方法。

标签: javascript arrays object data-structures functional-programming


【解决方案1】:

当前解决方案的问题之一是数据重复。您基本上是在尝试使不同项目之间的数据保持同步。不要更改具有相同 id 的所有项目,而是使用更接近您在合理数据库中找到的方法来确保没有重复的项目。

让我们首先对数据进行归一化:

const response = [...]; // data returned by the server

let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
  data.eventIds.push(id);
  data.events[id] = event;
  event.items = [];
  for (const {id, ...item} of items) {
    event.items.push(id);
    data.items[id] = item;
  }
}

这应该会导致:

const data {
  eventIds: [1, 2, 3, 0], // original order
  events: {
    0: { type: "Pickup", items: [0, 2] },
    1: { type: "Pickup", items: [1]    },
    2: { type: "Drop",   items: [0]    },
    3: { type: "Drop",   items: [1, 2] },
  },
  items: {
    0: { description: "Item 0" },
    1: { description: "Item 1" },
    2: { description: "Item 2" },
  },
};

接下来要意识到的是,事件的isSelected 属性是根据其项目的isSelected 属性计算的。存储它意味着更多的数据重复。而是通过函数计算它。

const response = [{id:1,type:"Pickup",items:[{id:1,description:"Item 1"}]},{id:2,type:"Drop",items:[{id:0,description:"Item 0"}]},{id:3,type:"Drop",items:[{id:1,description:"Item 1"},{id:2,description:"Item 2"}]},{id:0,type:"Pickup",items:[{id:0,description:"Item 0"},{id:2,description:"Item 2"}]}];

// normalize incoming data
let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
  data.eventIds.push(id);
  data.events[id] = event;
  event.items = [];
  for (const {id, ...item} of items) {
    event.items.push(id);
    data.items[id] = item;
    item.isSelected = false;
  }
}

// don't copy isSelected into the event, calculate it with a function 
const isEventSelected = ({events, items}, eventId) => {
  return events[eventId].items.every(id => items[id].isSelected);
};

// log initial data
console.log(data);
for (const id of data.eventIds) {
  console.log(`event ${id} selected?`, isEventSelected(data, id));
}

// moveEvent implementation with the normalized structure
const moveEvent = (data, eventId) => {
  let { events, items } = data;
  for (const id of events[eventId].items) {
    items = {...items, [id]: {...items[id], isSelected: true}};
  }
  return { ...data, items };
};

data = moveEvent(data, 0);

// log after data applying `moveEvent(data, 0)`
console.log(data);
for (const id of data.eventIds) {
  console.log(`event ${id} selected? `, isEventSelected(data, id));
}

// optional: convert structure back (if you still need it)
const convert = (data) => {
  const { eventIds, events, items } = data;
  return eventIds.map(id => ({
    id,
    ...events[id],
    isSelected: isEventSelected(data, id),
    items: events[id].items.map(id => ({id, ...items[id]}))
  }));
};

console.log(convert(data));
Check browser console, for better ouput readability.

我不确定这个答案是否能解决你的整个问题,但我希望你能从中得到一些有用的信息。

【讨论】:

  • 我很感激你的努力,它给了我一些可以继续发展的想法。但是,我更喜欢另一个答案(在 codereview.stackexchange 上)。它看起来更简单,并且不需要跟踪查找。顺便说一句,你在 moveEvent 方法中有一个错误。您正在使用 const itemsLookup,然后稍后重新分配它,这会引发错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-15
  • 1970-01-01
  • 2018-10-25
  • 2020-06-29
  • 2017-06-02
  • 1970-01-01
  • 2015-06-16
相关资源
最近更新 更多