如何将每个数组项绑定到其元素节点表示并使节点监视数组?

How to bind each array item to its element node representation and make the nodes watch the array?

我有一个数组,我想在 div 标签中打印它们,而且当我对数组进行更改时,我希望更改也发生在 div 上(因为例如,当我删除一个项目时,项目的 div 打印也被删除;或者更改项目的值,期望项目的 div 发生事情)。我做了一些研究,发现了一些我以前不知道的东西,叫做 Proxy 对象。我写了下面的代码:

let fruits = [
  "Apple", "Pear", "Oak", "Orange", "Tangerine", "Melon",
  "Ananas", "Strawberry", "Cherry", "Sour Cherry","Banana",
  "Plum", "Pomegranate", "Apricot", "Peach", "Nectarine",
  "Kiwi", "Mulberry", "Fig", "Grapefruit", "Grape",
];
let trees = document.getElementById("trees");
let tree = new Array();

let aproxy = new Proxy(fruits, {
  get: (target, key, receiver) => {
    let treeDiv = document.createElement("div");
    treeDiv.classList.add("tree");

    let span = document.createElement("span");
    span.textContent = target[key] + " Tree";

    treeDiv.appendChild(span);
    trees.appendChild(treeDiv);

    treeDiv.addEventListener("dblclick", () => {
      fruits.splice(key, 1);

      //Here I delete all the item print divs before reprint all of them
      trees.querySelectorAll("div.tree").forEach(el => {
        trees.removeChild(el);
      });

      //Here I make the iteration to reprint
      fruits.forEach((el, index) => {
        aproxy[index];
      });
    });
    return treeDiv;
  },
  set: (target, key, value) => {
    fruits.push(value);
    aproxy[key];
    return true;
  },
});

fruits.forEach((el, index) => {
  aproxy[index];
});

let input = document.querySelector("#inputTag");

input.addEventListener("keyup", ev => {
  if (ev.code === "Enter") {
    aproxy[fruits.length] = input.value;
    console.log(fruits);
  }    
});
@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,300;0,400;0,600;0,700;1,200;1,300;1,400;1,600&display=swap');

body {
  background: linear-gradient(135deg,rgb(204, 64, 64),rgb(204, 64, 118));
  margin: 0 0 100px 0;
  background-attachment: fixed;
  zoom: .7;
}
.as-console-wrapper { height: 100px!important; }

#container {
  width: 100%;
  margin: auto;
  padding: 10px;
}
#input {
  width: 93%;
}

#input input {
  border:none;
  padding: 17px 20px;
  margin: 0;
  border-radius: 15px;
  display: block;
  width: 100%;
  font-size: 14pt;
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 300;
  box-shadow: 0 10px 10px rgba(0,0,0,0.1);
}
#input input::placeholder {
  font-family: 'Nunito Sans', sans-serif;
  font-size: 14pt;
  font-style: italic;
}
#input input:focus {
  outline: none;
}

#trees {
  display: flex;
  margin-top: 15px;
  flex-wrap: wrap;
  width: 100%;
  justify-content: center;
}
#trees .tree {
  padding: 10px;
  background: rgba(255,255,255,1);
  margin: 7px;
  border-radius: 10px;
  cursor: pointer;
  transition: background .3s;
  user-select: none;
}
#trees .tree:hover {
  background: rgba(255,255,255,.1);
}
#trees .tree:hover span {
  color: #fff;
}
#trees .tree span {
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 600;
  color: rgb(204, 64, 64);
  transition: color .3s;
}
<div id="container">
  <div id="input">
    <input placeholder="Enter a tree and press 'Enter'" type="text" name="" id="inputTag">
  </div>
  <div id="trees">
  </div>
</div>

如您所见,我在删除项目时重新打印 divs。有没有另一种方法可以通过 Proxy 对象来做到这一点而无需重新打印?我可以将数组项绑定到 divs 吗?

另一种可能的解决方案是基于 hand-knitted 模型和控制器逻辑。

一个人会完全分离纯控制器任务,让它们只直接与 DOM 和最初提供的列表 items/values.

的建模抽象一起工作

模型本身可以是例如一个基于 Map 的注册表,它实现了始终控制正确列表状态的逻辑。

因此,除了最明显的 register/deregister 方法外,还将进行清理和检查任务,以防止例如双重注册(可能)等于 items/values。这样的注册表模型还可以为其注册项目的特殊列表表示提供吸气剂,例如仅提供每个项目的文本内容的当前数组或每个项目的模型的数组。

至于后者,这样的模型除了例如它的 id 和文本 value 也将具有自己的视图,例如待 rendered/removed 元素节点的 elm 引用。

为了保持每个项目特定DOM节点and/or每个项目的模型没有控制器逻辑,main控制器任务使用事件委托 [1],[2] 通过监听/处理列表 root-node 的 double-click 事件。

下一个提供的示例代码演示了 main 控制器任务如何操作 DOM 和项目列表抽象 ...

