如何写好JavaScript | 青训营

如何写好JavaScript

写好JS的一些原则:

  1. 各司其职:让HTMLCSSJavaScript职能分离。
  2. 组件封装:好的UI组件具备正确性、扩展性、复用性.。
  3. 过程抽象:应用函数式编程思想。

原则一:各司其职

例子:

​ 深夜食堂

​ 写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。如果是你来实现,你会怎么做?

版本一

作为小白,很容易写出以下代码:

html 复制代码
<header>
  <button id="modeBtn">🌞</button>
  <h1>深夜食堂</h1>
</header>
<main>
  <div class="pic">
    <img src="./img1.png" alt="" />
  </div>
  <div class="description">
    <p>
      Lorem, ipsum dolor sit amet consectetur adipisicing elit. Tempora
      sapiente, dolor reiciendis dolorem voluptatum, odit, est tempore
      incidunt at ad consectetur nisi pariatur doloremque! Nobis debitis
      amet expedita soluta laudantium. Lorem, ipsum dolor sit amet
      consectetur adipisicing elit. Tempora sapiente, dolor reiciendis
      dolorem voluptatum, odit, est tempore incidunt at ad consectetur nisi
      pariatur doloremque! Nobis debitis amet expedita soluta laudantium.
      Lorem, ipsum dolor sit amet consectetur adipisicing elit. Tempora
      sapiente, dolor reiciendis dolorem voluptatum, odit, est tempore
      incidunt at ad consectetur nisi pariatur doloremque! Nobis debitis
      amet expedita soluta laudantium.
    </p>
  </div>
</main>
css 复制代码
body,
html {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  overflow: hidden;
}
body {
  padding: 10px;
  box-sizing: border-box;
}
div.pic img {
  width: 100%;
}
#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  background: transparent;
}
javascript 复制代码
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
    const body = document.body;
    if (e.target.innerHTML === '浅色') {
        body.style.backgroundColor = 'black';
        body.style.color = 'white';
        e.target.innerHTML = '深色';
    } else {
        body.style.backgroundColor = 'white';
        body.style.color = 'black';
        e.target.innnerHTML = '浅色';
    }
});

最终效果:

问题一:该版本有什么问题?

HTML负责结构,CSS负责样式,JS负责行为,三者应当各司其职。

​ 在该版本的代码中JS部分修改了CSS样式,也就是JS干了CSS该干的活。

​ 这在团队开发中是一种不好的行为,弊端如下:

go 复制代码
甲方如果要求修改需求(例如:夜间模式下字体要求是灰白色),需要在`JS`文档里修改很多东西。修改样式却要在`JS`文档里修改,这是很奇怪的。
团队开发中,假如你把该版本的代码交给其他人,在其他人不了解需求文档的情况下大概率看不懂该代码要实现的功能,并且在该代码上很难去扩展。

问题二:该怎么优化?

​ 见版本二。

版本二

HTML没有太大改动。

css 复制代码
body,
html {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  overflow: hidden;
}
body {
  padding: 10px;
  box-sizing: border-box;
  transition: all 1s;
}
div.pic img {
  width: 100%;
}
#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  cursor: pointer;
  background: inherit;
}

body.night {
  background-color: black;
  color: white;
  transition: all 1s;
}

#modeBtn::after {
  content: "🌞";
}

body.night #modeBtn::after {
  content: "🌙";
}
javascript 复制代码
const btn = document.getElementById("modeBtn");
  btn.addEventListener("click", (e) => {
    const body = document.body;
    if (body.className !== "night") {
      body.className = "night";
    } else {
      body.className = "";
    }
  });

问题一:这一版本比起上一版本好在哪里?

  1. 相比较于版本一,该版本更加符合各司其职的原则。
  2. JS代码更加简洁,可读性更强。

问题二:还有没有其他方案?

JS代码只是修改了样式来实现白天和夜间模式的切换,只涉及样式的改变,完全可以是通过纯CSS来实现的,这就更加符合各司其职的原则。代码见版本三。

版本三

