03跟着月影学JavaScript | 青训营笔记

68 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的的第3天

课件: code.juejin.cn/pen/7117822437881806856

建议搭配课件阅读

如何写好JavaScript

写好JS的一些原则

  • 各司其职 让HTML, CSS, JS职能分离
  • 组件封装 好的UI组件具备正确性, 扩展性, 复用性
  • 过程抽象 应用函数式编程思想

1 各司其职

让HTML, CSS, JS职能分离

案例 - 夜间模式

版本一
 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.innerHTML = '🌞';
     }
   });

待改进: js操作样式, 做了css的工作, 降低了可维护性

版本二
  const btn = document.getElementById('modeBtn');
   btn.addEventListener('click', (e) => {
     const body = document.body;
     if(body.className !== 'night') {
       body.className = 'night';
     } else {
       body.className = '';
     }
   });

一般用class操作元素状态, 如night

还可改进: 因为需求除了点击外, 都是在操作样式, 所有可以考虑用纯CSS

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

通过伪类选择器, 和选择器的组合巧妙实现需求

结论

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

2 组件封装

好的UI组件具备正确性, 扩展性, 复用性

案例 - 轮播图

待改进改进: 不够灵活, 扩展性不强

重构: 插件化

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系
 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();
       });
     }  
   }

再改进: HTML模板化

 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>`;
     }
     ...
   }
 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 ''
     }
 }

改进还可改进的地方: CSS还需模板化, 组件嵌套问题未解决

总结

组件设计的原则

封装性、正确性、扩展性、复用性

实现组件的步骤

结构设计、展现效果、行为设计 三次重构

  • 插件化
  • 模板化
  • 抽象化(组件框架)

Q: 有破坏各司其职的原则吗?

A: 没有, 还是各司其职, 只不过在同一文件

3 过程抽象

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

高阶函数

返回函数的函数叫高阶函数

Once

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

 function once(fn) {
     return function(...args) {
       if(fn) {
         const ret = fn.apply(this, args);
         fn = null;
         return ret;
       }
     }
 }
HOF

HOF0 (等价高阶函数)

 function HOF0(fn) {
     return function(...args) {
       return fn.apply(this, args);
     }
   }

其他一半是在HOF0基础上

常用高阶函数 ⭐
  • Once
  • Throttle ⭐ 节流
 function throttle(fn, time = 500){
   let timer;
   return function(...args){
     if(timer == null){
       fn.apply(this,  args);
       timer = setTimeout(() => {
         timer = null;
       }, time)
     }
   }
 }
  • Debounce ⭐ 防抖
  • Consumer
 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 迭代
 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]);
   }
 }

纯函数: 输入相同则输出相同, 且不产生副作用的函数

高阶函数是纯函数, 方便测试

在一个库中尽量减少使用非纯函数

声明式编程扩展性更好 ⭐ (可以试着用用)