js三个原则与案例 | 青训营笔记

94 阅读12分钟

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

写好js的三个原则

各司其责:让HTML、CSS和js职能分离

组件封装:好的IU组件具备正确性、扩展性、复用性

过程抽象:应用函数式编程思想(不仅是对数据抽象)

关于函数式编程:非常重要的编程范式

各司其责

控制样式的代码可以用纯css实现。

改进一

版本二操控body.className,一般用class定义html元素的状态。

而版本一的body-style直接操控css,用js控制css,违背各司其责。

改进二

控制样式的代码可以用纯css实现。

css伪类选择器:可以修改、匹配元素状态。

首先,通过伪类选择器,实现点击CheckBox修改content状态。

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

这里通过给CheckBox设置一个checked状态,通过checked状态修改content样式。(修改CheckBox状态,后面用兄弟节点选择器匹配到下面的内容。)

 #modeCheckBox {
   display: none;
 }

上面这个地方,把CheckBox隐藏起来。

然后,把CheckBox与label关联起来,实现点击图标也有相同效果。

 <input id="modeCheckBox" type="checkbox">
   <div class="content">
     <header>
       <label id="modeBtn" for="modeCheckBox"></label>
       <h1>深夜食堂</h1>
     </header>

这里,给label设置for属性值,与CheckBox的id相同。

于是点击label也会改变CheckBox状态,根据CheckBox兄弟节点改变content状态。

总结

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

组件封装

组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性组件设计的原则)。

例:轮播图

CSS

轮播图切换的状态使用修饰符(modifier)——这是哪里体现的?--selected吗?了解BEM的CSS命名规范(搜索 使用修饰符(modifier))

“连接修饰符则使用‘-’,.block - modifier代表.block的不同状态或不同版本。

BEM

JS

JS里是在Slider类里,创建一个构造函数并实现五个api功能

下面代码用定时调用的函数,实现两秒切换下一张图片

 setInterval(() => {
     slider.slideNext();
 }, 2000);

通过控制流添加下方可以控制的点。

对应第几个图片那么第几个小圆点呈红色,从这可以看出小圆点有状态,且与图片状态绑定。通过自定义事件进行状态绑定。

轮播图-2增添的代码(除了html的):

在构造器中,定义controller为那四个小圆点,在controller里监听事件mouseover和mouseout;监听slide事件(这是自定义事件),在slide事件里拿到当前图片index,设置对应小圆点为红色状态。定义previous和next,左滑右滑标签因为是无状态的,所以不需要注册、监听slide事件。

evt.preventDefault();这里为什么要加上阻止浏览器默认事件?

通知浏览器不执行与事件(比如click)关联的默认动作(例如,当点击提交按钮时阻止对表单的提交)

补充:preventDefault是用来阻止浏览器的默认事件,stopPropagation是用来阻止冒泡事件(父子元素之间)。

const变let

const与let的区别?

在slideTo函数中,增加开发自定义事件的过程,在items设置好后new一个customEvent,为slide事件,把事件派发出去,这样可以在controller里监听slide事件。

增加start和stop事件,在start放入之前写的setInterval定时调用函数,this.cycle调用的是构造器传入的cycle参数(this.cycle = cycle;)。

最后slider.start();运行。

小结:实现组件的基本步骤

  • 结构设计html

  • 展现效果css

  • 行为设计js

    • API(功能)设计一些接口来操作
    • Event(控制流)通常情况下用自定义事件解耦

    这样做可以实现一个基础的组件

    但是如何重构这个轮播图组件呢?(目的:解耦)

重构-插件化

  • 将控制元素抽取成插件

  • 插件与组件之间通过 **依赖注入 **方式建立联系

(轮播图-3)

前面的:控制点和组件绑定在一起,要改则html、css、js都要改,牵一发则动全身。所以这里把组件的插件抽象出来。

重构前,构造函数(constructor)有四十几行代码,不是很好的实现。

重构后,构造函数只剩三行代码,后面增加插件的api:

注册插件registerPlugins、增加事件监听addEventListener

构造函数内其他的没变化, slideTo依旧派发事件。

插件抽象出pluginController、pluginPrevious、pluginNext三个函数,把注册事件的代码放到里面(这三个函数)去,通过依赖注入的方式把slider组件传到plugin构造方法里,即在构造函数api:registerPlugins内,plugins.forEach(plugin => plugin(this));在这个this里注入入进来 。这样,插件可以控制slider,而slider不关心有哪些插件,这些插件都是通过slider.registerPlugins注册进来的。

