Pull to refresh

Comments 17

Позволю себе совет. Перепишите на typescript. Ну и еще один, попросите опытных товарищей сделать codereview вашему коду. Я пробежался по диагонали, есть явные места где стоит чуть переделать, например
//....
 return nstd.Structure.sort((a, b) => {
      if (a.lkey > b.lkey) return 1
      if (a.lkey < b.lkey) return -1
      return 0
    })

// better replace with 
 return nstd.Structure.sort((a, b) => a.lkey - b.lkey )

уверен что будут и более веские замечания.
Большое спасибо, обязательно займусь

Это какой-то ужас, а не код...


Вот примеры того, как код писать нельзя:


  _nestedsets.removeItem = function (itemId) {
     // ...
     for (var i = 0; i < nstd.Structure.length; i++) {
        if (nstd.Structure[i].itemId === itemId) {
          var childs = nstd.getChilds(nstd.Structure[i]._id)

// ...

 _nestedsets.getChilds = function (nodeId, depth) {
    var nstd = this

    var parentNode = nstd.getNode(nodeId)

  _nestedsets.getNode = function (nodeId, asCopy) {
    var nstd = this

    var selectedNode = nstd.Structure.filter(n => n._id === nodeId)

Тут в массиве нод по индексу берётся _id, после чего сразу же в том же самом массиве выполняется по этому _id поиск...




          var childs = nstd.getChilds(nstd.Structure[i]._id)
          for (var j = 0; j < childs.length; j++) {
            nstd.removeNode(childs[j]._id)
          }

removeNode удаляет всё поддерево нод. Зачем тут вообще цикл и почему нельзя было написать просто nstd.removeNode(nstd.Structure[i]._id)?


На пустом месте оквадратили линейный алгоритм!




    var childs = nstd.getNodes().filter(n => {
      return n.lkey >= parentNode.lkey && n.rkey <= parentNode.rkey && nodeId !== n._id && (depth === undefined ? true : n.depth <= (parentNode.depth + depth))
    })

    // ...

      childs.sort((a, b) => {
        if (a.lkey > b.lkey) return 1
        if (a.lkey < b.lkey) return -1
        return 0
      })

getNodes() возвращает уже упорядоченный массив. Зачем его сортировать ещё раз по тому же самому свойству? С каких пор фильтрация сбивает сортировку?




      var results = []
      // ...
      .map(n => {
        n.data = nstd.Data[n.itemId]
        results.push(n)
      })
      childs = results

Ну а .map(n => nstd.Data[n.itemId]) не судьба написать было? Зачем тут вообще массив results?


И ещё вопрос: зачем использовать метод .map в качестве заменителя цикла for-of, если у массивов есть метод .forEach для тех же целей?




И ещё один пример как не надо писать в том же самом месте, что и прошлые два.


    if (childs.length > 0) {
      var results = []
      childs.sort((a, b) => {
        if (a.lkey > b.lkey) return 1
        if (a.lkey < b.lkey) return -1
        return 0
      }).map(n => {
        n.data = nstd.Data[n.itemId]
        results.push(n)
      })
      childs = results
    }

Зачем тут условный оператор? Что sort, что map на пустом массиве прекрасно работают. Если это была попытка оптимизации — то экономить надо в первую очередь на асимптотике, во вторую очередь на лишних массивах, и лишь в последнюю очередь на спичках.




Ну и последнее


var nstd = this

Зачем так делать в 2020м году? В стрелочных функциях this не "теряется", и его больше не требуется подобным способом "беречь".


Не говоря уже о том, что у вас уже есть переменная _nestedsets для тех же целей.

Большое спасибо за критику, она помогает развиваться.

По поводу удаление ветки узлов, да вы абсолютно правы, внес изменения.
С сортировкой опять же да, лишние сортировки не нужны, убрал.

Что касается .map(n => nstd.Data[n.itemId]) позвольте не согласиться, эта конструкция вернет элемент из свйоства Data, а там храняться данные, а не узлы, мне же необходима информация по узлам. В качестве свойства data для каждого узла (как дополнение) я указывал элемент из свойства Data, но вся информация об узле также осталась.

О использовании map вместо forEach, разница методов в том, что один вернет новый массив, а второй вернет undefined. Если говорить о скорости, то я видел тесты, где map значительно быстрее forEach. Возможно не прав, если так — поправьте.

var nstd = this
Да, полностью согласен, не нужно так

Переписал все на Typescript.
github.com/orxtime/nestedsetsjs/blob/typescript/src/nestedsets.ts

Буду ждать от вас новых комментариев, спасибо.
Что касается .map(n => nstd.Data[n.itemId]) позвольте не согласиться, эта конструкция вернет элемент из свйоства Data, а там храняться данные, а не узлы, мне же необходима информация по узлам

В таком случае вам тем более не нужен массив results, поскольку его содержимое совпадает с содержимым childs.


О использовании map вместо forEach, разница методов в том, что один вернет новый массив, а второй вернет undefined. Если говорить о скорости, то я видел тесты, где map значительно быстрее forEach.

Это весьма странная информация. Мне почему-то кажется, в тех тестах сравнивался одиночный map и комбинация forEach + push.

Убрал и childs. Теперь сразу return.

По поводу Map vs forEach: проводил такой эксперимент

const myAwesomeArray = Array.from({length: 1000}, (e,i) => i)

console.time("forEachTest")
myAwesomeArray.forEach(x => (x + x) * 10000000000)
console.timeEnd("forEachTest")

console.time("mapTest")
myAwesomeArray.map(x => (x + x) * 10000000000)
console.timeEnd("mapTest")


Результаты такие:

forEachTest: 0.123ms
mapTest: 0.069ms

forEachTest: 0.122ms
mapTest: 0.078ms

forEachTest: 0.127ms
mapTest: 0.068ms

Ну а вот мой результат:


forEachTest: 0.04296875 ms
mapTest: 0.05078125 ms


forEachTest: 0.052978515625 ms
mapTest: 0.055908203125 ms


forEachTest: 0.053955078125 ms
mapTest: 0.057861328125 ms


Ищите проблему в окружении. Может, где-то полифил неоптимальный стоит, или наоборот — нестандартный. Или транспайлер начудил. Или всё проще, и GC прибирает за прошлым map как раз во время forEach.

Последнее вряд ли, тесты запускал отдельно друг от друга. Буду разбираться. Спасибо.

Что же до вашего кода на Typescript — у вас там лишние классы NestedSetItem, NestedSetNode, NestedSetRootNode, NestedSetEmptyNode и NesteSetError (вместо них проще использовать литералы объектов, точно так же как вы на js делали). Кроме того, интерфейс INestedSet тоже лишний.


Ну и какой нафиг module.exports? Проще всего export class NestedSet { ... } написать. Ну а если так важно экспортировать именно объект — конструкция export = new NestedSet() в помощь. Хотя в таком случае лучше вообще отказаться от класса и экспортировать отдельные функции:


export function removeItem (itemId: number) {
  // ...
}

export function addNode (targetNodeId: number, itemId: number) {
  // ...
}
childs.sort((a, b) => {
        if (a.lkey > b.lkey) return 1
        if (a.lkey < b.lkey) return -1
        return 0
      })

Пять копеек от меня:


childs.sort((a, b)=>(+(a.lkey>b.lkey)-(a.lkey<b.lkey)))

:)

iulanovy4, да, все верно. Если говорить конкретно о моей задаче то мне нужно осущиствить переход от MySQL (Joomla, Nested Set) на ElasticSearch (Nested Set) + MongoDB (Nested Set), попутно отредактировав структуру. Отсюда это решение. Возможно это можно использовать как хранилище на клиентской стороне (но это уже догадки).
Все то, что вы назвали (Adjacency List, Matherialized Path, Nested Set и Closure Table), это способы хранения древовидных структур данных в реляционных базах. Зачем они в памяти?
Мне нужно осущиствить переход от MySQL (Joomla, Nested Set) на ElasticSearch (Nested Set) + MongoDB (Nested Set), попутно отредактировав структуру. Отсюда это решение.

Возможно это можно было бы использовать как хранилище на клиентской стороне (но это уже догадки). Из БД подгружать данные на клиент и тот уже редактирует дерево, не нагружая при этом сервер. А когда редактирование завершено — записывает данные в БД. Тут конечно вопрос синхронизации с сервером.

А не проще ли в таком случае перейти от nested sets к нормальному древовидному виду, внести там изменения — и снова построить nested sets? Быстрее так точно:


function nestedSetsToTree(items) {
    items.sort(...);

    let root = { rkey: +Infinity, data: { children: [] } };
    let stack = [ root ];
    let top = () => stack[stack.length-1];

    for (let { lkey, rkey, ...data } of items) {
        data = { ...data, children: [] };
        while (lkey >= top().rkey) stack.pop();
        top().data.children.push(data);
        stack.push({ rkey, data });
    }

    return root.data.children;
}

function treeToNestedSets(roots) {
    let result = [];
    let nextKey = 0;
    for (let root in roots) visit(root);
    return result;

    function visit({children, ...data}) {
        let item = { lkey: nextKey++, ...data };
        result.push(item);
        for (let child in children) visit(child);
        item.rkey = nextKey++;
    }
}
В мыслях было сделать универсальный переходник между Adjacency List, Matherialized Path, Nested Set, Closure Table и нормальным деревом.
const maxId = Math.max(...this.Structure.map(o => o._id)) || 0

Если будет слишком много элементов, случится Maximum call stack size exceeded
reduce будет понадежнее, хотя и медленнее
Но у вас уже используется map, и с учетом этого reduce будет уже быстрее чем map+rest arguments

var arr = Array.from(Array(100000), () => ({_id: Math.round(Math.random() * 10000)}));
console.time('reduce');// 1180ms
for(var i = 0; i < 1000; i++)
    arr.reduce((a,{_id}) => (a > _id ? a : _id), -Infinity)
console.timeEnd('reduce');
console.time('rest args');// 1748ms
for(var i = 0; i < 1000; i++)
    Math.max(...arr.map(x => x._id))
console.timeEnd('rest args');


Также || 0 выглядит как ненужная перестраховка
Если элементов не будет, то вы получите -Infinity, и этот код не поможет
В остальных случая _id вроде всегда число, а значит мы и так получим максимальный _id
Спасибо за рекомендации, поправил.
Sign up to leave a comment.

Articles