这是我参与「第五届青训营」伴学笔记创作活动的第 3 天。
写好JS的一些原则
- 各司其职:HTML(内容)、CSS(样式)与JS(行为)职能分离
注意是职能分离,而不是形式上分开写!
- 组件封装:好的UI组件具备正确性、扩展性、复用性
- 过程抽象:应用函数式编程思想
各司其职
应当避免不必要的由JS直接操作样式:
- 方案A:通过设置类的方式,将样式管理交给CSS
- 方案B:纯展示类交互寻求零JS方案(采用隐藏式的复选框控件记录状态+
label的for属性绑定实际的按钮,辅以:checked伪类选择器和后代选择器判断状态来设置样式)
组件封装:以轮播图为例
- 内容结构(HTML):
- 轮播图主体是一个典型的列表结构,可以使用无序列表
<ul>元素来实现 - 轮播图的状态(显示若干个小圆点表示当前是第几个图,可以用
<span>实现) - 轮播图的切换按钮(
<a>标签实现)
- 轮播图主体是一个典型的列表结构,可以使用无序列表
- 样式表现(CSS):
- 多张图片显示在同一位置:CSS绝对定位
- 图片切换:修饰符(modifier)
- 图片切换动画(渐入渐出):CSS过渡(transition)
- 交互行为(JS):API设计应保证原子操作、职责单一、满足灵活性
- 设计类与API:
- 构造器:获取轮播图元素(根据ID获取),获取
<li>中的图片元素 - 获取当前显示的图片(选择具有指定类的图片)与图片索引(利用
indexOf()函数) - 切换至给定索引图片(获取当前显示的图片,取消设置类:显示,获取指定索引的图片,设置类:显示)
- 切换到上一张图/下一张图(类似切换至给定索引图片,但是需要处理索引+1/-1的问题,并注意对索引取模来实现循环)
- 构造器:获取轮播图元素(根据ID获取),获取
- 自定义事件:与状态绑定的行为建议使用此方法,
CustomEvent
- 设计类与API:
改进与重构:插件化
原先的组件不够灵活(例如有时候我们不需要切换按钮,有时候不需要显示轮播图的状态),同时构造函数已经超过25行有效代码,且负责的工作比较冗杂,其实控制组件的部分可以单独提取成可选的“插件”,插件与组件之间通过依赖注入的方式建立联系,即所谓的解耦。
改进与重构:模板化
如果需要改动插件,仍然需要同时修改HTML和JS,这样修改图片也不方便,为此我们可以采用“数据驱动”的方式,HTML只提供一个容器,使用JS来填充具体的数据。
改进与重构:抽象化(封装组件)
可以进一步提取重复代码,抽象出组件类,通过类继承的方式实现代码复用。这实际上就是一个简单的组件框架。
这种设计没有考虑嵌套、子组件等问题,也没有考虑CSS模板化问题(CSS仍然需要手动修改),还可以进一步扩展。
过程抽象
例如,为了实现只运行一次的效果,可以:
- 在
addEventListener()中实现时,可以附加选项{ once: true }来保证只执行一次 - 可以建立一个高阶函数
once来实现
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
高阶函数
返回函数的函数被称为高阶函数(Higher-Order Function,HOF)。
以函数为参数、返回新函数的高阶函数也被称为函数装饰器。
这类函数通常是在如下等价函数的基础上修改:
function HOF0(fn) {
return function(...args) {
return fn.apply(this, args);
}
}
常用的高阶函数
-
只执行一次(Once)
function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } -
节流(Throttle):限制触发频率,超过不再触发
function throttle(fn, time = 500){ let timer; return function(...args){ if(timer == null){ fn.apply(this, args); timer = setTimeout(() => { timer = null; }, time) } } } -
防抖动(Debounce):等到数据稳定后再触发
function debounce(fn, dur){ dur = dur || 100; // dur 是 undefined 的时候取 100 var timer; return function(){ clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, dur); } } -
消费(Consumer,例1 / 例2):生成一个执行队列,拉开每次触发的执行间隔
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):把原本只接受单个元素的函数改造为接收多个元素(每个元素依次执行原来的函数)
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]); } }
为什么需要高阶函数
- 减少重复逻辑的实现
- 降低非纯函数的测试成本(不需要为每个类似的非纯函数做测试,只需要测试基本的函数+高阶函数即可)
编程范式
- 命令式(imperative):需要告知具体怎么做
let list = [1, 2, 3, 4]; let mapl = []; for(let i = 0; i < list.length; i++) { mapl.push(list[i] * 2); }- 面向过程(procedural)
- 面向对象(object-oriented)
- 声明式(declarative):只需要说结果是什么
let list = [1, 2, 3, 4]; const double = x => x * 2; list.map(double);- 逻辑式(logic)
- 函数式(functional)
声明式相对命令式的优势
- 隐藏了实现细节
- 添加状态/数据较为容易,可扩展性强
命令式和声明式区别的例子:Toggle
- 命令式
switcher.onclick = function(evt){ if(evt.target.className === 'on') { evt.target.className = 'off'; } else { evt.target.className = 'on'; } } - 声明式
function toggle(...actions){ return function(...args){ let action = actions.shift(); actions.push(action); return action.apply(this, args); } } switcher.onclick = toggle( evt => evt.target.className = 'off', evt => evt.target.className = 'on' );