Рекурсивно добавить свойство к каждому узлу в древовидной структуре и вернуть измененное дерево - PullRequest
0 голосов
/ 24 мая 2018

У меня есть следующая структура данных для дерева комментариев в теме.Эта структура содержится внутри одного объекта.

comment {
    id: 1,
    text: 'foo',
    children: [
        comment {
            id: 2,
            text: 'foo-child',
            children: []
        },
        comment {
            id: 3,
            text: 'foo-child-2',
            children: []
        }
    ]
},
comment {
    id: 4,
    text: 'bar',
    children: []
}

Это обеспечивается внутренним API, в этом нет проблем.Я хочу рекурсивно исследовать это дерево, и для каждого узла (корневого или дочернего узла) я хочу выполнить вызов API и получить некоторые дополнительные данные для каждого отдельного узла , добавив некоторые дополнительные свойства,и вернуть все дерево с новыми ключами, добавленными к каждому узлу.

function expandVoteData(comments) {
    return new Promise((resolve, reject) => {
        let isAuth = Auth.isUserAuthenticated();
        // 'this' is the vote collection
        async.each(comments, (root, callback) => {
            // First get the vote data
            async.parallel({
                votedata: function(callback) {
                    axios.get('/api/comment/'+root.id+'/votes').then(votedata => {
                        callback(null, votedata.data);
                    });
                },
                uservote: function(callback) {
                    if(!isAuth) {
                        callback(null, undefined);
                    } else {
                        axios.get('/api/votes/comment/'+root.id+'/'+Auth.getToken(), { headers: Auth.getApiAuthHeader() }).then(uservote => {
                            callback(null, uservote.data); // Continue
                        });
                    }
                }
            }, function(error, data) {
                if(error) {
                    console.log('Error! ', error);
                } else {
                    // We got the uservote and the votedata for this root comment, now expand the object
                    root.canVote = isAuth;
                    root.totalVotes = data.votedata.total;
                    root.instance = 'comment';

                    if(data.uservote !== undefined) {
                        root.userVote = data.uservote;
                    }

                    if(root.children && root.children.length > 0) {
                        // Call this function again on this set of children
                        // How to "wrap up" this result into the current tree?
                        expandVoteData(root.children);
                    }
                    callback(); // Mark this iteration as complete
                }
            });
        }, () => {
            // Done iterating
            console.log(comments);
            resolve();
        });
    })
}

Что он делает: принять параметр 'comments' (который является целым объектом дерева), создать обещание, выполнить итерацию по каждомуконечный узел и выполнять соответствующие вызовы API в асинхронных запросах.Если у конечного узла есть дочерние узлы, повторите функцию с каждым дочерним узлом.

Теоретически это прекрасно работает в синхронном мире, но мне нужно получить новое дерево после того, как каждый узел былобрабатывается для дальнейшей обработки, как один объект, так же, как это было в качестве ввода.Фактически, я получаю несколько отпечатков консоли для каждого отдельного узла в дереве, что свидетельствует о том, что код работает так, как написано ... Однако я не хочу отдельных отпечатков, я хочу обернуть весь набор результатов в один объект,В идеале, функцию следует вызывать так:

expandVoteData(comments).then(expanded => {
    // yay!
});

Какие-нибудь советы о том, как это сделать?Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 25 мая 2018

запросов в последовательном

Ниже addExtra принимает входные данные comment и асинхронно добавляет дополнительные поля к комментарию, а все комментарии children рекурсивно.

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
   , children: await Promise.all (children.map (addExtra))
   , extra: await axios.get (...)
  })

Чтобы показать это, мы сначала представим поддельную базу данных.Мы можем запросить дополнительные поля комментария по комментарию id

const DB =
  { 1: { a: "one" }
  , 2: { a: "two", b: "dos" }
  , 4: [ "anything" ]
  }

const fetchExtra = async (id) =>
  DB [id]
  
fetchExtra (2)
  .then (console.log, console.error)
  
// { "a": "two"
// , "b": "dos"
// }

Теперь вместо axios.get мы используем fetchExtra.Мы можем видеть, что addExtra работает как задумано, учитывая первый комментарий в качестве ввода

const comments =
  [ /* your data */ ]

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await Promise.all (children.map (addExtra))
  , extra: await fetchExtra (comment.id)
  })

addExtra (comments [0])
  .then (console.log, console.error)

// { id: 1
// , text: "foo"
// , children:
//   [ {id: 2
//     , text: "foo-child"
//     , children:[]
//     , extra: { a: "two", b: "dos" } // <-- added field
//     }
//   , { id: 3
//     , text: "foo-child-2"
//     , children:[]
//     }
//   ]
// , extra: { a: "one" } // <-- added field
// }

Поскольку у вас есть массив комментариев, мы можем использовать map до addExtra для каждого

Promise.all (comments .map (addExtra))
  .then (console.log, console.error)

// [ { id: 1
//   , text: "foo"
//   , children:
//     [ {id: 2
//       , text: "foo-child"
//       , children:[]
//       , extra: { a: "two", b: "dos" } // <--
//       }
//     , { id: 3
//       , text: "foo-child-2"
//       , children:[]
//       }
//     ]
//   , extra: { a: "one" } // <--
//   }
// , { id: 4
//   , text: "bar"
//   , children:[]
//   , extra: [ 'anything' ] // <--
//   }
// ]

Использование Promise.all является бременем для пользователя, поэтому было бы неплохо иметь что-то вроде addExtraAll

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

addExtraAll (comments)
  .then (console.log, console.error)

// same output as above

рефакторинг и просветить

