Оптимизировать прототип запроса «ближайший» элемент

Я разработал рекурсивную функцию для захвата «ближайшего» элемента. Это немного отличается от Element.closest, потому что вместо обхода только непосредственного родителя (ей); он проходит вверх, но проверяет влево-вправо-вверх-вниз.

Есть ли более эффективная проверка пропавших без вести братьев и сестер?

if (Element.prototype.nearest === undefined) {
  const nearest = (el, selector) => {
    if (el == null) return undefined;
    const prev = el.previousElementSibling;
    if (prev?.matches(selector)) return prev;
    const next = el.nextElementSibling;
    if (next?.matches(selector)) return prev;
    const parent = el.parentElement;
    if (parent?.matches(selector)) return parent;
    const relative = parent.querySelector(selector);
    if (relative) return relative;
    return nearest(el.parentElement, selector);
  };
  Element.prototype.nearest = function(selector) {
    return nearest(this, selector);
  };
}

const handleClick = (e) => {
  const btn = e.target;
  console.log(btn.nearest('.child'));
  console.log(btn.nearest('.grand-parent'));
  console.log(btn.nearest('#child-1'));
}

document.querySelectorAll('.btn').forEach(btn =>
  btn.addEventListener('click', handleClick));
.parent {
  display: flex;
}
<div class="grand-parent">
  <div class="parent">
    <div class="child" id="child-1">
      <button class="btn">
        Click
      </button>
    </div>
    <div class="child" id="child-2">
      <button class="btn">
        Click
      </button>
    </div>
  </div>
</div>

1 ответ
1

Вопрос

«Есть ли более эффективная проверка пропавших без вести братьев и сестер?»

Как это сделано в приведенном вами примере кода. Нет.

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

Ошибка

Линия…

    const relative = parent.querySelector(selector);

… бросит, если parent является null

Небезопасное расширение прототипа

Без всяких предосторожностей добавлять к существующему прототипу — плохая идея. Element.prototype.nearest может не использоваться в данный момент, но в любой момент может быть добавлен в DOM, нарушив любой код, в котором вы его используете.

Есть три способа избежать конфликтов / будущих расширений прототипов.

.1 100% безопасность. Не надо !!! расширение прототипа — это открытая ловушка, готовая укусить в любой момент.

.2 Почти безопасно. Используйте нестандартное соглашение об именах, например Element.prototype.select_nearest Стандарт не будет использовать это имя, но это не означает, что оно на 100% безопасно, так как это имя может использовать кто угодно.

.3 Безопасно, но неудобно. Использовать Символ для определения имени прототипа. Это гарантирует уникальность имени. К сожалению, это означает, что вам нужно будет использовать bracket[notation] чтобы получить доступ к свойству и поделиться им с кодом, которому требуется доступ. Например

// define
const nearest = Symbol("nearest");
Element.prototype[nearest] = function() { /* ... code ... */ }   // prototype is global

// call
btn[nearest](".child");

Чтобы предоставить доступ к символу в глобальной области видимости, используйте Символ. Для


{   // scope block
    const nearest = Symbol.for("nearest");  // find or create
    Element.prototype[nearest] = function() { log("Nearest") }  // prototype is global
}

// Access in unconnected scope
{   
    Symbol.for("nearest")
    btn[nearest](".child");

    // or 
    btn[Symbol.for("nearest")](".child");
}

Стиль

Замечание о стиле.

Плохая привычка не разграничивать блоки кода {} например if (prev?.matches(selector)) return prev; безопаснее как if (prev?.matches(selector)) { return prev; }

Несвязанная функция?

Зачем добавлять в прототип элемента функцию, вызывающую несвязанную стрелочную функцию. Это просто делает код чрезмерно сложным.

Вы можете вызвать функцию nearest непосредственно например nearest(btn, '.child'); или оставьте его на прототипе, избегая вторичного вызова и необходимости передавать this Смотрите переписывает.

Переписать

Несколько переписываний в качестве примеров, хотя, кроме ошибок и перечисленных выше моментов, я не вижу реальной необходимости в переписывании.

Как прототип

const nearest = Symbol.for("nearest");
Element.prototype[nearest] = function(selector) {  
    const prev = this.previousElementSibling;
    if (prev?.matches(selector)) { return prev }
    const next = this.nextElementSibling;
    if (next?.matches(selector)) { return next }
    const parent = this.parentElement;
    if (parent) {
        if (parent.matches(selector)) { return parent }
        return  parent.querySelector(selector) ?? parent[nearest](selector);
    }
}

Как функция


function nearest(self, sel) {  
    const prev = self.previousElementSibling;
    if (prev?.matches(sel)) { return prev }
    const next = self.nextElementSibling;
    if (next?.matches(sel)) { return next }
    const parent = self.parentElement;
    if (parent) {
        if (parent.matches(sel)) { return parent }
        return  parent.querySelector(sel) ?? parent.nearest(parent, sel);
    }
}

или же

const matches = (el, sel) => el && el.matches(sel) ? el : undefined;
function nearest(self, sel) {  
    const parent = self.parentElement;
    const sibling = matches(self.previousElementSibling, sel) ?? 
        matches(self.nextElementSibling, sel) ??
        matches(parent, sel) ?? 
        parent?.querySelector(selector) ?? 
        parent?.nearest(parent, selector);
}

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *