Skip to content

商品参数选项-关闭选中标签元素与商品参数选项-点击选项和选中(Day21 2/2) ​

本页对应 Day21_PC项目,共 4 段视频,约 1 小时 6 分。

学习目标 ​

  • 能使用事件委托监听选中标签上的关闭按钮,完成删除标签、取消对应选项选中、默认选中同组第一项,并同步维护选中项数组。
  • 能封装统一的价格计算函数,在点击参数选项、关闭选中标签等操作后调用该函数并更新页面价格显示。
  • 能通过选中项数组及选项上的价格变化自定义属性,完成商品单价的重新计算与展示。
  • 能实现商品数量加减按钮和输入框 change 事件,完成数量同步、最小数量为 1 及非法输入重置校验。
  • 能根据商品单价和数量计算主商品总价,并在数量变化后同步更新价格显示。
  • 能实现搭配商品复选框的选择与取消选择,动态加减搭配商品价格、更新搭配数量及最终总价显示。

知识点 ​

先明确:关闭选中标签要同步三件事 ​

为什么需要:每个选中标签右侧都有一个“叉”,用户点它不只是把标签删掉,还要让对应选项取消选中,并让该组第一个选项重新选中。否则会出现“标签没了,但选项还亮着”或“数组里还留着已删选项”的状态错乱。

怎么想:把一次点击拆成三个动作:删除当前选中标签、取消对应 dd 的选中、恢复同组第一个 dd 的选中。三件事必须围绕同一份数据同步,不能只改界面。

怎么做:先通过事件委托命中关闭按钮,拿到它所在的选中标签,再取出索引,找到数组中对应的 dd,完成状态修改。

易错点:event.target 是“叉”,不是整个选中标签;选中标签要用 parentElement 找。删除标签用 selectedBooks.removeChild(tag),不要误删别的节点。

为什么用事件委托,而不是直接给叉绑定 ​

为什么需要:选中标签是用户点击选项后新创建的 span,如果每创建一个就绑一次事件,代码重复且容易漏绑。转写稿里明确说“这个 span 很明显是新创建的,所以还是得委托”。

怎么想:委托目标必须是页面一开始就存在、并且能包住所有选中标签的元素。不能委托给新创建的 selectedTag,应委托给已经获取过的包裹元素 selectedBooks。

怎么做:先写验证逻辑,只打印“删除”,确认点叉能命中:

js
selectedBooks.onclick = function (event) {
  if (event.target.className === 'close') {
    console.log('删除');
  }
};

验证通过后再补删除和状态同步。

易错点:转写稿用 className === 'close' 判断;如果以后给叉加多个类名,这种写法会失效,可改成 event.target.classList.contains('close')。委托目标也不能选错。

用 data-index 建立“标签—选项—数组”的对应关系 ​

为什么需要:选中标签和对应的 dd 没有直接 DOM 父子关系,删除标签后还要找到数组里对应的 dd。转写稿里数组 selectedArr 存的就是 dd 元素,所以需要索引桥接。

怎么想:在创建选中标签时,把当前选项索引写到标签的自定义属性上:tag.dataset.index = index。删除时从标签上取索引,就能拿到 selectedArr[index]。

怎么做:删除标签、取消 dd 选中、恢复同组第一个 dd:

js
selectedBooks.onclick = function (event) {
  if (event.target.className === 'close') {
    const tag = event.target.parentElement;
    const index = tag.dataset.index;
    const dd = selectedArr[index];

    // 对应选项取消选中
    dd.classList.remove('active');

    // 同组第一个 dd 重新选中:children[0] 是 dt,children[1] 是第一个 dd
    dd.parentElement.children[1].classList.add('active');

    // 删除当前选中标签
    selectedBooks.removeChild(tag);

    // 从数组中删除,但保留数组长度
    delete selectedArr[index];

    // changePrice(); // 转写稿后续在这里补价格重算
  }
};

易错点:dataset.index 取出来是字符串,作为数组索引通常可用,但不要和数字类型混淆。dd.parentElement.children[1] 依赖结构:第一个子元素是 dt,第二个才是第一个 dd。如果直接 splice,数组长度会变,转写稿强调要用 delete 保留长度,避免已删选项再次出现。

调试过程与完整检查清单 ​

为什么需要:这一步同时改标签、dd、数组三处状态,最容易出现“界面删了但数组没删”的问题。转写稿中先点叉验证命中,再逐步加删除自身、取消 dd、恢复第一个,最后发现不删数组会导致已删选项再次出现。

怎么想:按“命中按钮 → 删除标签 → 取消对应 dd → 恢复同组第一个 → 删除数组项 → 价格重算”的顺序排查。每加一步就刷新页面点一次,观察控制台和界面。

怎么做:完整逻辑就是上面的委托代码。若删除后价格没变,不是关闭逻辑错,而是还没调用后续封装的 changePrice();转写稿在价格变化部分把调用补到了这里。

易错点:event.target 不是标签;parentElement 找错会删错节点;children[1] 写错会选中错误选项;忘记 delete selectedArr[index] 会让已删选项“复活”;className 判断对多类名不稳健。按转写稿的实现,关闭标签的核心就是:用索引同步数组,用数组同步 dd 选中状态,再统一重算价格。

一、先定思路:价格由多个操作共同触发 ​

为什么需要:点击选项、关闭选中标签、修改数量、勾选搭配商品,都会改变价格。若每个事件里各写一套算法,后续很难同步。

怎么想:把“算价格并显示”封装成 changePrice,所有会改价格的地方只负责改数据,然后调用它。转写稿里反复强调“只要一变化就重新算一遍”,这样最稳。

怎么做:先列出入口:选项点击、选中标签关闭、数量加减/输入、搭配商品勾选。每个入口处理完数据后调用 changePrice()。

易错点:漏调用是常见问题,表现为“数据变了但价格没变”。调试时可在 changePrice 里 console.log(price),确认每次操作都进入函数。

二、关闭选中标签:删标签、取消选项、恢复同组第一个 ​

为什么需要:选中标签上的“叉”不仅要删掉标签,还要让对应 DD 取消选中,并让该组第一个选项重新选中。

怎么想:选中标签是动态创建的,不能直接给每个 span 绑事件,应事件委托到包裹元素 selectedBooks。判断点击目标是否为关闭按钮:

js
selectedBooks.onclick = function (event) {
  if (event.target.className === 'close') {
    // 删除自己
  }
};

怎么做:创建选中标签时记录索引:selectedTag.dataset.index = index。数组 selectedArr 里存的是对应 DD。删除时:

js
const tag = event.target.parentElement;
const index = tag.dataset.index;
const ddEle = selectedArr[index];

selectedBooks.removeChild(tag);
ddEle.classList.remove('active');

// 同组第一个 DD 重新选中:DL 中第一个子元素是 DT,第二个才是 DD
ddEle.parentElement.children[1].classList.add('active');

// 保持数组长度,不改变后续索引
delete selectedArr[index];
changePrice();

易错点:事件委托别绑到动态 span 上;删除标签用 parentElement 找到整个选中标签;数组删除用 delete 而不是 splice,否则索引会错位;删除后必须调用 changePrice()。

三、价格计算:基础价 + 选项价格变化 ​

为什么需要:选项有加价或减价,价格要实时反映。

怎么想:创建 DD 时把该选项的 changePrice 存到自定义属性上:

js
ddEle.dataset.changePrice = ddItem.changePrice;

changePrice 里先取基础价,再遍历 selectedArr,把每个选中 DD 的 changePrice 加上:

js
function changePrice() {
  let price = goodsDetail.price;
  selectedArr.forEach(ddEle => {
    price += +ddEle.dataset.changePrice;
  });
  priceBox.innerHTML = price;
}

怎么做:点击选项后、关闭标签后都调用 changePrice()。转写稿中用控制台输出验证过:选中 128G、黑色等,价格会按选项变化;全部叉掉后回到基础价。

易错点:dataset 取出来是字符串,必须用 + 转数字;不要忘记关闭标签也要调用;显示价格元素要提前获取。

