这是我参与「第四届青训营 」笔记创作活动的第2天
跟着月影学JavaScript
写好JavaScript的三个原则
- 各司其责:HTML,CSS,JavaScript三者分离,各自做自己应该做的事。
- 组件封装:一个UI组件应该拥有正确性,可扩展性以及可复用性。
- 过程抽象:利用函数闭包,函数式编程的思想。
各司其责
举个例子:深夜食堂
- 一个网页要求实现浅色深色两种模式。
版本一
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 = '';
}
});
- 优点:由JS控制元素的class标签最为合适,由CSS设置元素样式。
- 缺点:JS代码可复用性低,一次性代码。
版本三
<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>这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自 信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6] 。
</p>
</div>
</main>
</div>
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
- 优点:仅使用了HTML和CSS,没有JS代码。
总结
- HTML,CSS,JS各司其责
- 避免不必要的JS直接操作样式
- 可以利用class来表示状态
- 对于纯展示的页面可以尝试仅使用HTML和CSS实现
组件封装
举个例子:轮播图
-
使用原生的JS写一个轮播图
-
结构: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>使用HTML确定页面结构,对于轮播图可以使用无需列表实现。
-
表现:CSS
#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; }使用CSS的绝对定位将所有的列表项叠在一起,切换动画使用transition。
-
行为:JS
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);- 轮播图支持五个动作:
- getSelectedItem():获取选中的列表项
- getSelectedItemIndex():获取选中的项的下标
- slideTo(idx):跳转到某一项
- slideNext():跳转到下一项
- slidePrevious():跳转到上一项
- 轮播图支持五个动作:
-
控制流:
<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)此处控制流使用了自定义事件解耦合
改进方式
-
插件化:
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(); }); } }将slider组件通过依赖注入的方式插入插件
-
HTML模版化
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(){ 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>`; } ... }将HTML模版化,放入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 '' } }将组件通用模型抽象出来(组件框架)
总结
- 组件设计的原则:封装性,正确性,可扩展性,可复用性
- 实现组件的步骤:结构,表现,行为
- 优化:
- 插件化
- 模版化
- 抽象化(组件框架)
- 听完月影老师讲这段组件封装再结合之前学习的react框架恍然大悟理解了react的设计理念。
过程抽象
- 在实际项目开发过程中,经常会遇到很多需要大量重复或者大量相似的,并且与实际业务没什么直接关联的功能,例如前端请求次数的限制,限制某个方法只执行一次,防抖操作等等。
- 为了实现这些功能,经常会导致项目中存在大量重复代码,不仅可能会破坏业务逻辑,还大大降低了代码可读性。
- 过程抽象,将那些需要重复的操作抽象出来,利用函数闭包的语法,实现类似装饰器的效果,将业务代码加上这些功能。
例子
-
操作次数限制:一次性的点击操作。
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事件,导致在元素已经被删除后继续尝试删除导致报错。
考虑如何能让这个函数只能执行一次。
高阶函数
- 高阶函数指一个函数的参数和返回值都是一个函数的函数
- 向高阶函数中传入一个函数,做一些装饰,再将这个函数返回,通过装饰的行为给函数加上一些功能
HOF0(等价函数)
-
传入和返回的函数等价
function HOF0(fn) { return function(...args) { return fn.apply(this, args); } }
常用高阶函数
Once
-
修饰传入的函数使其只能执行一次
function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } const foo = once(() => { console.log('bar'); }); foo(); foo(); foo();可以看到调用了三次经过修饰的foo函数,但是由于第一次执行之后闭包中就将函数设置为了null,因此foo只会执行一次
Throttle
-
节流函数,修饰传入的函数使得其每过一定时间只能执行一次
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); });第一次调用throttle返回的函数时,timer为null,执行一次函数然后设置一个延迟,时间到后再把timer设置为null,也就是说在time的时间之内再次点击按钮,由于timer不为null,因此不会执行函数,这样就实现每time毫秒只能点击一次的操作。
Debounced
-
防抖函数,在鼠标不停活动的时候不调用函数,当鼠标活动后停下来一段时间后如果没有移动才触发函数
var i = 0; setInterval(function(){ bird.className = "sprite " + 'bird' + ((i++) % 3); }, 1000/10); function debounce(fn, dur){ dur = dur || 100; var timer; return function(){ clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, dur); } } 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));在debounce函数中定义了一个timer,每次调用debounce函数返回的函数时,会清除定时器之后设置一个延时执行,这样如果在延时还没有结束的时候又移动鼠标(触发函数)就会清除定时器,导致上次移动操作没有被执行,直到某次鼠标移动停止并且在dur时间内没有再次移动,小鸟才会移动位置。
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) } } }当第一次调用函数时,会把函数push到一个数组(队列)中,接着由于是第一次执行,timer为null,因此创建一个定时任务,每隔time时间取出队列一个任务进行执行,如果在此期间再次调用此函数,则会将任务加入队列,又由于timer不是null,就会在队列内等待执行。当队列所有任务都执行结束之后,timer会被置为null,这样下一次再次调用的时候就可以重新创建一个定时任务。
编程范式
-
命令式以及声明式
-
命令式
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); -
命令式更加看重问题是如何一步一步执行的,而声明式更看重语意的表达
-
在JavaScript中,允许使用声明式和命令式两种写法,一般推荐使用声明式写法
举个例子
- Toggle(开关)
命令式
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'
);
- 命令式更注重业务是如何实现的,而声明式定义了一个列表,不断循环切换这个列表的状态
- 声明式的优点:当前这个只实现了两个状态开和关的切换,而如果需要更多状态的切换,就只需要在调用toggle的时候添加项就可以了,而命令式如果想添加状态只能添加elseif,相对而言声明式更简洁。
总结
- 经过半节课的JavaScript的学习,了解到如何写好JavaScript,JS的编写理念以及不同编程范式的使用。
- 通过轮播图的案例,学习到插件的使用方法,发现了自己以前写轮播图方式的不足以及不可扩展性。