html 复制代码
<input type="checkbox" id="modeCheckBox" />
<div class="content">
  <header>
    <label for="modeCheckBox" id="modeBtn"></label>
    <h1>深夜食堂</h1>
  </header>
  <main>
    <div class="pic">
      <img src="./img1.png" alt="" />
    </div>
    <div class="description">
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Tempora
        sapiente, dolor reiciendis dolorem voluptatum, odit, est tempore
        incidunt at ad consectetur nisi pariatur doloremque! Nobis debitis
        amet expedita soluta laudantium. Lorem, ipsum dolor sit amet
        consectetur adipisicing elit. Tempora sapiente, dolor reiciendis
        dolorem voluptatum, odit, est tempore incidunt at ad consectetur
        nisi pariatur doloremque! Nobis debitis amet expedita soluta
        laudantium. Lorem, ipsum dolor sit amet consectetur adipisicing
        elit. Tempora sapiente, dolor reiciendis dolorem voluptatum, odit,
        est tempore incidunt at ad consectetur nisi pariatur doloremque!
        Nobis debitis amet expedita soluta laudantium.
      </p>
    </div>
  </main>
</div>
css 复制代码
body,
html {
  width: 100%;
  height: 100%;
  max-width: 600px;
  padding: 0;
  margin: 0;
  overflow: hidden;
}
body {
  box-sizing: border-box;
}

.content {
  height: 100%;
  padding: 10px;
  transition: background-color 1s, color 1s;
}
div.pic img {
  width: 100%;
}
#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  cursor: pointer;
  background: inherit;
}

#modeBtn::after {
  content: "🌞";
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: "🌙";
}

#modeCheckBox {
  display: none;
}

#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

总结

  • html/css/js各司其职
  • 应当避免不必要的由js直接操作样式
  • 可以用class来表示状态
  • 纯展示类交互寻求零js方案

原则二:组件封装

组件:

​ Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。

好的组简具备封装性、正确性、扩展性、复用性。

例子:

​ 用原生JS写一个电商网站的轮播图,应该怎么实现?

html 复制代码
<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" />
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg" />
    </li>
    <li class="slider-list__item">
      <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg" />
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg" />
    </li>
  </ul>
  <a class="slide-list__next"></a>
  <a class="slide-list__previous"></a>
  <div class="slide-list__control">
    <span class="slide-list__control-buttons--selected"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
  </div>
</div>
css 复制代码
#my-slider {
  position: relative;
  width: 790px;
  height: 340px;
}

.slider-list ul {
  list-style-type: none;
  position: relative;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

.slider-list__item,
.slider-list__item--selected {
  position: absolute;
  transition: opacity 1s;
  opacity: 0;
  text-align: center;
}

.slider-list__item--selected {
  transition: opacity 1s;
  opacity: 1;
}

.slide-list__control {
  position: relative;
  display: table;
  background-color: rgba(255, 255, 255, 0.5);
  padding: 5px;
  border-radius: 12px;
  bottom: 30px;
  margin: auto;
}

.slide-list__next,
.slide-list__previous {
  display: inline-block;
  position: absolute;
  top: 50%;
  margin-top: -25px;
  width: 30px;
  height: 50px;
  text-align: center;
  font-size: 24px;
  line-height: 50px;
  overflow: hidden;
  border: none;
  background: transparent;
  color: white;
  background: rgba(0, 0, 0, 0.2);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.5s;
}

.slide-list__previous {
  left: 0;
}

.slide-list__next {
  right: 0;
}

#my-slider:hover .slide-list__previous {
  opacity: 1;
}

#my-slider:hover .slide-list__next {
  opacity: 1;
}

.slide-list__previous:after {
  content: "<";
}

.slide-list__next:after {
  content: ">";
}

.slide-list__control-buttons,
.slide-list__control-buttons--selected {
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  margin: 0 5px;
  background-color: white;
  cursor: pointer;
}