function sanitizeInputValue(value) {
  return value
    .trim()
    .replace(/\s+/g, ' ')
    .replace(/(?:\stree)+$/i, '');
}

function createItemNode(id, value) {
  const itemRoot = document.createElement('div');

  itemRoot.dataset.itemId = id;
  itemRoot.classList.add('item');

  let contentNode = document.createElement('span');
  contentNode.textContent = `${ value } Tree`;

  itemRoot.appendChild(contentNode);

  return itemRoot;
}
function createItemViewModel(id, value) {
  return {
    id,
    value,
    elm: createItemNode(id, value),
  };
}

function createItemListViewModel(itemList) {
  const registry = new Map;

  const register = value => {
    let viewModel;

    value = sanitizeInputValue(value);
    const id = value.toLowerCase();

    if (!registry.has(id)) {
      viewModel = createItemViewModel(id, value);

      registry.set(id, viewModel);
    }
    return viewModel;
  };
  const deregister = id => {
    let viewModel;

    if (registry.has(id)) {
      viewModel = registry.get(id);

      registry.delete(id);
    }
    return viewModel;
  };
  const getViewModels = () =>
    [...registry.values()];
  const getValueList = () =>
    [...registry].map(([id, viewModel]) => viewModel.value);

  // default register items from initial list.
  itemList.forEach(register);

  return {
    register,
    deregister,
    getViewModels,
    getValueList,
  }
}


function main/*Controller*/(itemList) {
  // create a view-model of the provided items by programmatically
  // adding each items own view-model to the overall items registry.
  const itemsRegistry = createItemListViewModel(itemList);

  let itemsRoot = document.querySelector("#item-list");
  let itemInput = document.querySelector("#item-input");
  
  // initial rendering of the provided items from each items's view model.
  itemsRegistry
    .getViewModels()
    .forEach(viewModel =>
      itemsRoot.appendChild(viewModel.elm)
    );

  // register controller logic for a list item's double-click handling.
  itemsRoot.addEventListener('dblclick', evt => {
    const target = evt.target.closest('.item');
    const itemId = target?.dataset.itemId;

    if (itemId) {
      // remove view-model from registry in case it does exist.
      const viewModel = itemsRegistry.deregister(itemId);

      if (viewModel) {
        // remove element node of a successfully removed view-model from DOM.
        viewModel.elm.remove();

        console.clear();
        console.log(itemsRegistry.getValueList());
      }
    }
  });

  // register controller logic for handling a potential new list item.
  itemInput.addEventListener('keyup', evt => {
    if (evt.code === "Enter") {

      // add view-model to registry in case it does not already exist.
      const viewModel = itemsRegistry.register(evt.currentTarget.value);

      if (viewModel) {
        // append element node of newly registered view-model to DOM.
        itemsRoot.appendChild(viewModel.elm);

        // sanitize any submitted input value.
        itemInput.value = viewModel.value;

        console.clear();
        console.log(itemsRegistry.getValueList());
      }
    }
  });
};


let fruits = [
  "Apple", "Pear", "Oak", "Orange", "Tangerine", "Melon",
  "Ananas", "Strawberry", "Cherry", "Sour Cherry","Banana",
  "Plum", "Pomegranate", "Apricot", "Peach", "Nectarine",
  "Kiwi", "Mulberry", "Fig", "Grapefruit", "Grape",
];
main/*Controller*/(fruits);
@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,300;0,400;0,600;0,700;1,200;1,300;1,400;1,600&display=swap');

body {
  background: linear-gradient(135deg,rgb(204, 64, 64),rgb(204, 64, 118));
  margin: 0 0 100px 0;
  background-attachment: fixed;
  zoom: .7;
}
.as-console-wrapper { height: 100px!important; }

#container {
  width: 100%;
  margin: auto;
  padding: 10px;
}
#input-wrapper {
  width: 93%;
}

#input-wrapper input {
  border:none;
  padding: 17px 20px;
  margin: 0;
  border-radius: 15px;
  display: block;
  width: 100%;
  font-size: 14pt;
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 300;
  box-shadow: 0 10px 10px rgba(0,0,0,0.1);
}
#input-wrapper input::placeholder {
  font-family: 'Nunito Sans', sans-serif;
  font-size: 14pt;
  font-style: italic;
}
#input-wrapper input:focus {
  outline: none;
}

#item-list {
  display: flex;
  margin-top: 15px;
  flex-wrap: wrap;
  width: 100%;
  justify-content: center;
}
#item-list .item {
  padding: 10px;
  background: rgba(255,255,255,1);
  margin: 7px;
  border-radius: 10px;
  cursor: pointer;
  transition: background .3s;
  user-select: none;
}
#item-list .item:hover {
  background: rgba(255,255,255,.1);
}
#item-list .item:hover span {
  color: #fff;
}
#item-list .item span {
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 600;
  color: rgb(204, 64, 64);
  transition: color .3s;
}
<div id="container">
  <div id="input-wrapper">
    <input placeholder="Enter a tree and press 'Enter'" type="text" id="item-input">
  </div>
  <div id="item-list">
  </div>
</div>