这是我参与「第四届青训营 」笔记创作活动的的第3天
前端小白表示js好难呐,笔记里代码部分借鉴了前辈的帖子,如有不当,敬请斧正!
写好JS的一些原则
- 各司其职:让HTML、CSS、JS职能分离。
- 组件封装:好的UI组件具备正确性、拓展性、复用性。
- 过程抽象:应用函数式编程思想。
各司其职——例子:实现网页深色和浅色模式
版本一
在js中通过点击事件,直接修改样式
//js
const btn = document.getElementById("modeBtn");
btn.addEventListener("click", (e) => {
const body = document.body;//获取页面body元素
if (e.target.innerHTML === "🌞") {
main.style.backgroundColor = "black";
main.style.color = "white";
e.target.innerHTML = "🌜";
} else {
main.style.backgroundColor = "white";
main.style.color = "black";
e.target.innerHTML = "🌞";
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>深夜食堂</title>
</head>
<body>
<header>
<button id="modeBtn">🌞</button>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/972256e1639d493eb165a5ddf111b39e~tplv-k3u1fbpfcp-zoom-1.image">
</div>
<div class="description">
<p>
这是一间……
</p>
</div>
</main>
</body>
</html>
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
body {
padding: 10px;
box-sizing: border-box;
}
div.pic img {
width: 100%;
}
#modeBtn {
font-size: 2rem;
float: right;
border: none;
background: transparent;
}
版本二
让CSS来负责样式
//js
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
在CSS中添加:
body.night {
background-color: black;
color: white;
transition: all 1s;/*过渡*/
}
#modeBtn::after {
content: '🌞';
}
body.night #modeBtn::after {
content: '🌜';
}
版本三
纯CSS版本
<!--通过修改checkbox的状态来改变样式-->
<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*/
#modeCheckBox {
display: none;
}
/*兄弟节点选择器?点击页面的时候因为label for导致input的click事件触发,通过checkbox 的伪类选择器checked来标记*/
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
结论
- HTML/CSS/JS 各司其责
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
组件封装——例子:用原生JS实现电商网站轮播图
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
结构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:
- getSelectedItem()获取当前选中的元素
- getSelectedItenIndex()获取当前选中的元素在列表内的下标
- sildeTo()silde到某index的元素上,即跳转到某一张图片
- slideNext()显示下一张
- sliderPrevious()显示上一张 js:
// 创建一个Slider类,实现这一些API
class Slider{
constructor(id){
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
// 获取选中的图片元素:通过选择器`.slider__item--selected`获得被选中的元素
getSelectedItem(){
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取选中图片的索引值:返回选中的元素在items数组中的位置。
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);
}
}
//手动调用API
const slider = new Slider {'my-slider'};
slider.slideTo(2); slider.slideNext(); slider.slidePrevious();
//或使用一个定时器
const slider = new Slider('my-slider');
setInterval(() => { slider.slideNext(); }, 2000);
控制流
接下来实现四个小圆点: 使用自定义事件解锁控制流 以下代码注释借鉴自:juejin.cn/post/700059…
<!--a标签分别表示左右-->
<a class="slide-list__next"></a>
<a class="slide-list__previous"></a>
<!--div下span表示四个小圆点-->
<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>
#my-slider{
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul{
list-style-type:none;
position: relative;
width: 100%;
height: 100%;
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;
}
.slide-list__control{
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous{
display: inline-block;
position: absolute;
top: 50%; /*定位在录播图组件的纵向中间的位置*/
margin-top: -25px;
width: 30px;
height:50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0,0,0,0.2); /*设置为半透明*/
cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/
opacity: 0; /*初始状态为透明*/
transition: opacity .5s; /*设置透明度变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0; /*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0; /*定位在slider元素的最右边*/
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: '<';
}
.slide-list__next:after {
content: '>';
}
/*下面是四个小圆点的样式,其实通过这种BEM命名规则你也能看出来*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected{
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
background-color: red;
}
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');
// 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
this.slideTo(idx);
this.stop();
}
});
// 鼠标移开小圆点,就继续开始循环轮播
controller.addEventListener('mouseout', evt=>{
this.start();
});
// 注册slide事件,将选中的图片和小圆点设置为selected状态
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';
})
}
// 点击左边小箭头,翻到前一页
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
// 点击右边小箭头,翻到后一页
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());
}
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();
结论
基本方法:
- 结构设计
- 展现效果
- 行为设计
- API(功能:设计接口)
- Event(控制流:使用自定义事件来解耦)
改进
插件化
原先的组件不够灵活,可以将组件插件化
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
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来使用各种插件
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);
}
}
//将小圆点的控制抽离成一个插件pluginController
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';
});
}
}
////将左翻页的控制抽离成插件pluginPrevious
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();
});
}
}
//将右翻页的控制抽离成插件pluginNext
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模板化,更易于拓展
<div id="my-slider" class="slider-list"></div>
class Slider{
constructor(id, opts = {images:[], cycle: 3000}){
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render();
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
//render方法,将图片放入一个images数组中,拼入列表
render(){
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);
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);
}
}
//插件中也要定义render(),action()用来初始化
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();
抽象
将组件通用模型抽象出来
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{
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){
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});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
总结
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
过程抽象——例子:操作次数限制
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
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);
});
});
const foo = once(() => {
console.log('bar');
});
foo();
foo();
foo();
bug:连续点击会报错,原因:执行一次就会被remove,下一次无法移除
- 改进一:
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事件只触发一次
});
- 改进二 once保证里面的function只被执行一次
//高阶函数:一个函数return另一个函数
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) => {
//为按钮绑定点击事件
//把过程分装成once的一个过程抽象/高阶函数!
button.addEventListener('click', once((evt) => {
const target = evt.target;
// 改变当前点击的元素样式,渐变消失
target.parentNode.className = 'completed';
// 两秒钟后删除这个元素
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
});
const foo = once(() => {
console.log('bar');
});
foo();
foo();
foo();
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
高阶函数HOF
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
HOFO(fn)与fn等价
function HOF0(fn) {
return function(...args) {
return fn.apply(this, args);
}
}
常用高阶函数
Once
Throttle节流函数
//连续点击,每500ms可记录一次
function throttle(fn, time = 500){
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);
});
Debounce防抖函数
var i = 0;
// 让鸟煽动翅膀
setInterval(function(){
bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
// 使用防抖函数
document.addEventListener('mousemove', debounce(function(evt){
var x = evt.clientX,
y = evt.clientY,
x0 = bird.offsetLeft,
y0 = bird.offsetTop;
console.log(x, y);
var a1 = new Animator(1000, function(ep){
bird.style.top = y0 + ep * (y - y0) + 'px';
bird.style.left = x0 + ep * (x - x0) + 'px';
}, p => p * p);
a1.animate();
}, 100));
Consumer
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)
}
}
}
- 逐步累加:
function add(ref, x){
const v = ref.value + x;
console.log(`${ref.value} + ${x} = ${v}`);
ref.value = v;
return ref;
}
let consumerAdd = consumer(add, 1000);
const ref = {value: 0};
for(let i = 0; i < 10; i++){
consumerAdd(ref, i);
}
- 连击异步增加(快速点击,延时执行,每隔800ms):
btn.onclick = consumer((evt)=>{
let t = parseInt(count.innerHTML.slice(1)) + 1;
count.innerHTML = `+${t}`;
count.className = 'hit';
let r = t * 7 % 256,
g = t * 17 % 128,
b = t * 31 % 128;
count.style.color = `rgb(${r},${g},${b})`.trim();
setTimeout(()=>{
count.className = 'hide';
}, 500);
}, 800)
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]);
}
}
//将改变颜色这个一次操作,经过iterative之后,传入的是可迭代对象,就会迭代对象中的所有元素进行操作
const setColor = iterative((el, color) => {
el.style.color = color;
});
const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');
使用高阶函数的好处
大大减小使用非纯函数的可能性
纯函数
输入值确定,输出值也确定
function add(a, b) {
return a + b;
}
add(2,3) // 5
add(2,3) // 5
非纯函数
每次调用结果不同,和调用次数有关
let idx=0;
function count(){
return ++idx;//会改变x
}
count();
count();
编程范式
命令式:怎么做
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);
案例:
命令式:
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'
);
声明式具有更强的可拓展性
总结
- 过程抽象 / HOF / 装饰器
- 命令式 / 声明式