.slide-list__control-buttons--selected {
  background-color: red;
}
javascript 复制代码
window.onload = function () {
  class Slider {
    constructor(id, cycle = 3000) {
      this.container = document.getElementById(id);
      this.items = this.container.querySelectorAll(
        ".slider-list__item, .slider-list__item--selected"
      );
      this.cycle = cycle;

      const controller = this.container.querySelector(".slide-list__control");
      if (controller) {
        const buttons = controller.querySelectorAll(
          ".slide-list__control-buttons, .slide-list__control-buttons--selected"
        );
        controller.addEventListener("mouseover", (evt) => {
          const idx = Array.from(buttons).indexOf(evt.target);
          if (idx >= 0) {
            this.slideTo(idx);
            this.stop();
          }
        });

        controller.addEventListener("mouseout", (evt) => {
          this.start();
        });

        this.container.addEventListener("slide", (evt) => {
          const idx = evt.detail.index;
          const selected = controller.querySelector(
            ".slide-list__control-buttons--selected"
          );
          if (selected) selected.className = "slide-list__control-buttons";
          buttons[idx].className = "slide-list__control-buttons--selected";
        });
      }

      const previous = this.container.querySelector(".slide-list__previous");
      if (previous) {
        previous.addEventListener("click", (evt) => {
          this.stop();
          this.slidePrevious();
          this.start();
          evt.preventDefault();
        });
      }

      const next = this.container.querySelector(".slide-list__next");
      if (next) {
        next.addEventListener("click", (evt) => {
          this.stop();
          this.slideNext();
          this.start();
          evt.preventDefault();
        });
      }
    }
    getSelectedItem() {
      let selected = this.container.querySelector(
        ".slider-list__item--selected"
      );
      return selected;
    }
    getSelectedItemIndex() {
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx) {
      let selected = this.getSelectedItem();
      if (selected) {
        selected.className = "slider-list__item";
      }
      let item = this.items[idx];
      if (item) {
        item.className = "slider-list__item--selected";
      }

      const detail = { index: idx };
      const event = new CustomEvent("slide", { bubbles: true, detail });
      this.container.dispatchEvent(event);
    }
    slideNext() {
      let currentIdx = this.getSelectedItemIndex();
      let nextIdx = (currentIdx + 1) % this.items.length;
      this.slideTo(nextIdx);
    }
    slidePrevious() {
      let currentIdx = this.getSelectedItemIndex();
      let previousIdx =
        (this.items.length + currentIdx - 1) % this.items.length;
      this.slideTo(previousIdx);
    }
    start() {
      this.stop();
      this._timer = setInterval(() => this.slideNext(), this.cycle);
    }
    stop() {
      clearInterval(this._timer);
    }
  }

  const slider = new Slider("my-slider");
  slider.start();
};

总结:基本方法

  • 结构设计
  • 展现效果
  • 行为设计
    • API(功能)
    • Event(控制流)

​ 以上代码实现了轮播图的功能,但是它不够灵活,HTMLCSSJS三者的耦合性太强,可以通过以下方法降低耦合度。另外,构造函数中的代码非常的冗长,这是不好的。

重构:插件化

解耦:

css 复制代码
- 将控制元素抽取成插件
- 插件与组件之间通过<font color='red'>依赖注入</font>的方式建立联系

组件化以后的JS代码:

javascript 复制代码
window.onload = function () {
  class Slider {
    constructor(id, cycle = 3000) {
      this.container = document.getElementById(id);
      this.items = this.container.querySelectorAll(
        ".slider-list__item, .slider-list__item--selected"
      );
      this.cycle = cycle;
    }
    registerPlugins(...plugins) {
      plugins.forEach((plugin) => plugin(this));
    }
    getSelectedItem() {
      const selected = this.container.querySelector(
        ".slider-list__item--selected"
      );
      return selected;
    }
    getSelectedItemIndex() {
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx) {
      const selected = this.getSelectedItem();
      if (selected) {
        selected.className = "slider-list__item";
      }
      const item = this.items[idx];
      if (item) {
        item.className = "slider-list__item--selected";
      }

      const detail = { index: idx };
      const event = new CustomEvent("slide", { bubbles: true, detail });
      this.container.dispatchEvent(event);
    }
    slideNext() {
      const currentIdx = this.getSelectedItemIndex();
      const nextIdx = (currentIdx + 1) % this.items.length;
      this.slideTo(nextIdx);
    }
    slidePrevious() {
      const currentIdx = this.getSelectedItemIndex();
      const previousIdx =
        (this.items.length + currentIdx - 1) % this.items.length;
      this.slideTo(previousIdx);
    }
    addEventListener(type, handler) {
      this.container.addEventListener(type, handler);
    }
    start() {
      this.stop();
      this._timer = setInterval(() => this.slideNext(), this.cycle);
    }
    stop() {
      clearInterval(this._timer);
    }
  }

  function pluginController(slider) {
    const controller = slider.container.querySelector(".slide-list__control");
    if (controller) {
      const buttons = controller.querySelectorAll(
        ".slide-list__control-buttons, .slide-list__control-buttons--selected"
      );
      controller.addEventListener("mouseover", (evt) => {
        const idx = Array.from(buttons).indexOf(evt.target);
        if (idx >= 0) {
          slider.slideTo(idx);
          slider.stop();
        }
      });

      controller.addEventListener("mouseout", (evt) => {
        slider.start();
      });

      slider.addEventListener("slide", (evt) => {
        const idx = evt.detail.index;
        const selected = controller.querySelector(
          ".slide-list__control-buttons--selected"
        );
        if (selected) selected.className = "slide-list__control-buttons";
        buttons[idx].className = "slide-list__control-buttons--selected";
      });
    }
  }

  function pluginPrevious(slider) {
    const previous = slider.container.querySelector(".slide-list__previous");
    if (previous) {
      previous.addEventListener("click", (evt) => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }
  }

  function pluginNext(slider) {
    const next = slider.container.querySelector(".slide-list__next");
    if (next) {
      next.addEventListener("click", (evt) => {
        slider.stop();
        slider.slideNext();
        slider.start();
        evt.preventDefault();
      });
    }
  }

  const slider = new Slider("my-slider");
  slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
  slider.start();
};

把所有的控制流pluginController、pluginPrevious、pluginNext以插件的形式注入Slider组件中。

假如我不需要轮播图下方的四个按钮,只需要按照如下修改:

slider.registerPlugins(/*pluginController*/, pluginPrevious, pluginNext);

添加控制流也是同样的道理,非常的方便。

当我们弃用一个插件后,JS的功能已经不在了,但是HTML页面上的按钮还在,我们还要再次去修改HTML的内容,所以我们还可以进一步优化。

重构:模板化

我们将模板写死在HTML中,假如轮播图的图片数量经常的改变或者不确定该怎么办?

我们不可能说去频繁的去修改HTML,所以我们在JS中要做到数据驱动,即根据数据来生成DOM结点放入到页面中。

HTML修改如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="/index.js"></script>
    <link rel="stylesheet" href="/index.css" />
  </head>
  <body>
    <div id="my-slider" class="slider-list"></div>
  </body>
</html>

CSS不做任何修改

JS代码修改如下:

javascript 复制代码
window.onload = function () {
  class Slider {
    constructor(id, opts = { images: [], cycle: 3000 }) {
      this.container = document.getElementById(id);
      this.options = opts;
      this.container.innerHTML = this.render();
      this.items = this.container.querySelectorAll(
        ".slider-list__item, .slider-list__item--selected"
      );
      this.cycle = opts.cycle || 3000;
      this.slideTo(0);
    }
    render() {
      const images = this.options.images;
      const content = images.map((image) =>
        `
        <li class="slider-list__item">
          <img src="${image}"/>
        </li>    
      `.trim()
      );

      return `<ul>${content.join("")}</ul>`;
    }
    registerPlugins(...plugins) {
      plugins.forEach((plugin) => {
        const pluginContainer = document.createElement("div");
        pluginContainer.className = ".slider-list__plugin";
        pluginContainer.innerHTML = plugin.render(this.options.images);
        this.container.appendChild(pluginContainer);

        plugin.action(this);
      });
    }
    getSelectedItem() {
      const selected = this.container.querySelector(
        ".slider-list__item--selected"
      );
      return selected;
    }
    getSelectedItemIndex() {
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx) {
      const selected = this.getSelectedItem();
      if (selected) {
        selected.className = "slider-list__item";
      }
      let item = this.items[idx];
      if (item) {
        item.className = "slider-list__item--selected";
      }

      const detail = { index: idx };
      const event = new CustomEvent("slide", { bubbles: true, detail });
      this.container.dispatchEvent(event);
    }
    slideNext() {
      const currentIdx = this.getSelectedItemIndex();
      const nextIdx = (currentIdx + 1) % this.items.length;
      this.slideTo(nextIdx);
    }
    slidePrevious() {
      const currentIdx = this.getSelectedItemIndex();
      const previousIdx =
        (this.items.length + currentIdx - 1) % this.items.length;
      this.slideTo(previousIdx);
    }
    addEventListener(type, handler) {
      this.container.addEventListener(type, handler);
    }
    start() {
      this.stop();
      this._timer = setInterval(() => this.slideNext(), this.cycle);
    }
    stop() {
      clearInterval(this._timer);
    }
  }

  const pluginController = {
    render(images) {
      return `
        <div class="slide-list__control">
          ${images
            .map(
              (image, i) => `
              <span class="slide-list__control-buttons${
                i === 0 ? "--selected" : ""
              }"></span>
           `
            )
            .join("")}
        </div>    
      `.trim();
    },
    action(slider) {
      const controller = slider.container.querySelector(".slide-list__control");

      if (controller) {
        const buttons = controller.querySelectorAll(
          ".slide-list__control-buttons, .slide-list__control-buttons--selected"
        );
        controller.addEventListener("mouseover", (evt) => {
          const idx = Array.from(buttons).indexOf(evt.target);
          if (idx >= 0) {
            slider.slideTo(idx);
            slider.stop();
          }
        });

        controller.addEventListener("mouseout", (evt) => {
          slider.start();
        });

        slider.addEventListener("slide", (evt) => {
          const idx = evt.detail.index;
          const selected = controller.querySelector(
            ".slide-list__control-buttons--selected"
          );
          if (selected) selected.className = "slide-list__control-buttons";
          buttons[idx].className = "slide-list__control-buttons--selected";
        });
      }
    },
  };

  const pluginPrevious = {
    render() {
      return `<a class="slide-list__previous"></a>`;
    },
    action(slider) {
      const previous = slider.container.querySelector(".slide-list__previous");
      if (previous) {
        previous.addEventListener("click", (evt) => {
          slider.stop();
          slider.slidePrevious();
          slider.start();
          evt.preventDefault();
        });
      }
    },
  };

  const pluginNext = {
    render() {
      return `<a class="slide-list__next"></a>`;
    },
    action(slider) {
      const previous = slider.container.querySelector(".slide-list__next");
      if (previous) {
        previous.addEventListener("click", (evt) => {
          slider.stop();
          slider.slideNext();
          slider.start();
          evt.preventDefault();
        });
      }
    },
  };

  const slider = new Slider("my-slider", {
    images: [
      "https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
      "https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg",
      "https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg",
      "https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg",
    ],
    cycle: 3000,
  });

  slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
  slider.start();
};