总结:这样做两个优点——1.构造函数精简 2.某个插件功能不用时,只需要把注册插件那里注释掉(slider.registerPlugins(/*pluginController,*/ pluginPrevious, pluginNext);这样插件功能取消但模块还在),然后把html对应代码删掉就好。 3.新增插件也方便

 <button id="randomGet">手气不错</button>
 function pluginRandom(Slider) {
   randomGet.addEventListener('click', () => {
     const idx = Math.floor(slider.items.length * Math.random());
     // items长度×随机数,然后向下取整
     slider.stop();
     slider.slideTo(idx);
     slider.start();
   })
 }

重构-模板化

  • 将HTML模板化,更易于扩展

js的ui组件首先要做到数据驱动,根据数据生成html模板,不需要把图片写死到模板中去。

(轮播图-4)

要将组件和插件模板化。通过Slider的render生成代码,图片通过最后创建Slider对象时传入,代码生成通过this.container.innerHTML = this.render();调用render方法返回innerHTML并插入到容器中去。

在注册插件这里也是用的插件pluginController的render方法,传入images图像并根据传入图像决定对应的模板,把插件的模板插入到插件容器中,然后插入组件容器中。然后调用插件的action去完成插件自己的初始化。

pluginController里有两个方法:render和action。render的渲染下方小圆点。action完成初始化。

重构-抽象化

  • 将组件通用模型抽象出来

将Slider这个特定的组件模型抽象成通用的组件。通用组件里有两个抽象的方法,注册插件和渲染(抽象方法)。然后Slider就可以继承Component, SliderPlugin也可以完成渲染(模板化的内容)。

形成一个抽象通用得到模型(组件框架),支持定义一个组件,在组件里注册若干控制插件。

new Slider这里传入id,css名字前缀,data。

这种方法不好的地方:

1.没有考虑组件嵌套,因为component和plugin分开。更通用的办法是component和plugin组合起来,可以有子组件,子组件也可以作为父组件来使用。

总结

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

过程抽象

  • 用来处理局部细节控制的一些方法

  • 函数式编程思想的基础应用

无副作用的输入和输出:纯函数(不改变外界环境的函数)才有。

组件框架和MVVM框架普遍应用过程抽象,比如:react hooks (典型抽象应用,过程抽象之后,可以通过hooks去存储状态,这个状态在函数调用的scope之外,我们存储和同步的一些状态 )

例子:学习列表-Once-

让事件只能触发一次

法一:加入once()

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

法二: {once: true}

 buttons.forEach((button) => {
   button.addEventListener('click', (evt) => {
     const target = evt.target;
     target.parentNode.className = 'completed';
     setTimeout(() => {
       list.removeChild(target.parentNode);
     }, 2000);
   }, {once: true});
 });

这个是DOM事件里提供的。

向服务器get请求数据,这个数据希望只是被请求一次,要是发送多个请求,只会返回第一次请求,其他请求block掉。类似这种请求(多次调用只被允许执行一次),可以把整个过程封装成一个once的过程抽象(高阶函数)(法一),保证里面的函数只被执行一次。

高阶函数

一个函数返回另一个函数

 function once(fn) {
     // outer scope closure
     return function(...args) {
         // inner scope
       if(fn) {
         const ret = fn.apply(this, args);
         fn = null; // 把函数值设为null,防二次调用
         return ret;
       }
     }
   }

调用foo()实际上调用的是里面的函数,因为第一次fn设为null了,就永远不会第二次调用到里面的方法。

经典的过程抽象:抽象了一个once函数,它的参数是一个函数,能把里面的任意函数变成只调用一次。

过程抽象对应数据抽象

数据抽象针对实体,过程抽象针对动作(复用某个动作函数即过程抽象,可以用高阶函数实现)。

HOF

HOF0为等价高阶函数,HOF0(fn)与fn函数完全等价。一般的高阶函数都是在HOF0的基础上做一些事情,比如改变某个函数的参数、返回值等。

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

常用的高阶函数:

包括前面Once

Throttle节流函数(类库Underscore、Lodash)

防止事件触发频率过高(mousemove、scoll)。

timer = setTimeout(() => {

timer = null;

}, time)

在500ms后再把timer设为null

Debounce防抖函数

延伸:自动保存功能

Consumer

把同步调用的函数变成异步的。

2-延时调用的实现,800ms调用一次

Iterative可迭代方法

jQuery框架设计原则:批量操作(调用一个api,同时操作多个元素)

同样这里,实现批量操作。

判断subject是否是可迭代(比如list),是的话就用subject里面的每一个元素去调用fn方法,不是的话就只调用一次。

为什么用高阶函数

减少使用非纯函数的可能性

纯函数:没有副作用,结果可预期。纯函数的好处:不需要构建context就可以知道它结果是否符合预期

纯函数例子:

