这是我参与「第四届青训营 」笔记创作活动的第3天
写好JS的一些原则
- 各司其职:让
HTML、CSS和JavaScript职能分离 - 组件封装:好的UI组件具备
正确性、扩展性、复用性 - 过程抽象:
应用函数式编程思想
各司其职
深夜食堂:写一段JS控制一个网页,让它支持浅色和深色两种浏览模式。
版本一:
const btn = document.getElementById('modeBtn');
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来控制CSS的改变。
版本二:
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
提前在CSS文件中设置好类名为night的样式,通过修改类名来做到改变颜色。
版本三:
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。
</p>
</div>
</main>
</div>
设置一个checkbox,将其隐藏并使用label进行绑定,编写checkbox被选中后状态的样式,通过点击即可改变,不通过JS来控制CSS。
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
结论:
- HTML/CSS/JS各司其职
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
组件封装
用原生 JS 写一个电商网站的轮播图。
结构:HTML
轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现。
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png">
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg">
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg">
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg">
</li>
</ul>
</div>
表现:CSS
- 使用 CSS 绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用 CSS transition
#my-slider{
position: relative;
width: 790px;
}
.slider-list ul{
list-style-type:none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected{
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected{
transition: opacity 1s;
opacity: 1;
}
行为:JS
构造函数实现轮播图的轮播效果,API:
class Slider{
constructor(id){
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
getSelectedItem(){
//得到当前选中的元素
const selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex(){
//得到当前选中的元素的下标
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx){
//跳转到哪张图片
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
}
slideNext(){
//下一张图片
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious(){
//上一张图片
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
}
const slider = new Slider('my-slider');
slider.slideTo(3);
轮播图左右键及轮播图下方圆点,通过自定义事件来解耦
<a class="slide-list__next"></a>
<a class="slide-list__previous"></a>
<div class="slide-list__control">
<span class="slide-list__control-buttons--selected"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
</div>
注册一个自定义事件,来实现圆点随轮播图变化
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
总结:基本方法
- 结构设计
- 展现效果
- 行为设计
- API(功能)
- Event(控制流)
改进空间
缺点:构造函数内代码过多
改进一:重构:插件化
改进二:重构:模版化
改进三:组件框架
总结
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
例:操作次数限制
- 一些异步交互
- 一次性的HTTP请求
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);
});
});
点击后删除
li标签,有2000毫秒的过渡动画,但如果短时间内点击多次,click事件被多次触发,第一次removeChild将li标签删除,之后的removeChild找不到对应节点,报出错误。
高阶函数
为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
高阶函数意为:
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
function HOF0(fn) {
return function(...args) {
return fn.apply(this, args);
}
}
once仅执行一次函数
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。执行一次后将fn设置为null,由于这是一个闭包函数,第二次执行时fn还是null,则不会再次执行。
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
Throttle节流函数
首先创建一个timer,第一次执行时直接执行返回的函数,然后将time人设置为null,第二次执行如果时间不到500ms,setTimeout函数未执行,则不会执行if语句内部。
function throttle(fn, time = 500){
let timer;
return function(...args){
if(timer == null){
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, time)
}
}
}
3.Debounce防抖函数
传入一个时间,在时间结束后执行函数,而在这个时间内重复点击,则执行clearTimeout函数,重新设置setTimeout重新计时。
function debounce(fn, dur){
dur = dur || 100;
var timer;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, dur);
}
}
为什么要使用高阶函数
有一种函数叫做纯函数,它的定义是传入一个a他会固定输出一个b,就是说输入输出是一对一的。
纯函数的好处:方便测试,非纯函数由于在调用的时候有时会改变外界的值,所以在执行时需要构建好外部环境,非纯函数测试难度是比纯函数更高的,成本更大,系统中非纯函数越多,系统的可维护性更差。
编程范式
- 命令式:面向过程、面向对象
- 声明式:逻辑式、函数式
例:实现将数组中每个数乘以二
- 命令式:命令式更加强调怎么做
let list = [1, 2, 3, 4];
let mapl = [];
for(let i = 0; i < list.length; i++) {
mapl.push(list[i] * 2);
}
- 声明式:声明式使用方法直接实现
let list = [1, 2, 3, 4];
const double = x => x * 2;
list.map(double);
声明式的函数要比命令式更加简洁。
举例
实现一个多个交通信号灯的切换功能
- 版本一
由于setTimeout是异步的,所以需要将其嵌套,但这样的代码很丑并且很难维护。
const traffic = document.getElementById('traffic');
(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);
})();
- 版本二:数据抽象
设置好三种状态,将数据抽象出来,生成一个状态列表stateList,定义一个start方法,对状态进行切换,applyState方法是一个递归调用。
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);
- 版本三:过程抽象
首先定义一个wait函数表示等待多久时间,再定义一个poll方法代表一个轮循,再利用setState方法设置类名并设置等待时间。利用过程抽象会写很多代码,但好处是抽象出了一个轮循方法可以处理其他问题。
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();
}
}());
- 版本四:异步+函数式
利用async及await写出最简单也是最容易理解的方式
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的幂
- 版本一
最普通的方法,4的幂一直除以四最终是会等于一的,用while循环执行
while(num > 1) {
if(num % 4) return false;
num /= 4;
}
return num === 1;
}
- 版本二
对版本一进行一定修改,版本一是一直除以四,这里是向右偏移,原理相同
function isPowerOfFour(num) {
num = parseInt(num);
while(num > 1) {
if(num & 0b11) return false;
num >>>=2;
}
return num === 1;
}
- 版本三
已知一个数若是4的幂,那它一定是2的幂,前两个判断它是2的幂。(num & (num - 1)) === 0这个判断会使数字转换为二进制后从末尾减少一个一,已知2的幂的二进制数是只有一个一其他都是零,所以这个式子可以判断数字为2的幂。(num & 0xAAAAAAAAAAAAA) === 0,已知4的幂转换为二进制数后是一个一后面跟偶数个零,而0xAAAAA转换为二进制数是1010101010,进行&运算后,如果偶数位都是零,则最终结果为零,所以这个式子可以判断是否是4的幂。
function isPowerOfFour(num){
num = parseInt(num);
return num > 0 && (num & (num - 1)) === 0 && (num & 0xAAAAAAAAAAAAA) === 0;
}
- 版本四
可以直接用正则匹配二进制数是否是一个一后面跟着偶数个零
function isPowerOfFour(num) {
num = parseInt(num).toString(2);
return /^1(?:00)*$/.test(num);
}