以上代码就是一个完整的组件了,但是一个网页中的组件远远不止这一个,但是全都按照上面同样的方法实现的话,就会发现所有插件都有共同的方法,我们把这些方法提炼出来,封装成一个通用的抽象插件,这就实现了进一步优化。

重构:抽象化

JS代码:

javascript 复制代码
window.onload = function () {
  class Component { //抽象出的通用组件
    constructor(id, opts = { name, data: [] }) {
      this.container = document.getElementById(id);
      this.options = opts;
      this.container.innerHTML = this.render(opts.data);
    }
    registerPlugins(...plugins) {
      plugins.forEach((plugin) => {
        const pluginContainer = document.createElement("div");
        pluginContainer.className = `.${name}__plugin`;
        pluginContainer.innerHTML = plugin.render(this.options.data);
        this.container.appendChild(pluginContainer);

        plugin.action(this);
      });
    }
    render(data) {
      /* abstract */
      return "";
    }
  }

  class Slider extends Component {
    constructor(id, opts = { name: "slider-list", data: [], cycle: 3000 }) {
      super(id, opts);
      this.items = this.container.querySelectorAll(
        ".slider-list__item, .slider-list__item--selected"
      );
      this.cycle = opts.cycle || 3000;
      this.slideTo(0);
    }
    render(data) {
      const content = data.map((image) =>
        `
        <li class="slider-list__item">
          <img src="${image}"/>
        </li>    
      `.trim()
      );

      return `<ul>${content.join("")}</ul>`;
    }
    getSelectedItem() {
      const selected = this.container.querySelector(
        ".slider-list__item--selected"
      );
      return selected;
    }
    getSelectedItemIndex() {
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx) {
      const selected = this.getSelectedItem();
      if (selected) {
        selected.className = "slider-list__item";
      }
      const item = this.items[idx];
      if (item) {
        item.className = "slider-list__item--selected";
      }

      const detail = { index: idx };
      const event = new CustomEvent("slide", { bubbles: true, detail });
      this.container.dispatchEvent(event);
    }
    slideNext() {
      const currentIdx = this.getSelectedItemIndex();
      const nextIdx = (currentIdx + 1) % this.items.length;
      this.slideTo(nextIdx);
    }
    slidePrevious() {
      const currentIdx = this.getSelectedItemIndex();
      const previousIdx =
        (this.items.length + currentIdx - 1) % this.items.length;
      this.slideTo(previousIdx);
    }
    addEventListener(type, handler) {
      this.container.addEventListener(type, handler);
    }
    start() {
      this.stop();
      this._timer = setInterval(() => this.slideNext(), this.cycle);
    }
    stop() {
      clearInterval(this._timer);
    }
  }

  const pluginController = {
    render(images) {
      return `
        <div class="slide-list__control">
          ${images
            .map(
              (image, i) => `
              <span class="slide-list__control-buttons${
                i === 0 ? "--selected" : ""
              }"></span>
           `
            )
            .join("")}
        </div>    
      `.trim();
    },
    action(slider) {
      let controller = slider.container.querySelector(".slide-list__control");

      if (controller) {
        let buttons = controller.querySelectorAll(
          ".slide-list__control-buttons, .slide-list__control-buttons--selected"
        );
        controller.addEventListener("mouseover", (evt) => {
          var idx = Array.from(buttons).indexOf(evt.target);
          if (idx >= 0) {
            slider.slideTo(idx);
            slider.stop();
          }
        });

        controller.addEventListener("mouseout", (evt) => {
          slider.start();
        });

        slider.addEventListener("slide", (evt) => {
          const idx = evt.detail.index;
          let selected = controller.querySelector(
            ".slide-list__control-buttons--selected"
          );
          if (selected) selected.className = "slide-list__control-buttons";
          buttons[idx].className = "slide-list__control-buttons--selected";
        });
      }
    },
  };

  const pluginPrevious = {
    render() {
      return `<a class="slide-list__previous"></a>`;
    },
    action(slider) {
      let previous = slider.container.querySelector(".slide-list__previous");
      if (previous) {
        previous.addEventListener("click", (evt) => {
          slider.stop();
          slider.slidePrevious();
          slider.start();
          evt.preventDefault();
        });
      }
    },
  };

  const pluginNext = {
    render() {
      return `<a class="slide-list__next"></a>`;
    },
    action(slider) {
      let previous = slider.container.querySelector(".slide-list__next");
      if (previous) {
        previous.addEventListener("click", (evt) => {
          slider.stop();
          slider.slideNext();
          slider.start();
          evt.preventDefault();
        });
      }
    },
  };

  const slider = new Slider("my-slider", {
    name: "slide-list",
    data: [
      "https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
      "https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg",
      "https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg",
      "https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg",
    ],
    cycle: 3000,
  });

  slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
  slider.start();
};