function add(x, y){ return x + y; } // test console.assert(add(4, 5) === 9, 'failed');

非纯函数:有副作用 function with side effect,会改变scope外面的值(外部状态),需要构建特定的环境。维护难度大。

let idx = 0; function count() { return ++idx; } // test const result = count(); // setup() 或 init() 前面环境的初始化 console.assert(result === 3, failed ${result}); // teardown() 把环境销毁掉

function setColor(el, color){ el.style.color = color; }

function setColors(els, color){ els.forEach((el) => { setColor(el, color); }) } setColors([...document.querySelecorAll('li:nth-child(2n+1)')], 'green');

测试时需要给两个非纯函数写testcase,所以用Iterative高阶函数的好处是把非纯函数变为纯函数(记得前面加上是否可迭代的判断)(setColors是一个表达式,不是函数,它调用高阶函数)

 function setColor(el, color){
     el.style.color = color;
 }
 ​
 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]);
   }
 }
 ​
 const setColors = iterative(setColor)
 setColors([...document.querySelecorAll('li:nth-child(2n+1)')], 'green');

测试:测试iterative函数(可迭代方法,批量操作)是否符合预期

 const addMany = iterative((a, b) => a + b);
 addMany([1, 2, 3, 4], 5) // 每个数加5,得6,7,8,9

为什么Iterative是纯函数:输入输出是确定的,运行逻辑只由传给它的函数决定。

编程范式

上面高阶函数的使用是一种模式。

编程范式分为两类:命令式(面向过程 / 面向对象),声明式(逻辑式 / 函数式)

声明式的代码更整洁

js数组迭代方法有map、forEach,通过这个实现声明式的代码。

命令式趋向于怎么做,声明式趋向于做什么

例子:1-命令式(和深夜食堂例子是一样的)

2-声明式(首先做一个过程抽象,定义一个toggle的高阶函数,里面接收一系列的actions,把第一个action取出来,push到列表最后一个,然后调用当前的action),方便添加逻辑分支(更强的可拓展性->3-三态)

总结:

  • 过程抽象 / HOF / 函数装饰器

  • 命令式 / 声明式

写代码最应该关注什么?

风格?效率?约定?使用场景?设计?

案例:当年的Leftpad事件

(npm模块,作用:补齐字符串长度,比如将所有数字变为5位,前面补0;参数:str字符串,len长度,ch用什么字符补齐)

优化后用repeat()代替while (),二次幂快速方法而不用循环线性地加。O(n)变O(log(n))

repeat() 类似原理:把n转为二进制数,从n的末位 依次判断n每一位的值。n>1是判断前面是否还有一位,n>>=1;是把n右移一位。(前面的依赖是最终变成string prototype方法)

   module.exports = function repeat(string, count) {
     var n = count;
     // Account for out-of-bounds indices
     if (n < 0 || n == Infinity) {
       throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
     }
 ​
     var result = '';
     while (n) {
       if (n % 2 == 1) {
         result += string; // * // - // *****
       }
       if (n > 1) {
         string += string; // ** // **** // - 
       }
       n >>= 1;
     }
     return result;
   };
   console.log(repeat('*', 5)) // 只需要循环3次

优化空间:字符串加法

改进二 是先计算出总共需要循环的次数,再循环,最后通过substring取出

注意性能

优化意义不大,考虑可读性更佳。

可读性、可拓展性、封装....

案例一:交通灯状态切换

普通

setTimeout由于异步,所以嵌套5层

数据抽象(封装)

把交通灯状态和时间等数据抽象出来,定义一个状态列表。

定义一个状态切换方法(传入html元素、状态列表),里面递归调用applyState方法。

过程抽象

定义了几个函数

wait等待

poll轮询(抽象出来),使代码更有灵活性和可拓展性

......

然后异步执行。

但是过度抽象。

异步+函数式

最佳

案例二:洗牌

错误

用Math.random决定这两位是否交换位置。

把每个位置的值相加会发现并不均等

这是因为sort随机交换不是两两位置均衡交换

正确

随机抽一张牌放最后一位,然后剩余牌随机抽放到倒数第二位...

使用生成器

抽奖,没有必要把所有牌洗完的情况下用。

yield c[i - 1];让每抽一次就有一次返回结果

console.log([...result]);全部打印

console.log(result.next().value);打印一次

console.log(result.next().value,result.next().value);

案例三:分红包

切西瓜法

细节:每人能分到最少数额0.01,否则提示钱不够分

每次切最大的那块。

O(mn)

抽牌法

把红包看成一个数列(0-9999),随机插入分隔符。

抽10份则随机抽出9个值,排序...插入值相减

O(n)

但空间复杂度比较高

最后

打好数学与算法基础

\