Вы заметили дублирование кода?Здравствуйте, взаимная рекурсия ...

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  <del>, children: await Promise.all (children .map (addExtra))</del>
  , children: await addExtraAll (children)
  , extra: await fetchExtra (comment.id)
  })

addExtra (singleComment) // => Promise

addExtraAll (manyComments) // => Promise

Проверьте результаты в вашем собственном браузере ниже

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await addExtraAll (children)
  , extra: await fetchExtra (comment.id)
  })

const DB =
  { 1: { a: "one" }
  , 2: { a: "two", b: "dos" }
  , 4: [ "anything" ]
  }

const fetchExtra = async (id) =>
  DB [id]
  
const comments =
  [ { id: 1
    , text: "foo"
    , children:
      [ {id: 2
        , text: "foo-child"
        , children:[]
        }
      , { id: 3
        , text: "foo-child-2"
        , children:[]
        }
      ]
    }
  , { id: 4
    , text: "bar"
    , children:[]
    }
  ]

addExtra (comments [0])
  .then (console.log, console.error)

// { id: 1
// , text: "foo"
// , children:
//   [ {id: 2
//     , text: "foo-child"
//     , children:[]
//     , extra: { a: "two", b: "dos" } // <-- added field
//     }
//   , { id: 3
//     , text: "foo-child-2"
//     , children:[]
//     }
//   ]
// , extra: { a: "one" } // <-- added field
// }

addExtraAll (comments)
  .then (console.log, console.error)

// [ { id: 1
//   , text: "foo"
//   , children:
//     [ {id: 2
//       , text: "foo-child"
//       , children:[]
//       , extra: { a: "two", b: "dos" } // <--
//       }
//     , { id: 3
//       , text: "foo-child-2"
//       , children:[]
//       }
//     ]
//   , extra: { a: "one" } // <--
//   }
// , { id: 4
//   , text: "bar"
//   , children:[]
//   , extra: [ 'anything' ] // <--
//   }
// ]

добавить несколько полей

Выше addExtra просто и добавляет только одно поле extra к вашему комментарию.Мы можем добавить любое количество полей

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await addExtraAll (children)
  , extra: await axios.get (...)
  , other: await axios.get (...)
  , more: await axios.get (...)
  })

результаты слияния

Вместо добавления полей в comment, также возможно объединить извлеченные данные. Однако вы должны принять некоторые меры предосторожности здесь ...

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...await fetchExtra (comment.id)
   , ...comment
   , children: await addExtraAll (children)
  })

addExtra (comments [0])
  .then (console.log, console.error)

// { 
// , a: 1 // <-- extra fields are merged in with the comment
// , id: 1
// , text: "foo"
// , children: [ ... ]
// }

Обратите внимание на порядок вызовов выше.Поскольку сначала мы вызываем ...await, для выбранных данных невозможно заменить поля в вашем комментарии.Например, если fetchExtra(1) вернул { a: 1, id: null }, мы все равно получили бы комментарий { id: 1 ... }.Если вы хотите, чтобы добавленные поля могли перезаписывать существующие поля в вашем комментарии, вы можете изменить порядок

И, наконец, вы можете сделать несколько слияний, если хотите

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...await fetchExtra (comment.id)
   , ...await fetchMore (comment.id)
   , ...await fetchOther (comment.id)
   , ...comment
   , children: await addExtraAll (children)
  })

запросов параллельно

Один из недостатков вышеописанного подхода заключается в том, что запросы на дополнительные поля выполняются в последовательном порядке.

Было бы неплохоесли бы мы могли указать функцию, которая принимает наш комментарий в качестве входных данных и возвращает объект полей, которые мы хотим добавить.На этот раз мы пропускаем ключевые слова await, чтобы наша функция могла автоматически распараллеливать подзапросы для нас

addFieldsAll
  ( c => ({ extra: fetchExtra (c.id), other: fetchOther (c.id) })
  , comments
  )
  .then (console.log, console.error)

// [ { id: 1
//   , children: [ ... ] // <-- fields added to children recursively
//   , extra:  ... // <-- added extra field
//   , other: ... // <-- added other field
//   }
// , ...
// ]

Вот один из способов реализации addFieldsAll.Также обратите внимание, что из-за упорядочения аргументов Object.assign для дескриптора возможно *1094* указать поля, которые будут перезаписывать поля во входном комментарии - например, c => ({ id: regenerateId (c.id), ... }).Как описано выше, это поведение может быть изменено путем изменения порядка аргументов по желанию

const addFieldsAll = async (desc = () => {} , comments = []) =>
  Promise.all (comments .map (c => addFields (desc, c)))

const addFields = async (desc = () => {}, { children = [], ...comment}) =>
  Object.assign
    ( comment
    , { children: await addFieldsAll (desc, children) }
    , ... await Promise.all
        ( Object .entries (desc (comment))
            .map (([ field, p ]) =>
              p.then (res => ({ [field]: res })))
        )
    )
0 голосов
/ 24 мая 2018

Будет проще, если вы разделите код на несколько функций и используете классный синтаксис async / await.Furst определяет асинхронную функцию, которая обновляет один узел, не заботясь о дочерних элементах:

async function updateNode(node) {
 const [votedata, uservote] = await Promise.all([
   axios.get('/api/comment/'+root.id+'/votes'),
    axios.get('/api/votes/comment/'+root.id+'/'+Auth.getToken(), { headers: Auth.getApiAuthHeader() })
 ]);

 node.totalVotes = votedata.total;
 node.instance = 'comment';

 if(uservote)
   node.userVote = uservote;
}

Чтобы рекурсивно обновить все узлы, достаточно просто:

async function updateNodeRecursively(node) {
  await updateNode(node);
  await Promise.all(node.children.map(updateNodeRecursively));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...