四、数量与总价:加减按钮、手动输入 ​

为什么需要:数量影响总价,且加减按钮和输入框都能改数量。

怎么想:定义全局 productNumber 记录数量,所有入口同步它;总价 = 单价 × 数量。

怎么做:

js
plusBtn.onclick = function () {
  productNumber++;
  nameInput.value = productNumber;
  changePrice();
};

minusBtn.onclick = function () {
  productNumber--;
  if (productNumber < 1) productNumber = 1;
  nameInput.value = productNumber;
  changePrice();
};

nameInput.onchange = function () {
  productNumber = +nameInput.value;
  if (isNaN(productNumber) || productNumber < 1) {
    productNumber = 1;
    nameInput.value = productNumber;
  }
  changePrice();
};

在 changePrice 中计算主商品总价:

js
const masterPrice = price * productNumber;
masterPriceBox.innerHTML = '¥' + masterPrice;
totalPriceBox.innerHTML = '¥' + (masterPrice + collectionPrice);

易错点:输入框用 change,不是一输入就触发,而是值改变且失去焦点;空字符串转数字为 0,会被 <1 拦下;不要忘记回写输入框。

五、搭配商品:勾选加价、取消减价 ​

为什么需要:套餐总价要包含搭配商品,勾选和取消要同步数量与总价。

怎么想:定义 collectionPrice 和 collectionNumber。每个复选框监听 change,用 checked 判断选中还是取消。

怎么做:

js
collectionInput.forEach(input => {
  input.onchange = function () {
    if (input.checked) {
      collectionPrice += +input.value;
      collectionNumber++;
    } else {
      collectionPrice -= +input.value;
      collectionNumber--;
    }
    totalNumberBox.innerHTML = collectionNumber;
    changePrice();
  };
});

易错点:input.value 是字符串,必须转数字;总价拼接时曾出现多一个 0,原因是字符串拼接,解决方法是先算数字:(masterPrice + collectionPrice);最后统一调用 changePrice() 显示。

修改商品数量价格变化:从删除标签到总价同步 ​

本节围绕“商品参数选项”继续完善:删除选中标签、价格随选项变化、数量修改、搭配商品选择。转写稿里反复强调的核心是:选中项统一放进数组,所有会改变选择的操作用数组同步,再封装价格计算函数统一刷新显示。本节原始笔记未附图片,以下保留可复现的关键代码。

删除选中标签并同步选项 ​

为什么需要:选中标签上的“叉”不能只删掉标签本身,否则对应的 DD 仍处于选中态,价格和选项状态会错乱。怎么想:关闭按钮是动态创建的,事件要委托给已经存在的包裹元素 selectedBooks;删除时要知道当前标签对应数组里的哪个 DD,所以在创建选中标签时把索引写到 dataset.index。怎么做:

js
selectedBooks.addEventListener('click', function (e) {
  if (e.target.classList.contains('close')) {
    const tag = e.target.parentElement;      // 选中标签
    const index = tag.dataset.index;         // 对应DD的索引
    const dd = selectedArr[index];           // 数组里存的是DD元素

    selectedBooks.removeChild(tag);          // 删除自己
    dd.classList.remove('active');           // 对应DD取消选中
    dd.parentElement.children[1].classList.add('active'); // 同组第一个选中
    delete selectedArr[index];               // 删除数组元素,但保留长度
    changePrice();                           // 价格重算
  }
});

易错点:不能把事件委托给新创建的 span;不要用 splice 把数组长度改掉,转写稿明确用 delete 保留槽位;索引要存在关闭按钮的父元素上。调试时可先 console.log(dd),确认点“128G”拿到的是对应 DD,再继续删除和取消选中。

封装价格计算函数 ​

为什么需要:点击选项、删除标签、修改数量、勾选搭配商品都会影响价格,如果每处都写一遍计算,后期必乱。怎么想:基础价从数据里取,选中项的价格变化从数组里每个 DD 的 data-change-price 累加。怎么做:创建 DD 时把 changePrice 写成自定义属性;计算时注意从 dataset 取出来是字符串,要转数字:

js
function changePrice() {
  let unitPrice = goodsDetail.price;
  selectedArr.forEach(dd => {
    unitPrice += +dd.dataset.changePrice; // + 转数字
  });

  const masterPrice = unitPrice * productNumber;
  masterPriceBox.textContent = masterPrice;
  totalPriceBox.textContent = masterPrice + collectionPrice;
}

易错点:数组里存的是 DD 元素,不是普通对象,所以取价格要用 dataset;不要忘记正负价格都可能存在;每次选择变化后都要调用 changePrice()。调试时先 console.log(unitPrice),再显示到页面,确认价格随选项增减。

数量加减与手动输入 ​

为什么需要:数量既要点加、点减,也要允许输入框手改,而且价格要跟着变。怎么想:定义一个全局 productNumber 记录数量,三个入口都同步它。怎么做:加按钮累加,减按钮累减且最小为 1;输入框用 change 事件,因为既要值改变又要失去焦点:

js
let productNumber = 1;

plusBtn.addEventListener('click', () => {
  productNumber++;
  numberInput.value = productNumber;
  changePrice();
});

minusBtn.addEventListener('click', () => {
  productNumber--;
  if (productNumber < 1) productNumber = 1;
  numberInput.value = productNumber;
  changePrice();
});

numberInput.addEventListener('change', () => {
  productNumber = +numberInput.value;
  if (isNaN(productNumber) || productNumber < 1) {
    productNumber = 1;
  }
  numberInput.value = productNumber;
  changePrice();
});

易错点:空字符串转数字是 0,会小于 1,所以要兜底为 1;不校验会出现负数或“hello”;input 事件与 change 事件触发时机不同,这里按转写稿用 change。调试时输入 -45、hello、空值,确认都回到 1。

搭配商品选择与总价 ​

为什么需要:勾选搭配商品后,总价要加上它的价格,并统计搭配数量。怎么想:给每个复选框监听 change,用 checked 判断选中还是取消;选中加 input.value,取消减。怎么做:

js
collectionInputs.forEach(input => {
  input.addEventListener('change', () => {
    if (input.checked) {
      collectionPrice += +input.value;
      collectionNumber++;
    } else {
      collectionPrice -= +input.value;
      collectionNumber--;
    }
    totalPriceBox.textContent = masterPrice + collectionPrice;
  });
});

易错点:value 是字符串,必须用 + 转数字;总价拼接时注意运算顺序,转写稿出现过多一个 0 的问题,排查后要用括号或先转数字,避免“字符串 + 数字”变成拼接。最终显示要同时刷新主商品价格区域和总价区域,搭配数量也要同步到页面。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

代码示例 ​

