在元素中保存超时

Saving a Timeout within the Element

我创建了一个带有下拉菜单的 <select multiple>(如果您对完整代码感兴趣:Codepen 或底部的代码段)以及处理模糊的辅助函数(下面的简化版本)事件(输入焦点丢失,也就是点击其他地方):

function blur(element) {
    clearTimeout(eval(element.dataset.timer));
    element.dataset.timer = setTimeout(() => {
        if (activeElement !== el)
            el.style.display = "none";
    }, 200).toString();
}

它创建一个超时并将其存储在元素的 dataset-property 中。由于此 属性 仅接受字符串,因此它由 toString() 转换并在清除前一个计时器时由 eval() 转换回来。这不是必需的,因为它在不进行转换和评估时工作得很好:

function blur(element) {
    clearTimeout(element.dataset.timer);
    element.dataset.timer = setTimeout(() => {
        if (activeElement !== el)
            el.style.display = "none";
    }, 200);
}

无论哪种方式,它都感觉像是一个 hack,我也知道不应该使用 eval(),这就是为什么我想知道我应该如何解决这个问题。我已经 showed my code to CodeReview 特别提到了这个功能,但没有人抱怨它。这不可能是正确的做法,对吗?

如果相关,这里是完整代码的片段:

convertSelect("001", "Options");

function convertSelect(el_id, name) {
    let el = document.getElementById(el_id),
        opts = Array.from(el.options);

    let input_el = document.createElement('input');
    input_el.setAttribute('id', el_id + '_input');
    input_el.setAttribute('type', 'text');
    input_el.setAttribute('autocomplete', 'off');
    input_el.setAttribute('readonly', 'readonly');
    input_el.setAttribute('style', `width:${el.offsetWidth}px`);
    input_el.addEventListener('focus', () => document.getElementById(el_id + '_span').style.display = "");
    input_el.addEventListener('blur', () => blur(el_id));
    el.parentNode.insertBefore(input_el, el.nextSibling);

    let span_el = document.createElement('span');
    span_el.setAttribute('id', el_id + '_span');
    span_el.setAttribute('style', `min-width:${(input_el.offsetWidth + 50)}px;margin-top:${input_el.offsetHeight}px;margin-left:-${input_el.offsetWidth}px;position:absolute;border:1px solid grey;display:none;z-index:9999;text-align:left;background:white;max-height:130px;overflow-y:auto;overflow-x:hidden;`);
    span_el.addEventListener('mouseout', () => blur(el_id));
    span_el.addEventListener('click', () => document.getElementById(el_id + '_input').focus());
    input_el.parentNode.insertBefore(span_el, input_el.nextSibling);

    opts.forEach(opt => {
        let i = opts.indexOf(opt);

        let temp_label = document.createElement('label');
        temp_label.setAttribute('for', el_id + '_' + i);

        let temp_input = document.createElement('input');
        temp_input.setAttribute('style', 'width:auto;');
        temp_input.setAttribute('type', 'checkbox');
        temp_input.setAttribute('id', el_id + '_' + i);
        temp_input.checked = opt.selected;
        temp_input.disabled = opt.disabled || el.disabled;
        temp_input.addEventListener('change', () => check(el_id, name));

        temp_label.appendChild(temp_input);
        temp_label.appendChild(document.createTextNode(opt.textContent));
        span_el.appendChild(temp_label);
    });
    el.style.display = 'none';
    check(el_id, name);
}


function blur(el_id) {
    let el = document.getElementById(el_id);
    clearTimeout(el.dataset.timer);
    el.dataset.timer = setTimeout(() => {
        if (document.activeElement.id !== el_id + '_input' && document.activeElement.id !== el_id + '_span')
            document.getElementById(el_id + '_span').style.display = "none";
    }, 200);
}

function check(el_id, name) {
    let el = document.getElementById(el_id),
        opts = Array.from(el.options),
        select_qty = 0,
        select_name;

    opts.forEach(opt => {
        let i = opts.indexOf(opt),
            checkbox = document.getElementById(`${el_id}_${i}`);

        el.options[i].selected = checkbox.checked;
        if (checkbox.checked) {
            select_name = checkbox.parentElement.childNodes[1].textContent;
            select_qty++;
        }
        document.getElementById(`${el_id}_input`).value = select_qty < 1 ? '' : (select_qty > 1 ? `${select_qty} ${name}` : select_name);
    });

    el.dispatchEvent(new Event('change', { 'bubbles': true }));
}
label {
  display: block;
}

input[type="text"]:hover {
  cursor: default;
}
<select id="001" multiple>
  <option value="2">Option Two</option>
  <option value="4">Option Four</option>
  <option value="6">Option Six</option>
  <option value="8" disabled>Disabled Option</option>
</select>

您能否创建一个全局计时器对象,在其中使用连续键存储超时并仅将键存储在数据集中? 有点像:

var timersObj = {};
//store timeout in object
timersObj["timerIndex_0"] = setTimeout(() => {
        if (activeElement !== el)
            el.style.display = "none";
    }, 200)
element.dataset.timer = "timerIndex_0";
//clear timeout by key
clearTimeout(timersObj[element.dataset.timer])

如果您使用的是静态编译语言,有时 JavaScript 松散的类型装箱/拆箱会让人觉得陌生。不过,您的示例应该没有任何问题。

但是,还有其他方法可以在元素上存储数据,如果您的数据变得更加复杂,存储在 data-attribute 上可能不是最好的主意。


一个选项,只是你data-attribute想法的延伸。您可以使用 JSON 序列化您的数据:

element.dataset.options = JSON.stringify({timerId: setTimeout(....)});

上述的一个缺点是您可以存储的类型仅限于 JSON 可以序列化的类型,但是字符串/整数/数组等可以很好地工作。


JavaScript 中的元素与任何其他对象一样。所以理论上你可以存储为 属性:

element._mytimerid = setTimeout(...`, 

使用这种方法,您必须小心所谓的 属性。


闭包是一个流行的选择:

function setupBlur() {
  var timerId = null;
  element.onBlur = function () {
    clearTimeout(timerId);
    timerId = setTimeout(.....
  }
}

setupBlur();

您也可以在 IIFE 中使用上面的方法,以节省调用 setupBlur:

(function () { 
  var timerId = null;
  element.onBlur = function () {
  clearTimeout(timerId);
  timerId = setTimeout(.....
}());

一种更现代的在元素上存储数据的方法是使用 Wea​​kMap:

const timerIds = new WeakMap();
.....
const timerId = timerIds.get(element);
clearTimeout(timerId);
timerIds.set(element, setTimer(.....));