各司其职
html/css/js分离,各司其职。以一个更换主题样式的按钮为例:
最开始会考虑采用DOM按钮绑定js事件的方法:
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) =>{
const body = document.body;
if(e.target.innerHTML === 'DAYMODE'){
body.style.backgroundColor = 'black';
body.style.color = 'white';
e.target.innerHTML = 'NIGHTMODE';
} else {
body.style.backgroundColor = 'white';
body.style.color = 'black';
e.target.innerHTML = 'DAYMODE';
}
})
但是这种使用js修改样式的方法会使得代码内容混乱,复用性不高。对新的需求,如要新增一个样式主题,或者新增某个主题的样式,就需要新增js代码。
所以考虑将在css中编写两种主题样式,通过js切换类名的方法:
const btn = doucument.getElmentById('modeBtn');
btn.addEventListener('click', (e)=>{
const body = document.body;
if(body.className === 'daymode'){
body.className = 'nightmode';
} else {
body.classNamw = 'daymode'
}
})
这样做比第一种方法节省了许多代码量,但是没有完全做到css和js分离,各司其职。
有一种可以只使用css更换主题的方法:在主题更换按钮前添加勾选框,使用伪类checked作为主题的标识。
<body>
<input id="modeCheckBox" type="checkBox" />
<div class="container">
<label for="modeCheckBox">{{ mode }}</label>
... <!--主体内容-->
</div>
</body>
#modeCheckBox: checked + .container {
backgrond-color: black;
color: white;
transition: all 1s;
}
组件封装
以轮播图组件为例:
-
html代码:
<!--轮播图主体--> <div id="my-slider" class="slider-list"> <ul> <li class="slider-list__item--selected"> <img src="img1.png"> </li> <li class="slider-list__item"> <img src="img2.png"> </li> <li class="slider-list__item"> <img src="img3.png"> </li> <li class="slider-list__item"> <img src="img4.png"> </li> </ul> </div> <!--轮播图底部用于控制的圆点--> <div class="slide-list__control"> <span class="slide-list__control-buttons--slected"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> </div> -
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; } .slider-list__control-buttons--selected, .slider-list__control-buttons{ background-color: grey; transition: background-color 1s; } .slider-list__control-buttons--selected{ background-color: red; transition: background-color 1s; } -
js代码:
- 定义一个轮播图类
Slider,给类定义修改状态的API - 创建轮播图类实例,将实例和
dom元素联系起来
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 controler = this.container.querySelector('.slider-list__control'); // 四个圆点个体 if(controler){ const buttons = this.controler.querySelectorAll('.slider-list__control-buttons, slider-list__control-buttons__selected'); //鼠标悬停时,轮播暂停,显示悬停索引指向的图片 controler.addEventListener('mouseover', e=>{ const ind = Array.form(buttons).indexOf(e.target); if(ind >= 0){ this.slideTo(ind); this.stop(); } }) //鼠标离开时,轮播继续 controler.addEventListener('mouseout', e=>{ this.start(); }) //自定义事件slide表示图片轮播,下面将轮播图播放和红点的显示联系起来 this.container.addEventListener('slide', e=>{ const ind = e.detail.index; const selected = controler.querySelector('.slide-list__conctrol-buttons-selected'); if(selected) selected.className = '.slide-list__control-buttons'; buttons[ind].className = '.slide-list__conctrol-buttons-selected'; }) } } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected; } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(ind){ const selected = this.getSelectedItem(); if(selected){ selected.className = '.slider-list__item' } const item = this.items[idx]; if(item){ item.className = '.slider-list__item--selected'; } // 自定义事件slide:表示轮播图在进行图片轮播的状态 const event = new CustomEvent('slide', { bubbles: true, detail: {index: ind} }); this.container.dispatchEvent(event); } slideNext(){ const currentInd = this.getSelectedItemIndex(); const nextInd = (currentInd + 1) % this.items.length; this.slideTo(nextInd); } sliderPrevious(){ const currentInd = this.getSelectedItemIndex(); const preInd = (this.items.length + currentInd - 1) % this.items.length; this.slideTo(preInd); } stop(){ clearInterval(this._timer); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle) } } //将html元素和轮播图类联系起来 const slider = new Slider('my-slider'); slider.start(); - 定义一个轮播图类
对于上面的代码,可以从三个方面进行优化:
-
插件化
组件拆卸,保留主体,其余部分抽取成可添加到主体上的插件
比如,将轮播图中的图片列表作为主体,而底部圆点抽取成插件,通过依赖注入的方式添加到整个组件中
// 将轮播图类的构造函数中创建底部圆点的部分提取出来 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)); } ... } // 对应底部圆点插件的创建方法 function pluginController(slide) { const controler = this.container.querySelector('.slider-list__control'); if (controler) { const buttons = this.controler.querySelectorAll('.slider-list__control-buttons, slider-list__control-buttons__selected'); //鼠标悬停时,轮播暂停,显示悬停索引指向的图片 controler.addEventListener('mouseover', e => { const ind = Array.form(buttons).indexOf(e.target); if (ind >= 0) { this.slideTo(ind); this.stop(); } }) //鼠标离开时,轮播继续 controler.addEventListener('mouseout', e => { this.start(); }) //自定义事件slide表示图片轮播,下面将轮播图播放和红点的显示联系起来 this.container.addEventListener('slide', e => { const ind = e.detail.index; const selected = controler.querySelector('.slide-list__conctrol-buttons-selected'); if (selected) selected.className = '.slide-list__control-buttons'; buttons[ind].className = '.slide-list__conctrol-buttons-selected'; }) } } const slider = new Slider('my-slider'); // 注册底部圆点的插件 slider.registerPlugins(pluginController); slider.start();这样写如果要自己增加其他的小插件比如前一页和后一页的按钮,就可以直接在类外新增
pluginPrevious/pluginNext方法,然后通过registerPlugins注入插件。使用组件的时候可以更自由的增删组件主体上的其他功能,而不会将组件写死,或者频繁修改类的构造函数导致类的代码冗杂。 -
模板化
在插件化的基础上,将可以动态添加(如图片列表、插件)的html部分抽象出来,在创建组件的时候再动态添加html模板,避免手动修改html代码:
<!--html中只剩下外部容器-> <!--轮播图主体--> <div id="my-slider" class="slider-list"> <!--<ul> <li class="slider-list__item--selected"> <img src="img1.png"> </li> <li class="slider-list__item"> <img src="img2.png"> </li> <li class="slider-list__item"> <img src="img3.png"> </li> <li class="slider-list__item"> <img src="img4.png"> </li> </ul>--> </div> <!--轮播图底部用于控制的圆点--> <!-- <div class="slide-list__control"> <span class="slide-list__control-buttons--slected"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> </div> -->在轮播图类中新建
render()函数用于动态生成html代码,将轮播图中的图片列表作为参数传入:class Slider{ constructor(id, opts = {images: [], cycle: 3000}){ // 轮播图容器 this.container = document.getElementById(id); // 动态生成html this.container.innerHTML = this.render(); // 获取传入的参数对象 this.options = opts; // 轮播图播放的图片 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>`; } ... } const slider = new Slider('my-slider', {images: [img1, img2, img3], cycle: 3000}); // 注册底部圆点的插件 slider.registerPlugins(pluginController); slider.start();插件动态生成html代码:
class Slider { ... 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); }); } ... }插件作为模块:
// puginController不再是一个函数 const pluginController = { // 动态生成插件html render(images){ return ` <div class="slide-list__control"> ${images.map((img, i)=> ` <span class="slide-list__control-buttons${i==0?'--selected':''}"></span> `).join('')} </div> `.trim(); }, // 实现html交互 action(slider){ const controler = this.container.querySelector('.slider-list__control'); if (controler) { const buttons = this.controler.querySelectorAll('.slider-list__control-buttons, slider-list__control-buttons__selected'); controler.addEventListener('mouseover', e => { const ind = Array.form(buttons).indexOf(e.target); if (ind >= 0) { this.slideTo(ind); this.stop(); } }) controler.addEventListener('mouseout', e => { this.start(); }) this.container.addEventListener('slide', e => { const ind = e.detail.index; const selected = controler.querySelector('.slide-list__conctrol-buttons-selected'); if (selected) selected.className = '.slide-list__control-buttons'; buttons[ind].className = '.slide-list__conctrol-buttons-selected'; }) } } }这样大大简化了html中的代码,也实现了对组件的封装,避免使用者直接修改组件的内容
-
通用组件抽象
在前面两者的基础上,可以总结出组件通用的部分并抽象出来,形成组件的模板/框架,比如:
- 组件的构成包含两部分:
Component + ComponentPlugins - 组件所需的接口包括:
constructor() + render() + registerPlugins() - 组件插件所需接口包括:
render() + action()
- 组件的构成包含两部分:
过程抽象
-
高阶函数&函数装饰器
高阶函数:
- 以函数为返回值
- 以函数为返回值
- 常用于函数装饰器
形成一个独立的作用域,可以单独给内部函数(即返回的函数)使用
function HOF0(fn) { return function(...args){ return fn.appy(this, args); } }用高阶函数进行过程抽象,常见几种应用:
-
Once限制函数只执行一次:将需要执行的函数
fn作为参数传入once函数中,在once函数内部,判断fn若不为null,则执行fn并将其设为null,否则不做操作;当第一次执行完once函数后,fn被设为null,此后就不再执行:// once函数定义 function once(fn){ return function(...args){ if(fn){ const ret = fn.apply(this, args); // 将函数设置为null fn = null; return ret; } } } // 使用once函数 const func = once(()=>{ console.log('inner'); }) func(); func(); func(); // 三次调用只打印一次'inner' -
Throttle节流函数:函数
fn规定每500毫秒可以记录一次,即在一段连续点击中,只有与上次点击间隔了500毫秒才会触发点击事件。将fn作为throttle函数的参数传入,在throttle函数中,设置timer变量(该变量在外部函数作用域中),每次触发fn时检查timer是否为null,为null则执行并间隔时间销毁timer,若不为null则不执行操作,从而实现节流:function throttle(fn, time = 500){ let timer; return function(...args){ if(timer==null){ fn.apply(this, args); timer = setTimeout(()=>{ timer = null; }, time) } } } -
Debounce防抖函数:和节流类似,但是在连续不断的点击中,节流是不断间隔时间触发点击事件,防抖是仅在最后一次触发。将触发的事件函数
fn作为参数传入debounce函数,在debounce函数内部,规定每间隔一个dur时间触发fn;但是每次执行fn函数前,都会将原有的timer清除,因此前一次触发fn的timer还没有到达时间可能被清除了,只有在最后一次触发fn时顺利执行完:function debounce(fn, dur){ dur = dur || 100; var timer; return function(){ clearTimeout(timer); timer = setTimeout(()=>{ fn.apply(this, arguments); }, dur) } } -
Consumer函数消费器(同步执行任务变为异步执行):要实现
add函数多次但是按一定时间间隔执行,将add函数作为consumer函数的参数传入,返回的是consumerAdd函数,每次执行consumerAdd函数时,都会先将“执行add”这一任务放入tasks数组中,在设置定时器(setInterval)间隔一定时间从tasks中取出一个任务并执行。设置timer==null的判断保证只有在存入第一个任务(即开始存任务那一刻)才设置一个定时器,直到所有任务执行完定时器才销毁。这里传入
this的作用是保存每个任务的上下文。因为每执行依次add都会使ref中的value改变,而每个任务执行时都是在当前最新的状态,即前一个任务执行完成了的状态下,才能执行的。所以传入this保证每次都是用最新的value值。function consumer(fn, time){ // tasks用于存储需要执行的函数,timer用于存储定时器的引用,控制函数的执行间隔 let tasks = [], timer; return function(...args){ // bind:生成一个新函数,该函数会以fn的内容执行,但this指向传入的this tasks.push(fn.bind(this, ...args)); if(timer == null){ timer = setInterval(()=>{ // shift+call:弹出第一个任务,并执行 task.shift().call(this); if(task.length <= 0){ clearInterval(timer); timer = null; } }, time) } } }; // 使用consumer函数 // 加法函数 function add(){ const v = ref.value + x; console.log(`${ref.value} + ${x} = ${v}`); return ref; }; // 每个1s执行一次加法并打印,而不是一次性将所有结果打印出来 let consumerAdd = consumer(add, 1000); const ref = {value: 0}; for(let i = 0; i <10; i++){ consumerAdd(ref, i); }; -
Iterative可迭代函数:要实现将索引为奇数的元素修改为红色,若含多个元素(为可迭代的元素数组),则将返回修改后的可迭代数组;若只有一个元素,则只返回修改后的元素。
//检查是否为可迭代对象 const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'; // iterative函数 function iterative(){ return function(subject, ...rest){ // 可迭代对象:迭代执行 if(isIterableSubject){ 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');
-
编程范式
-
命令式编程
更偏向于如何做,包括面向对象和面向过程两种
如下面实现一个按钮点击切换状态的事件:
button.onclick = function(e){ if(e.target.className == 'on'){ e.target.className = 'off'; } else { e.target.className = 'on'; } } -
声明式编程
更偏向于做什么,可扩展性更强,增删改更便捷
同样实现一个按钮点击切换状态的事件:
function toggle(...actions){ return function(...args){ // 将所有的状态放入actions队列中 // 每次点击触发点击事件,实现队列头的状态,再将队列头的状态取出放在队列未 // 实现每次点击都轮换状态的效果 let action = actions.shift(); actions.push(action); return action.apply(this, args); } } button.onclick = toggle( // 表示两种状态 evt => evt.target.className = 'off', evt => evt.target.className = 'on' // ...新增其他状态 // evt => evt.target.className = 'other' );当有新的状态需要添加时,命令式编程中需要新增其他的
if-else语句,而在声明式编程中,只需要在toggle中新增状态
-