这只是组价抽象的一种方法,这种方法的好处就是设计简单,任何组件都是由组件加上控制插件组成,但是也有不好的地方,没有实现父子组件嵌套,有些复杂的组件是需要嵌套的,这种情况下,需要子组件作为插件让父组件使用。

总结

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构
    • 插件化
    • 模板化
    • 抽象化(组件框架)

思考:是否还可以继续优化?

肯定是可以继续优化的,可以从以上方案的不足入手:

  1. 以上的方法组件和插件是扁平化的,父子组件之间无法依赖注入。
  2. CSS没有做模板化。

以上两个方案自己探索。

以上的重构方案是否违背了"各司其职"的原则?(用JS渲染DOM结点)

没有,"各司其职"指的是各自做各自的事,跟写的位置无关,例如Vue,在一个文档中写HTMLCSSJS,这并不违反"各司其职"的原则,在这个文档中HTML负责页面结构,CSS负责展示效果,JS负责页面行为,三者各司其职。

原则三:过程抽象

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

实现以下效果:

​ 一个简单的Todolist,点击事件完成按钮,该事项会逐渐淡出,并最终消失。

效果如下:

实现代码如下:

html 复制代码
<ul>
  <li>
    <input type="checkbox" id="test1" />
    <label for="test1">任务一:学习HTML</label>
  </li>
  <li>
    <input type="checkbox" id="test2" />
    <label for="test2">任务二:学习CSS</label>
  </li>
  <li>
    <input type="checkbox" id="test3" />
    <label for="test3">任务三:学习JavaScript</label>
  </li>
