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

122 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的的第2天。本篇笔记是对第二天月影老师的 JS 课程内容总结,并结合了自己以往的学习笔记对内容,进行了一定的梳理。有意见和建议也欢迎在评论区交流讨论呀~

知识点

  • 各司其职
  • 组件封装
  • 过程抽象
  • 代码优化

注意:本笔记中不对 JS 的基础内容做过多记录,如有需要可以查阅文档

让 JS 在开发中各司其职

先回答一个问题,什么是各司其职呢?

image.png

  • 使用 JS 实现页面中的交互(逻辑行为)
  • 使用 CSS 实现页面中的样式(表现形式)
  • 使用 HTML 实现页面结构

为了实现“各司其职”:

  • 避免不必要的 JS 操作样式行为(改变元素 class 表示状态的转换)
  • 纯展示类交互寻求零 JS 方案

为什么要“各司其职”

下面用一个控制网页支持深、浅色转换浏览模式的例子说明

Version 1 实现了一个深浅切换的页面

code.juejin.cn/pen/7108183…

我们在 JS 里直接将 body 的样式与按钮内容进行了改变,这样的方式合适吗?

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 在显示地改变样式。

因此,我们就有了 Version 2

code.juejin.cn/pen/7108183…

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

于是在这一版,我们达到了“各司其职”的基本要求—— JS 只显示地改变了内容的 className,使其匹配到了预设好的 night 样式。

这样做的优势:代码更加简介,便于代码阅读、理解业务。

那么最后的问题来了,我们是否有无 JS 的方案实现这一效果呢?

Version 3

code.juejin.cn/pen/7108184…

在这里我们使用了一个 CheckBox 的“魔法”,将 <button> 元素替换成 <label> 并与 <CheckBox> 使用 for 属性绑定,点击 <label> 会改变 <CheckBox>checked 属性,CSS 通过伪类原则器依据这一属性的改变,改变样式。(当然,不要忘记在 CSS 里隐藏 <CheckBox>

如何进行组件封装

组件:Web页面中抽出来的一个个包含模板(HTML)、功能(JS)、样式(CSS)的单元。

优秀的组件应当具有封装性、正确性、扩展性、复用性

组件设计基本方法:

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

重构轮播图组件

以一个轮播图的设计例子举例:

code.juejin.cn/pen/7108187…

解耦方法:

插件化

  • 将控制元素抽取成插件
  • 插件与组件之间依赖注入

code.juejin.cn/pen/7108191…

HTML 模板化

code.juejin.cn/pen/7108191…

抽象方法:

抽象通用组件模型,使用插件层对组件进行封装。

code.juejin.cn/pen/7108185…

使用过程抽象解决问题

过程抽象:是用来处理局部细节控制的一些方法,是函数式编程思想的基础应用。

例子:当我们设置了一个 button 但这个按钮只能执行一次时。以下的代码,因为移除按钮元素是异步执行的,如果多次点击,后面点击执行的异步操作会因为当前DOM元素已被移除而报错。

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', (evt) => {
  const target = evt.target;
  target.parentNode.className = 'completed';
  setTimeout(() => {
    list.removeChild(target.parentNode);
  }, 2000);
});
});

解决方法:在click事件后添加 {once: true}

虽然 DOM 为我们提供了事件仅执行一次的 API,那自定义的函数或者事件呢?

高阶函数 HOF

如果一个函数可以作为另一个函数的参数传入,或者该函数反回一个函数,那么这个函数就被称为高阶函数(Higher Order Function)。——JavaScript高阶函数 - 掘金 (juejin.cn)

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

常见的高阶函数

  • Once
  • Throttle 节流
  • Debounce 防抖
  • Consumer
  • Iterative
  • Array.prototype.filter() 等接收函数作为参数的函数

函数式编程

编程范式命令式声明式的区别。

命令式:让代码去做一件事,并告诉他怎么去做。

let array = [1,2,3];
for(i=0;i<array.length;i++)
  console.log(array[i]);

声明式:让代码去做一类事(做什么),具体的方法(如何去做)被抽象到高阶函数中。

let array = [1,2,3];
array.forEach((e)=>console.log(e);)

优势:声明式更加简介,使用纯函数不会改变外部环境也不依赖外部变量

代码优化

想要对代码进行优化,那么 什么是优秀的代码

代码优化方向:

  • 风格
  • 效率
  • 约定
  • 使用场景
  • 设计
// 判断一个mat2d矩阵是不是单位矩阵
function isUnit(m) {
  return m[0] === 1 && m[1] === 0 && m[2] === 0
    && m[3] === 1 && m[4] === 0 && m[5] === 0;
}

上面这段代码好吗?好又不好

  • 不好:通用性差,不严谨
  • 好:这是一段用于图形系统 Spritejs 的源码,性能高,资源消耗小

因此,代码是否优秀是由 使用场景 所决定的。

Leftpad npm 模块优化

为字符串 str 左端用 ch 补全至 len 位。

function leftpad(str, len, ch) {
  str = "" + str;
  const padLen = len - str.length;
  if(padLen <= 0) {
    return str;
  }
  return (""+ch).repeat(padLen)+str;
} 

效率低,但通用,使用场景性能优化空间有限

  • 快速幂算法优化
  • 基于底层字符串拼接算法优化

交通灯状态切换

  • 使用异步函数叠加实现切换,不够优雅,代码难读且难维护。
(function reset(){
  traffic.className = 's1';
  setTimeout(function(){
      traffic.className = 's2';
      setTimeout(function(){
        traffic.className = 's3';
        setTimeout(function(){
          traffic.className = 's4';
          setTimeout(function(){
            traffic.className = 's5';
            setTimeout(reset, 1000)
          }, 1000)
        }, 1000)
      }, 1000)
  }, 1000);
})();
  • 数据抽象实现
const traffic = document.getElementById('traffic');

const stateList = [
  {state: 'wait', last: 1000},
  {state: 'stop', last: 3000},
  {state: 'pass', last: 3000},
];

function start(traffic, stateList){
  function applyState(stateIdx) {
    const {state, last} = stateList[stateIdx];
    traffic.className = state;
    setTimeout(() => {
      applyState((stateIdx + 1) % stateList.length);
    }, last)
  }
  applyState(0);
}

start(traffic, stateList);
  • 过程抽象实现
const traffic = document.getElementById('traffic');

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}

async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}

let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 3000),
                            setState.bind(null, 'pass', 3000));

(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());
  • 异步函数 + 函数式
const traffic = document.getElementById('traffic');

function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}

function setState(state){
  traffic.className = state;
}

async function start(){
  //noprotect
  while(1){
    setState('wait');
    await wait(1000);
    setState('stop');
    await wait(3000);
    setState('pass');
    await wait(3000);
  }
}

start();

判断是否是 4 的幂

  • 取余、计数(性能不优秀)
  • 二进制位运算实现
  • 二进制转字符串,正则判断

红包算法

  • 切西瓜(红包平均)
  • 抽牌法(性能差,但比较随机)

参考文章