这是我参与「第四届青训营 」笔记创作活动的的第2天。本篇笔记是对第二天月影老师的 JS 课程内容总结,并结合了自己以往的学习笔记对内容,进行了一定的梳理。有意见和建议也欢迎在评论区交流讨论呀~
知识点
- 各司其职
- 组件封装
- 过程抽象
- 代码优化
注意:本笔记中不对 JS 的基础内容做过多记录,如有需要可以查阅文档
让 JS 在开发中各司其职
先回答一个问题,什么是各司其职呢?
- 使用 JS 实现页面中的交互(逻辑行为)
- 使用 CSS 实现页面中的样式(表现形式)
- 使用 HTML 实现页面结构
为了实现“各司其职”:
- 避免不必要的 JS 操作样式行为(改变元素 class 表示状态的转换)
- 纯展示类交互寻求零 JS 方案
为什么要“各司其职”
下面用一个控制网页支持深、浅色转换浏览模式的例子说明
Version 1 实现了一个深浅切换的页面
我们在 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
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
于是在这一版,我们达到了“各司其职”的基本要求—— JS 只显示地改变了内容的 className,使其匹配到了预设好的 night 样式。
这样做的优势:代码更加简介,便于代码阅读、理解业务。
那么最后的问题来了,我们是否有无 JS 的方案实现这一效果呢?
Version 3
在这里我们使用了一个 CheckBox 的“魔法”,将 <button> 元素替换成 <label> 并与 <CheckBox> 使用 for 属性绑定,点击 <label> 会改变 <CheckBox> 的 checked 属性,CSS 通过伪类原则器依据这一属性的改变,改变样式。(当然,不要忘记在 CSS 里隐藏 <CheckBox>)
如何进行组件封装
组件:Web页面中抽出来的一个个包含模板(HTML)、功能(JS)、样式(CSS)的单元。
优秀的组件应当具有封装性、正确性、扩展性、复用性
组件设计基本方法:
- 结构设计
- 展现效果
- 行为设计
- API(功能)
- Event(控制流)
重构轮播图组件
以一个轮播图的设计例子举例:
解耦方法:
插件化
- 将控制元素抽取成插件
- 插件与组件之间依赖注入
HTML 模板化
抽象方法:
抽象通用组件模型,使用插件层对组件进行封装。
使用过程抽象解决问题
过程抽象:是用来处理局部细节控制的一些方法,是函数式编程思想的基础应用。
例子:当我们设置了一个 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 的幂
- 取余、计数(性能不优秀)
- 二进制位运算实现
- 二进制转字符串,正则判断
红包算法
- 切西瓜(红包平均)
- 抽牌法(性能差,但比较随机)
参考文章
- 青训营视频与PPT课件
- 部分月影老师的代码
- JavaScript高阶函数 - 掘金 (juejin.cn)
- 【函数式编程-1】什么是函数式编程 | 纯函数 | 命令式与声明式 | 优点 - 掘金 (juejin.cn)