这是我参与「第五届青训营 」笔记创作活动的第3天
一、本堂课重点内容:
JavaScript三大原则之各司其职、组件封装、过程抽象
代码质量优化
二、详细知识点介绍:
js三大原则
1.各司其职
让HTML、CSS、JavaScript职能分离
2.组件封装
好的UI组件具备正确性、拓展性、复用性
3.过程抽象
应用函数式编程思想
各司其职
主题切换
版本1
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 = '🌞';
}
});
用JavaScript控制CSS,没有做到各司其职
版本2
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
body {
transition: all 1s; //动画效果 更加自然
}
做到各司其职,但还能改进,直接用纯css实现
版本3
//html
<input id="modeCheckBox" type="checkbox">
<label id="modeBtn" for="modeCheckBox"></label> //使用for将modeBtn和modeCheckBox绑定,当点击modeBtn时,触发input的click事件
//css
#modeCheckBox {
display: none;//隐藏复选框
}
#modeCheckBox:checked + .content {//切换成夜间主题,content的颜色发生变化
background-color: black;
color: white;
transition: all 1s;
}
//:after伪类的作用就是在指定的元素内容(而不是元素本身)之后插入一个包含content属性指定内容的行内元素
#modeBtn::after {
content: '🌞';
}
#modeCheckBox:checked + .content #modeBtn::after {
content: '🌜';
}
纯CSS,利用伪类选择器,隐藏modeCheckBox,最优
组件封装
用原生 JS 写一个电商网站的轮播图
版本1
行为:api
//html
<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>
//js
class Slider{
constructor(id){//获取.slider-list__item和.slider-list__item--selected元素
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
getSelectedItem(){//获取selected元素
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex(){//获取selected的元素在items里的下标
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx){//
const selected = this.getSelectedItem();//当前选择的元素
if(selected){
selected.className = 'slider-list__item';//把当前选择的元素的ClassName改为非选择状态
}
const item = this.items[idx];//要选择的元素
if(item){
item.className = 'slider-list__item--selected';//把要选择的元素的ClassName改为选中状态
}
}
slideNext(){
const currentIdx = this.getSelectedItemIndex();//当前选中元素的index
const nextIdx = (currentIdx + 1) % this.items.length;//当前选中元素的下一个的index
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);
//验证功能,定时器每个两秒自动切换轮播图
// setInterval(() =>{
// slider.slideNext();
// },2000);
轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现
使用 CSS 绝对定位将图片重叠在同一个位置
轮播图切换的状态使用修饰符(modifier)
轮播图的切换动画使用 CSS transition
定义一个类Slider,在类里面实现api
Slider
+getSelectedItem()
+getSelectedItemIndex()
+slideTo()
+slideNext()
+slidePrevious()
版本2
行为:控制流 使用自定义事件来解耦
//html
<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>
//js
class Slider{
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
const controller = this.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
//小圆点监听mouseover事件,当鼠标放在小圆点上显示该位置的图片,并把自动播放停止
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
this.slideTo(idx);
this.stop();
}
});
//小圆点监听mouseout事件,当鼠标离开小圆点,继续自动播放
controller.addEventListener('mouseout', evt=>{
this.start();
});
//监听slide事件,将图片和小圆点设置为选中状态,来修改其样式
this.container.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
})
}
//上一页,调用slidePrevious()事件,在调用之前停止自动播放,调用后打开自动播放
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
//下一页,调用slideNext()事件,在调用之前停止自动播放,调用后打开自动播放
const next = this.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
getSelectedItem(){
let selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
//自定义事件,实现在container里监听slider事件
slideTo(idx){
let selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
let item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
slideNext(){
let currentIdx = this.getSelectedItemIndex();
let nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious(){
let currentIdx = this.getSelectedItemIndex();
let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
//定时器循环播放
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
//停止播放
stop(){
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
不够灵活?-> 改进
改进1
重构:插件化
解耦:
1.将控制元素抽取成插件
2.插件与组件之间通过依赖注入方式建立联系
//js
class Slider{
constructor(id, cycle = 3000){ //原先的构造函数里内容很多,改进后只有三行
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
}
registerPlugins(...plugins){//注册插件,注入依赖
plugins.forEach(plugin => plugin(this));
}
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';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
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);
}
addEventListener(type, handler){
this.container.addEventListener(type, handler)
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
function pluginController(slider){
const controller = slider.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt=>{
slider.start();
});
slider.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
});
}
}
function pluginPrevious(slider){//上一张
const previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
function pluginNext(slider){//下一张
const next = slider.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
当不需要底部小圆点时,可以通过删除html中的小圆点控件,同时slider.registerPlugins(/*pluginController,*/ pluginPrevious, pluginNext);即可
当需要增加插件时,如需添加一个按钮
1.在html中增加<button id="randomGet">随机图片</button>
2.在js中增加
function pluginRandomGet(slider){
randomGet.addEventListener('click',()=>{
const idx=Math.floor(slider.items.length*Math.random());//长度乘随机数然后向下取整
slider.stop();
slider.slideTo(idx);
slider.start();
})
}
3.在js中把插件pluginRandomGet注册进去
slider.registerPlugins(pluginController, pluginPrevious, pluginNext ,pluginRandomGet);
但是我还是要去修改html,能不能不去修改html?
改进2
HTML模板化
//html
<div id="my-slider" class="slider-list"></div>
//js
class Slider{
constructor(id, opts = {images:[], cycle: 3000}){
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render();//调用render()
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(){//通过slider里的render()方法生成html代码
const images = this.options.images;
const content = images.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
registerPlugins(...plugins){
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = '.slider-list__plugin';
pluginContainer.innerHTML = plugin.render(this.options.images);//调用插件里的render方法
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
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';
}
let item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
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);
}
addEventListener(type, handler){
this.container.addEventListener(type, handler);
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
const pluginController = {
render(images){//把内容渲染出来
return `
<div class="slide-list__control">
${images.map((image, i) => `
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
`).join('')}
</div>
`.trim();
},
action(slider){//完成初始化
const controller = slider.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
controller.addEventListener('mouseover', evt => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt => {
slider.start();
});
slider.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
});
}
}
};
const pluginPrevious = {
render(){
return `<a class="slide-list__previous"></a>`;
},
action(slider){
const previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
};
const pluginNext = {
render(){
return `<a class="slide-list__next"></a>`;
},
action(slider){
const previous = slider.container.querySelector('.slide-list__next');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
};
//图片数据
const slider = new Slider('my-slider', {images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
如果不需要底部的小圆点,直接把pluginController注释掉即可,不需要再去修改html
slider.registerPlugins(/*pluginController,*/pluginPrevious, pluginNext);
改进3
抽象
将组件通用模型抽象出来
//js
class Component{//组件框架
constructor(id, opts = {name, data:[]}){
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render(opts.data);
}
registerPlugins(...plugins){//注册插件
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = `.${name}__plugin`;
pluginContainer.innerHTML = plugin.render(this.options.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
render(data) {//渲染,是个抽象的方法,空的
/* abstract */
return ''
}
}
class Slider extends Component{//继承Component
constructor(id, opts = {name: 'slider-list', data:[], cycle: 3000}){
super(id, opts);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(data){//重写render方法
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
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';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
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);
}
addEventListener(type, handler){
this.container.addEventListener(type, handler);
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
const pluginController = {
render(images){
return `
<div class="slide-list__control">
${images.map((image, i) => `
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
`).join('')}
</div>
`.trim();
},
action(slider){
let controller = slider.container.querySelector('.slide-list__control');
if(controller){
let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
controller.addEventListener('mouseover', evt=>{
var idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt=>{
slider.start();
});
slider.addEventListener('slide', evt => {
const idx = evt.detail.index;
let selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
});
}
}
};
const pluginPrevious = {
render(){
return `<a class="slide-list__previous"></a>`;
},
action(slider){
let previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
};
const pluginNext = {
render(){
return `<a class="slide-list__next"></a>`;
},
action(slider){
let previous = slider.container.querySelector('.slide-list__next');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
};
const slider = new Slider('my-slider', {name: 'slide-list', data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000});//此处的name相当于id,控制相应的class名
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
总结
组件设计的原则:封装性、正确性、扩展性、复用性
实现组件的步骤:结构设计、展现效果、行为设计
三次重构:
1.插件化
2.模板化
3.抽象化(组件框架)
过程抽象
用来处理局部细节控制的一些方法
函数式编程思想的基础应用
高阶函数:以函数作为参数、 以函数作为返回值、常用于作为函数装饰器
Once 只执行一次
操作次数限制
用户疯狂点击同一个会报错,每点一次就会触发click事件,都会removeChild,而第一次就已经remove了,后面的点击就会报错Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node'
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);
});
});
修改 方法1
加上{once:true}
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);
},{once:true});
});
然而有的时候不是仅仅针对于click事件。比如向服务器get请求数据,这类多次调用都只允许一次的情况,可以再次改进
修改 方法2
可以抽象出来一个高阶函数once(),保证function只被执行一次
第一次调用的时候接受的是一个函数,不是null,会返回值。第一次调用之后fn = null,第二次调用时接受的函数为null,不会调用到里面的方法
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', once((evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
});
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象
Throttle 节流
事件触发频率太高会有一定性能的消耗,当只需要每隔几秒触发一次时可以用节流函数限制其频率
第一次调用timer为null,500ms内timer都是有值的,调用后不会执行函数如果,当500ms之后,timer为null,就可以被再次调用执行
function throttle(fn, time = 500){//传入function和频率
let timer;
return function(...args){
if(timer == null){
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, time)
}
}
}
btn.onclick = throttle(function(e){
circle.innerHTML = parseInt(circle.innerHTML) + 1;
circle.className = 'fade';
setTimeout(() => circle.className = '', 250);
});
Debounced 防抖
永远只会被调用最后一次
每次调用functin都会把timer清空,只有当不再重复调用function并且dur时间到了以后才会执行
function debounce(fn, dur){
dur = dur || 100;
var timer;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, dur);
}
}
Consumer
consumer每隔time时间去调用function
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)
}
}
}
例1
每隔一定时间输出一行计算结果
例2
每点击一次按钮就会显示+1,当快速点击后,它会间隔相同的时间显示+1,有延时显示的效果
Iterative
如果subject是可迭代的,比如list,则subject的每一个元素调用functi方法,否则只调用一次
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 setColor = iterative((el, color) => {
el.style.color = color;
});
const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');
把list中下标为奇数的元素颜色设为红色
编程范式
Toggle - 命令式
switcher.onclick = function(evt){
if(evt.target.className === 'on'){
evt.target.className = 'off';
}else{
evt.target.className = 'on';
}
}
需要添加新的样式会很复杂
Toggle - 声明式
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 = 'warn',
evt => evt.target.className = 'off',
);
需要添加新的样式,则只需添加如下即可
//css
#switcher.warn {
background-color: yellow;
}
#switcher.warn:after {
content: 'warn';
color: black;
}
#switcher.off:after {
content: 'off';
color: white;
}
//js
switcher.onclick = toggle(
evt => evt.target.className = 'warn', //加入
evt => evt.target.className = 'off',
evt => evt.target.className = 'on'
);
JavaScript 代码质量优化
交通灯状态切换
实现一个切换多个交通灯状态切换的功能
版本一
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);
})();
直接用setTimeout嵌套,很难维护
版本二(数据抽象)
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的幂
//法1:
//一直除以4
function isPowerOfFour(num) {
num = parseInt(num);
while(num > 1) {
if(num % 4) return false;
num /= 4;
}
return num === 1;
}
//法2:
//用位运算
function isPowerOfFour(num) {
num = parseInt(num);
while(num > 1) {
if(num & 0b11) return false;
num >>>=2;
}
return num === 1;
}
//法3:
/*num为二进制数,num & (num - 1)会使num中1的个数减少一个
如 ...1 & ...0 =...0
num & 0xAAAAAAAAAAAAA 即 num& 010101010..10
只要有一个偶数位上是1,结果就不是0*/
function isPowerOfFour(num){
num = parseInt(num);
return num > 0 &&
(num & (num - 1)) === 0 && //num是一个2的幂
(num & 0xAAAAAAAAAAAAA) === 0;
}
//法4:
//转换成二进制字符串,用正则表达式匹配
function isPowerOfFour(num) {
num = parseInt(num).toString(2);
return /^1(?:00)*$/.test(num);
}
洗牌
错误写法
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
console.log(shuffle(cards));
//测试
const result = Array(10).fill(0);
for(let i = 0; i < 1000000; i++) {
const c = shuffle(cards);
for(let j = 0; j < 10; j++) {
result[j] += c[j];
}
}
console.table(result);
数字越小,排在前面的概率就越大,sort交换不是均匀交换,不能做到真正意义上的乱序
正确写法
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
const c = [...cards];
for(let i = c.length; i > 0; i--) {
const pIdx = Math.floor(Math.random() * i);
[c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
}
return c;
}
console.log(shuffle(cards));
//测试
const result = Array(10).fill(0);
for(let i = 0; i < 10000; i++) {
const c = shuffle(cards);
for(let j = 0; j < 10; j++) {
result[j] += c[j];
}
}
console.table(result);
[a1-ak],每次从k中选择一个数放到前面k-1任意一个位置,概率p=(k-1)/k+1/(k-1),
每个数在自己原本的位置的概率为1/k
使用生成器
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function * draw(cards){
const c = [...cards];
for(let i = c.length; i > 0; i--) {
const pIdx = Math.floor(Math.random() * i);
[c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
yield c[i - 1];
}
}
const result = draw(cards);
console.log([...result]);
不洗牌,直接用随机数取
如果需要前三张牌,可以用如下方法,不需要把整个都洗牌,性能提升
console.log([
result.next().value,
result.next().value,
result.next().value
]);
分红包
切西瓜法
function generate(amount, count){
let ret = [amount];
while(count > 1){
//挑选出最大一块进行切分
let cake = Math.max(...ret),//用Math.max取出数组中最大值
idx = ret.indexOf(cake),
part = 1 + Math.floor((cake / 2) * Math.random()),
rest = cake - part;
ret.splice(idx, 1, part, rest);
count--;
}
return ret;
}
先把红包分成两份,从剩下的中挑选出最大的继续分,以此类推,每次选择的都是剩下的中最大的,这样就不会出现不够分的情况
每次分的都是最大的,相对均匀
抽牌法
function * draw(cards){
const c = [...cards];
for(let i = c.length; i > 0; i--) {
const pIdx = Math.floor(Math.random() * i);
[c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
yield c[i - 1];
}
}
function generate(amount, count){
if(count <= 1) return [amount];
const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
const pick = draw(cards);
const result = [];
for(let i = 0; i < count - 1; i++) {
result.push(pick.next().value);
}
result.sort((a, b) => a - b);
result.push(amount);
for(let i = result.length - 1; i > 0; i--) {
result[i] = result[i] - result[i - 1];
}
return result;
}
使用洗牌的算法,随机排序,如果要分成n分,就插入n-1个隔板,把每两个隔板之间的数相加给同一个人
相对不均匀,更加刺激
三、课后个人总结:
通过这节课的学习,对组件实现有了更深入的了解,没想到组件如此复杂,需要从结构设计、效果展示、行为设计一步步实现,还需要进行重构,包括插件化、模板化、抽象化。这都是我以前没有接触过的,收获颇丰。还学习了一些简单的JavaScript算法例子,一步步的优化也拓宽了我的思考。