示例一:商品参数选项 —— 关闭选中标签元素(事件委托 + 数组同步) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>商品参数选项 - 关闭选中标签</title>
<style>
  .choose-wrap dl{display:flex;align-items:center;margin:8px 0}
  .choose-wrap dt{width:60px;color:#999}
  .choose-wrap dd{margin:0 8px 0 0;padding:4px 10px;border:1px solid #ddd;border-radius:4px;cursor:pointer}
  .choose-wrap dd.active{border-color:#e4393c;color:#e4393c}
  .selected-books{min-height:30px;margin:10px 0}
  .selected-tag{display:inline-block;padding:4px 8px;margin-right:8px;border:1px solid #e4393c;color:#e4393c;border-radius:4px}
  .selected-tag .close{margin-left:6px;font-style:normal;cursor:pointer}
</style>
</head>
<body>
<div class="choose">
  <div class="choose-wrap" id="chooseWrap"></div>
  <div class="selected-books" id="selectedBooks"></div>
</div>

<script>
  /* 1. 数据:每个选项都带自己的价格变化 changePrice */
  var goodsDetail = {
    price: 5299,
    options: [
      { title: '版本', list: [
        { name: '电信优惠版', changePrice: -300 },
        { name: '移动版',     changePrice: -500 },
        { name: '官方标配',   changePrice: 0 },
        { name: '公开版',     changePrice: 100 }
      ]},
      { title: '颜色', list: [
        { name: '黑色', changePrice: 200 },
        { name: '金色', changePrice: 0 }
      ]},
      { title: '容量', list: [
        { name: '128G', changePrice: 0 },
        { name: '256G', changePrice: 800 }
      ]}
    ]
  };

  var chooseWrap    = document.getElementById('chooseWrap');
  var selectedBooks = document.getElementById('selectedBooks');

  /* 核心数据源:数组里存的是被选中的 dd 元素,下标 = 组的索引 */
  var selectedArr = [];

  /* 2. 创建选中标签 */
  function createTag(dd, index) {
    var span = document.createElement('span');
    span.className = 'selected-tag';
    span.dataset.index = index;          // 关键:把“组索引”记在标签上
    span.innerText = dd.innerText;
    var i = document.createElement('i');
    i.className = 'close';
    i.innerText = '×';
    span.appendChild(i);
    selectedBooks.appendChild(span);
  }

  /* 3. 渲染参数选项,每组第一个默认选中 */
  goodsDetail.options.forEach(function (item, index) {
    var dl = document.createElement('dl');
    var dt = document.createElement('dt');
    dt.innerText = item.title;
    dl.appendChild(dt);

    item.list.forEach(function (opt, i) {
      var dd = document.createElement('dd');
      dd.innerText = opt.name;
      dd.dataset.index = index;                  // 组索引
      dd.dataset.changePrice = opt.changePrice;  // 价格变化
      if (i === 0) {
        dd.classList.add('active');
        selectedArr[index] = dd;
        createTag(dd, index);
      }
      dl.appendChild(dd);
    });
    chooseWrap.appendChild(dl);
  });

  /* 4. 点击选项:同组切换 + 同步数组 + 同步标签 */
  chooseWrap.addEventListener('click', function (e) {
    var dd = e.target;
    if (dd.tagName !== 'DD') return;

    var index = +dd.dataset.index;
    dd.parentNode.querySelectorAll('dd').forEach(function (el) {
      el.classList.remove('active');
    });
    dd.classList.add('active');
    selectedArr[index] = dd;

    selectedBooks.querySelectorAll('.selected-tag').forEach(function (tag) {
      if (+tag.dataset.index === index) selectedBooks.removeChild(tag);
    });
    createTag(dd, index);
  });

  /* 5. 点击叉:关闭选中标签(事件委托给包裹元素) */
  selectedBooks.addEventListener('click', function (e) {
    if (!e.target.classList.contains('close')) return;  // 只认叉

    var tag   = e.target.parentNode;   // 所在的选中标签
    var index = +tag.dataset.index;    // 拿到组索引

    selectedBooks.removeChild(tag);            // ① 删掉自己
    var dd = selectedArr[index];
    dd.classList.remove('active');             // ② 对应的 dd 取消选中
    var firstDd = dd.parentNode.children[1];   // ③ 同组第一个(children[0] 是 dt)
    firstDd.classList.add('active');
    selectedArr[index] = firstDd;              // 数组同步,长度保持不变
  });
</script>
</body>
</html>

关键行与易错点

  • span.dataset.index = index:这一行是整个联动的枢纽。标签是新建的,只有把「组索引」存到自定义属性上,删除时才能反查回数组里对应的 dd。
  • 事件必须委托给 selectedBooks(包裹元素),因为 span/i 都是动态创建的,直接绑定绑不到。
  • e.target.parentNode 才是选中标签本身,e.target 只是那个叉;用 selectedBooks.removeChild(e.target) 会报错。
  • dd.parentNode.children[1]:children[0] 是 dt,同组第一个 dd 是 children[1],别写成 children[0]。
  • 转写稿里用 delete selectedArr[index] 保留数组长度,但会留下 empty 空洞,forEach 会跳过它,导致该组第一个虽被选中却不在数组里。这里改成 selectedArr[index] = firstDd,长度不变且数据一致。

示例二:点击选项和选中标签,价格跟着变(封装 changePrice) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>商品参数选项 - 价格变化</title>
<style>
  .choose-wrap dl{display:flex;align-items:center;margin:8px 0}
  .choose-wrap dt{width:60px;color:#999}
  .choose-wrap dd{margin:0 8px 0 0;padding:4px 10px;border:1px solid #ddd;border-radius:4px;cursor:pointer}
  .choose-wrap dd.active{border-color:#e4393c;color:#e4393c}
  .selected-books{min-height:30px;margin:10px 0}
  .selected-tag{display:inline-block;padding:4px 8px;margin-right:8px;border:1px solid #e4393c;color:#e4393c;border-radius:4px}
  .selected-tag .close{margin-left:6px;font-style:normal;cursor:pointer}
  .price-box{color:#e4393c;font-size:20px}
</style>
</head>
<body>
<div class="choose">
  <div class="choose-wrap" id="chooseWrap"></div>
  <div class="selected-books" id="selectedBooks"></div>
  <div class="price-box">价格:¥<span id="price">5299</span></div>
</div>

<script>
  var goodsDetail = {
    price: 5299,
    options: [
      { title: '版本', list: [
        { name: '电信优惠版', changePrice: -300 },
        { name: '移动版',     changePrice: -500 },
        { name: '官方标配',   changePrice: 0 },
        { name: '公开版',     changePrice: 100 }
      ]},
      { title: '颜色', list: [
        { name: '黑色', changePrice: 200 },
        { name: '金色', changePrice: 0 }
      ]},
      { title: '容量', list: [
        { name: '128G', changePrice: 0 },
        { name: '256G', changePrice: 800 }
      ]}
    ]
  };

  var chooseWrap    = document.getElementById('chooseWrap');
  var selectedBooks = document.getElementById('selectedBooks');
  var priceBox      = document.getElementById('price');
  var selectedArr   = [];

  /* 1. 封装价格计算:基础价 + 所有选中项的价格变化 */
  function changePrice() {
    var price = goodsDetail.price;            // 基础价格
    selectedArr.forEach(function (dd) {
      price += +dd.dataset.changePrice;       // 前面的 + 做类型转换,不能省
    });
    priceBox.innerText = price;
    return price;
  }

  function createTag(dd, index) {
    var span = document.createElement('span');
    span.className = 'selected-tag';
    span.dataset.index = index;
    span.innerText = dd.innerText;
    var i = document.createElement('i');
    i.className = 'close';
    i.innerText = '×';
    span.appendChild(i);
    selectedBooks.appendChild(span);
  }

  goodsDetail.options.forEach(function (item, index) {
    var dl = document.createElement('dl');
    var dt = document.createElement('dt');
    dt.innerText = item.title;
    dl.appendChild(dt);
    item.list.forEach(function (opt, i) {
      var dd = document.createElement('dd');
      dd.innerText = opt.name;
      dd.dataset.index = index;
      dd.dataset.changePrice = opt.changePrice;  // 提前把价格变化存进 DOM
      if (i === 0) {
        dd.classList.add('active');
        selectedArr[index] = dd;
        createTag(dd, index);
      }
      dl.appendChild(dd);
    });
    chooseWrap.appendChild(dl);
  });

  /* 2. 点击选项 → 同步标签 → 重新算价 */
  chooseWrap.addEventListener('click', function (e) {
    var dd = e.target;
    if (dd.tagName !== 'DD') return;
    var index = +dd.dataset.index;
    dd.parentNode.querySelectorAll('dd').forEach(function (el) {
      el.classList.remove('active');
    });
    dd.classList.add('active');
    selectedArr[index] = dd;

    selectedBooks.querySelectorAll('.selected-tag').forEach(function (tag) {
      if (+tag.dataset.index === index) selectedBooks.removeChild(tag);
    });
    createTag(dd, index);

    changePrice();     // 关键调用
  });

  /* 3. 关闭选中标签 → 也要重新算价 */
  selectedBooks.addEventListener('click', function (e) {
    if (!e.target.classList.contains('close')) return;
    var tag = e.target.parentNode;
    var index = +tag.dataset.index;

    selectedBooks.removeChild(tag);
    var dd = selectedArr[index];
    dd.classList.remove('active');
    var firstDd = dd.parentNode.children[1];
    firstDd.classList.add('active');
    selectedArr[index] = firstDd;

    changePrice();     // 关键调用
  });

  /* 4. 初始化一次 */
  changePrice();
</script>
</body>
</html>

**关键行与

常见错误 ​

  1. 现象: 删除选中标签后,再点同一选项,之前删掉的选项又全部“复活”。
    原因: 只删除了 DOM,没有同步清理记录选中项的数据集合。
    修法: 删除标签时同时移除对应数据项,并用 delete 保留数组长度,避免索引错位。

  2. 现象: 价格显示成类似 2264950 的异常大数,或出现字符串拼接。
    原因: 从自定义属性取出的值是字符串,直接参与加法导致拼接。
    修法: 参与计算前统一做数字转换,如 +el.dataset.changePrice;涉及多项相加时用括号保证先算数值再加显示单位。

  3. 现象: 点击动态创建的关闭按钮没有反应。
    原因: 事件绑定在动态元素本身,元素创建前绑定或重建后失效。
    修法: 使用事件委托,绑定到稳定的父容器上,再判断 event.target 是否为关闭按钮。

  4. 现象: 修改数量或删除选项后,价格不同步;搭配商品勾选/取消后总价不对。
    原因: 价格计算逻辑分散,某些变更入口漏调用重算函数。
    修法: 统一封装价格重算函数,所有会影响价格的入口都调用同一函数。

  5. 现象: 数量输入框可输入 hello、0、负数,或清空后数量异常。
    原因: 未校验输入值,未处理 NaN、空字符串转数字为 0、小于最小值等情况。
    修法: 用 change 事件在值改变且失焦后校验;非法值统一回退为最小值 1,并回写输入框。

  6. 现象: 搭配商品取消勾选后,数量或价格没有减回去。
    原因: 只处理了选中分支,未处理取消选中分支,或加减逻辑不对称。
    修法: 用 checked 判断选中/取消,选中加价、取消减价,并同步搭配商品数量与总价显示。

练习 ​

  1. 实现“删除选中标签”功能:要求删除标签本身、取消对应选项选中、同组第一个选项恢复选中,并同步清理记录选中项的数据集合。
  2. 封装 changePrice():根据基础价、选中项价格变化、商品数量、搭配商品价格计算主商品总价和最终总价,并在所有价格变更入口调用。
  3. 修复数量输入框:输入非数字、空字符串、0、负数时,统一回退为 1;合法数字保留,并同步价格。
  4. 实现搭配商品勾选联动:勾选时增加搭配商品价格和数量,取消时减回,并同步“搭配商品数量”“最终总价”显示。
  5. 排查一个 bug:删除某个选中标签后价格未恢复原价。请写出排查顺序,并说明如何定位是数据集合、索引、类型转换还是重算函数调用问题。

原视频对照 ​

#视频时长
1商品参数选项-关闭选中标签元素15:04
2商品参数选项-点击选项和选中标签价格变化14:08
3修改商品数量价格变化17:36
4选择搭配商品19:28

小结 ​

  1. 动态生成的交互元素优先用事件委托,绑定目标要选稳定父容器。
  2. 删除选中项不是单纯删 DOM,必须同步维护数据集合和同组默认选中状态。
  3. 价格计算要收敛到一个统一函数,所有变更入口只负责触发重算。
  4. 自定义属性适合携带索引、价格变化等轻量数据,但取出后必须注意类型转换。
  5. 数量输入必须做边界校验,重点处理 NaN、空字符串、零和负数。
  6. 搭配商品的选中与取消必须成对处理,加价和减价逻辑要对称。
  7. 先保证数据状态正确,再渲染 DOM 和价格;状态同步比局部改界面更可靠。
  8. 调试价格类问题时,按“数据集合→索引对应→类型转换→重算调用”顺序排查效率最高。