这是我参与「第四届青训营」笔记创作活动的第4天
写好JS的三个原则
- 各司其职 让HTML、CSS和JavaScript职能分离
- 组件封装 好的UI组件具备正确性、扩展性、复用性
- 过程抽象 应用函数式编程思想
1. 各司其职
Demo 深夜食堂-暗黑模式切换
(1) 版本一
用JS做了CSS的工作,没有做到各司其职,不利于维护修改
const btn = document.getElementById("app_btn")
btn.addEventListener("click", (e) => {
const body = document.body
const DARK_MODE_ON = "Dark Mode On"
const DARK_MODE_OFF = "Dark Mode Off"
if(e.target.innerHTML === DARK_MODE_ON){
body.style.backgroundColor = 'black';
// ...
}else{
// ...
}
})
(2) 版本二
通过指定类名来切换样式,做到了CSS和JavaScript的职能分离
if(body.className != 'night') {
body.className = 'night';
}
(3) 版本三
纯CSS实现,通过使用伪类选择器来完成功能。
首先在HTML中嵌入一个类型为checkbox的input组件,设置为display: none;
其次,通过使用label组件的for属性来修改这个checkbox的状态
最后,在CSS中指定一个状态性伪类+兄弟选择器,在checkbox的状态为checked的情况下选中后面类名为content的组件修改样式,从而实现暗黑模式。
选中暗黑模式时,由于该选择器最特殊,因此该选择器指定的样式会被选择。
另外通过CSS的transition属性可以实现一个简单的动画。
#mode-checkbox:checked + #app {
background-color: black;
color: white;
transition: all 1s;
}
(4) 结论
HTMLCSSJavaScript应该各司其责- 应当避免不必要的由JavaScript直接操作样式
- 可以用class来表示状态
- 可以尝试寻求纯展示类交互的零JS方案
2. 组件封装
组件是指Web页面上抽出来的一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。
Demo 轮播图
(1) 版本一 设计
结构: HTML
轮播图是一个典型的列表结构,可以使用无序列表ul元素来实现
表现: CSS
- 使用CSS的绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画可使用CSS
transition
行为: JS
用类封装Slider组件,设计API
- Slider
- +getSelectedItem() // 获取被选中的项
- +getSelectedItemIndex() // 获取被选中的项的下标
- +slideTo() // 滑到某一项
- +slideNext() // 滑到后一项
- +slidePrevious() // 滑到前一项
注: +在UML类图中表示可见性,指公开方法
另外,可以使用自定义事件来实现 控制Slider的组件的状态 和 Slider类 之间的解耦
(2) 版本二 插件化重构
版本一不够灵活:修改一处,其他地方都要改
将控制元素抽取成插件
插件与组件之间通过依赖注入的方式建立联系
即将控制插件依赖的Slider类实例注入到控制插件中
从而将控制插件与Slider类的实现解耦
方便添加或减少控制插件
(3) 版本三 模板化重构
之前的版本将无序列表和图片写死在HTML中
可以将HTML模板化,更易于扩展,做到数据驱动
(4) 版本四 组件框架
可以将组件和插件通用模型抽象出来
可以考虑将组件和插件统一用一个类来实现
从而实现父插件可以有子插件的嵌套
还可以做CSS的模板化
(5) 总结
设计的过程本身是进行一种抽象,不影响各司其职的原则
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构:插件化、模板化、抽象化(组件框架)
3. 过程抽象
过程抽象是用来处理局部细节控制的一些方法,是函数式编程思想的基础应用,如React Hooks
为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求 剥离出来,这个过程我们称为过程抽象。
比如把开门这个动作抽象出来,应用到不同的门和不同的人
Demo 简单任务列表
存在的问题:连续点击,删除子元素 的代码被执行多次
解决的方法:利用高阶函数once多次调用只执行一次
function once(fn) {
return function(...args){
// 判断是否已经执行
if(fn){
// 如果没有执行,应用参数返回结果
const result = fn.apply(this, args);
// 把传入的fn设置为null,保证执行一次
fn = null;
return result;
}
}
}
其中,once函数是一个外部闭包,once函数返回的函数是一个内部闭包
once函数的参数fn作为它返回的函数的外部环境
所以once函数返回的函数可以对fn有副作用
通过执行一次后将fn设置为null实现只执行一次
高阶函数(HOF)
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
常见的高阶函数
throttle- 截流函数
- 记录高频操作,每隔一段时间才记录一次
// 默认每隔0.5秒执行一次fn
function throttle(fn, interval = 500){
let timer;
return function(...args){
if(timer == null){
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, interval)
}
}
}
debounce- 防抖函数
- 应用场景:在线文档,键盘停止操作一段时间后自动保存等
// 默认等待时间为100毫秒
// 每执行一次包装后的fn,都会重新计时执行fn前的等待时间
function debounce(fn, duration){
// 默认参数的另一种方式
duration = duration || 100;
var timer;
return function(...args) {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, duration);
}
}
consumer- 把同步操作转换成异步,实现延时调用
- 应用场景:同时点击鼠标多次,HP减少的动画延时执行多次
// 默认间隔500毫秒
// 每执行一次包装后的fn,向任务队列push一次
// 同时函数以固定的时间间隔消费(consume)任务队列
function consumer(fn, interval = 500){
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;
}
}, interval)
}
}
}
iterative- 类似foreach
// 判断是否可迭代
const isIterable = obj => obj != null
&& typeof obj[Symbol.iterator] === 'function';
function iterative(fn) {
return function(subject, ...rest) {
// 如果subject是一个可迭代对象,如一个数组
if(isIterable(subject)){
const results = [];
// 那么对subject里能迭代到的元素一一执行fn
for (let obj of subject){
results.push(fn.apply(this, [obj, ...rest]));
}
// 返回结果数组
return results;
}
// 如果subject不可迭代,即单个对象,直接执行fn返回
return fn.apply(this, [subject, ...rest]);
}
}
为什么要使用高阶函数?
| - | |||
|---|---|---|---|
| 纯函数 | 没有副作用 是一个规范的过程 | 不会改变外部状态 | 方便测试 |
| 非纯函数 | 有副作用 是一个不规范的过程 | 会改变外部状态 | 测试开始需要初始化环境 测试结束需要销毁环境 不方便测试 |
使用高阶函数可以大大减少我们使用非纯函数的可能性
编程范式
- 命令式:我要怎么做
- 声明式:我要做什么
Demo 点击按钮切换状态
- 命令式
switcher.onclick = function(evt){
// 增加状态后一长串if-else较为冗长
if(evt.target.className === 'on'){
evt.target.className = 'off';
} else {
evt.target.className = 'on';
}
}
- 声明式
// 执行队首的事件; 执行完后将队首push到队尾
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'
)