</ul>
css 复制代码
ul {
  list-style-type: none;
}

li {
  opacity: 1;
  transition: opacity 1s;
}

ul li:has(input:checked) {
  opacity: 0;
}
javascript 复制代码
window.onload = function () {
  const ul = document.querySelector("ul");
  const btn = ul.querySelectorAll("li input");
  btn.forEach((item) => {
    item.addEventListener("click", (e) => {
      const target = e.target;
      setTimeout(() => {
        ul.removeChild(target.parentNode);
      }, 1000);
    });
  });
};

以上代码就实现了这个简易的Todolist案例,但是以上代码存在一个bug,当点击完成按钮后,在该事项开始淡出,还未消失的过程中,快速多次点击,控制台报错如下:

这是因为每次点击就会开启一个定时器,定时器内是删除一个元素,第一次已经把元素删除了,以后的删除操作就找不到该元素,要解决这个问题很简单,最简单的方法就是让这个点击的回调函数只允许执行一次,为事件绑定添加{once: true}的配置参数即可解决。

这是事件回调的情况,如果是点击按钮向服务器请求数据,不能配置参数的情况那?

我们可以封装一个名为once的高阶函数,来保证传入的函数只执行一次。

代码如下:

javascript 复制代码
function once(fn) {
  return function (...args) {
    if (fn) {
      const ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  };
}

以上为了能够让"只执行一次"的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

高阶函数

HOF

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

示例:

js 复制代码
function HOFO(fn) {
  return function (...args) {
    return fn.apply(this, args);
  };
}

同时,以上示例还是一个等价高阶函数 ,即对同样的参数调用fn和调用HOFO,效果是一样的。

常用的高阶函数

  • once

  • Throttle(节流函数)

    经常在类似Mousemovescroll这样的事件上,因为这样的事件触发频率很高,在一定程度上会带来性能上的开销,有时我们不需要这么频繁的触发,我们可以用节流函数限制。

    js 复制代码
    function throttle(fn, time = 500) {
      return function (...args) {
        let timer;
        if (timer == null) {
          fn.apply(this, args);
          timer = setTimeout(() => {
            timer = null;
          }, time);
        }
      };
    }
  • debouce(防抖函数)

    可以用在像文档自动保存这样的功能上,当我不再向文档中输入内容后,等一段时间再保存。

    js 复制代码
    function debouce(fn, time = 100) {
      var timer;
      return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, time);
      };
    }
  • consumer

    把一个函数变成异步函数,延时调用的一个效果。

    js 复制代码
    function consumer(fn, time) {
      let tasks = [],
        timer;
    
      return function (...args) {
        tasks.push(fn.bind(this, ...args));
        if (timer == null) {
          timer = setInterval(() => {
            tasks.shift().call(this);
            if (tasks.length <= 0) {
              clearInterval(timer);
              timer = null;
            }
          }, time);
        }
      };
    }
  • iterative

    js 复制代码
    const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
    
    function iterative(fn) {
      return function (subject, ...rest) {
        if (isIterable(subject)) {
          const ret = [];
          for (let obj of subject) {
            ret.push(fn.apply(this, [obj, ...rest]));
          }
          return ret;
        }
        return fn.apply(this, [subject, ...rest]);
      };
    }
相关推荐
轻口味42 分钟前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
程序员_三木5 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
开心工作室_kaic7 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育7 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博7 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
温轻舟7 小时前
前端开发 之 12个鼠标交互特效上【附完整源码】
开发语言·前端·javascript·css·html·交互·温轻舟
web135085886357 小时前
2024-05-18 前端模块化开发——ESModule模块化
开发语言·前端·javascript