这是我参与「第四届青训营 」笔记创作活动的的第2天
⭐好的js书写原则
1. html css js 各司其职、职能分离
在一个好的项目里,html操控结构,css操控样式,js操控事件。 避免用js直接修改样式,仅使用修改类名的方式来修改样式, 一些简单的纯样式修改交互,我们可以使用纯css的方式来实现,例如下面的例子:
- 我们为顶部的checkbox书写checked伪类选择器样式,当该checkbox被选择,切换为深夜模式,不被选择时回到正常样式。
- 我们将右上角图标绑定为该checkbox的label,通过点击图标达到同样的效果。
- 将checkbox使用display:none隐藏,这样就可以达到使用纯css来变化样式的效果
2. 组件封装
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
简易版组件封装
以实现一个轮播图为案例,我们将该轮播图封装为一个组件用最简单的方式可以这样做:
- 准备一份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>
- 在此基础上,我们使用css来实现轮播图表现(略),使用js来实现行为
- 将该轮播图看为一个对象,即一个组件,我们使用构造函数的方式来生成这样一个轮播图,并将行为与模板相关联起来(传入模板el)
- 将轮播图行为拆分为相关API,比如,向左,向右,切换到第几张图,下标控件点击等
- 自定义一个slide事件,将图片的滚动这个状态从组件中分离出来,降低耦合性,通过监听这个滚动事件,来帮助下标圆点进行切换
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();
});
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();
这样,我们完成了一个最简单的封装,当我们需要使用该轮播图时。只需要将html模板和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;
}
//仅展示新的API
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(this));
}
}
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();
现在,我们已经对使用js的所有功能进行封装,但是html还于组件为分离的状态,我们需要将html模板也进行封装
模板化
我们直接将图片传入组件,render方法生成结构并将它们渲染出来
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结构我们还是需要自己引入
抽象化
我们将整个html模板抽象化,封装进组件里,直接进行生成,在现在的html部分,我们只需要向构造函数传入一个容器,即可获得整个轮播图。(代码略)
总结
3.过程抽象
使用函数将一个操作封装起来,需要进行操作时,直接调用相关函数。
但是在过程抽象后,这个抽象的过程中可能会存在一些问题,如操作次数限制。比如说一个按键的click事件被要求只能调用一次。解决这个问题,我们有两种方法:
- 在开启事件监听时,传入once:true的配置参数
- 调用高级函数嵌套生成一个只能被调用一次的函数
高级函数
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
那么这个被剥离出来的需求作用对象是一个函数,我们称这种需求函数为高级函数(也就是对一个原始函数进行修饰,装饰器模式)
常见高级函数有:
| 函数名 | 作用 |
|---|---|
| once | 使某函数只被调用一次 |
| throttle | 操作节流 |
| debounce | 防抖 |
| consumer | 延时表现 |
| iterative | 可迭代的方法 |
一个超形象的解释🤣
在一个库里,推荐使用纯函数,而不是非纯函数,将一些额外的操作抽象出来。这也是推荐使用高阶函数的原因
编程泛式
分为命令式(面向过程与面向对象)与声明式。 命令式强调怎么做,而声明式只管有什么状态
新函数认识
🎉自定义事件
在这里,插入一个对我来说新的知识点,自定义事件。 在js中,除了内置的click、mouseover等事件,我们还可以自定义事件
创建事件
在js中,有两种方法创建事件
1.Event() 构造函数
let myEvent = new Event(typeArg , eventInit);
其中typeArg为自定义事件名,eventInit为事件配置对象(可选),用来控制该事件是否冒泡等,配置参数如下:
| 配置参数 | 类型 | 用处 | 默认值 |
|---|---|---|---|
| bubbles | Boolean | 事件是否冒泡 | false |
| cancelable | Boolean | 该事件能否被取消 | false |
| composed | Boolean | 事件是否会在阴影根之外 | false |
2.CustomEvent() 构造函数
let myEvent = new Event(typeArg , customEventInit);
其中typeArg为自定义事件名,customEventInit为事件配置对象(可选),配置参数如下:
| 配置参数 | 类型 | 用处 | 默认值 |
|---|---|---|---|
| detail | Object | 配置项,传入与event相关的属性值 | null |
| bubbles | Boolean | 事件是否冒泡 | false |
| cancelable | Boolean | 该事件能否被取消 | false |
事件监听
当一个对象需要监听该事件时,使用addEventListener函数开启监听
obj.addEventListener(typeArg,cb);
事件触发
使用dispatchEvent触发相关事件
window.dispatchEvent(myEvent);