写好JavaScript的原则
这是我参与「第四届青训营」笔记创作活动的的第4天!
1. 各司其职
-
结构、表现、行为分离
-
三种方案改进:
a. JS直接操作DOM修改样式
b. JS只更改CSS类名
c. 纯CSS实现(结合checkbox的伪类checked + label的for)
2. 组件封装
对于一个好的组件而言,具有封装性、正确性、扩展性、复用性
结构设计 👉 展示效果 👉 行为设计(Api设计、Event控制流)
-
依赖注入 + 插件封装
-
HTML的模板化 => 更利于扩展
-
抽象成模型
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>
<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>
</div>
(1)原始想法: 通过API的方式,封装Slide类的行为
Slide
- getSelectedItem () // 获取当前的元素
- getSelectedIndex () // 获取当前选中的索引值 - 内部调用getSelectedItem(), 找到元素
- slideTo (index) // 滑动到指定索引 - 内部调用getSelectedIndex(), 清除状态
- slideNext () // 滑动到下一个 - 内部调用SlideTo()
- slidePrev () // 滑动到上一个 - 内部调用SlideTo()
class Slide {
constructor (id) {
this.container = document.getElementById(id)
// 不一定要通过document进行querySelector
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected')
// 记录当前的元素长度
this.len = this.items.length
const prev = this.container.querySelector('.slide-list__previous')
const next = this.container.querySelector('.slide-list__next')
if (prev) {
prev.addEventListener ('click', event => {
this.slidePrev()
event.preventDefault()
})
}
if (next) {
next.addEventListener ('click', event => {
this.slideNext()
event.preventDefault()
})
}
}
getSelectedItem () {
return this.container.querySelector('.slider-list__item--selected')
}
getSelectedIndex () {
return Array.from(this.items).indexOf(this.getSelectedItem())
}
slideTo (index) {
this.items[this.getSelectedIndex()].className = 'slider-list__item'
const target = this.items[index]
target.className = 'slider-list__item--selected'
}
slideNext () {
let index = this.getSelectedIndex()
this.slideTo(++index % this.len)
}
slidePrev () {
let index = this.getSelectedIndex()
this.slideTo((this.len + (--index)) % this.len)
}
}
const slide = new Slide('my-slider')
(2)解耦: 如果要加入底部的小点点进行联合控制,则会出现两份代码耦合在一起的现象,通过自定义事件的方式,进行解耦
// 在slideTo() 方法中加入如下自定义事件
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
// 则可以在底部小点点的地方进行事件监听,从而更新状态
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';
})
// 在小点点事件onmouseover事件中,则可以通过直接调用Slide的slideTo()方法更新状态
前述两种方式都过于依赖内部代码,有没有一种可以扩展性更高的代码呢?这就要提到插件封装了~
(3)插件化: 将控制元素抽取成插件,使得插件和组件以依赖注入的方式建立联系
在Slide类中新增一个方法:
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(this));
}
在构造完Slide实例后,以slider.registerPlugins(pluginController, pluginPrevious, pluginNext);方式进行插件注册,从而实现较高程度的解耦
(4)模板化: 我们同样可以将HTML部分进行解耦,以render方式进行渲染
- 在HTML部分就可以简化成如下内容:
<div id="my-slider" class="slider-list"></div>
- 对于Silder类,本身有一个render方法如下:
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>`;
}
- 可以通过在构造函数中进行render方法的调用,以如下的方式进行参数传递
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中的插件则以分成两个部分 ①
render方法 ②action方法,在插件注册的过程中调用这两个方法
具体的调用方法还是如前面的插件化一样,只是多了一个render方法
(4)抽象化: 将Slide中能公用部分抽象出来成为一个Component通用组件,适用于更多的场景
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, 3000) {
super(id, opts);
...
}
...
}
3. 过程抽象 - 函数式编程
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
常用的高阶函数
Once: 只执行一次的函数,依赖于闭包,返回一个新函数,原函数位于闭包作用域内,在第一次执行完之后,就将闭包作用域内的fn设为空,使其无法再次执行
/* * * * * *
- Once -
* * * * * */
function once(fn) {
return function (...args) {
if (fn) {
let result = fn.apply(this, args);
fn = null;
return result;
}
}
}
/* * * * * *
- Test -
* * * * * */
const foo = once(() => {
console.log('bar');
});
foo();
foo();
foo();
Throttle: 节流函数,在规定时间内只让第一次生效,如果已经执行了该事件,在该段时间内不再触发该事件,比如鼠标的scroll
/* * * * * * * * *
- Throttle -
* * * * * * * * */
function Throttle (fn, delay) {
let timer;
return function (...args) {
if (!timer) {
fn.call(this, args)
timer = setTimeout(() => {
timer = null
}, delay)
}
}
}
const buy = document.querySelector('#buyIt')
function buyFn (msg) {
console.log("I'm buy it now! ", msg)
}
buy.addEventListener('click', Throttle(buyFn, 2000))
Debounce: 防抖函数,在规定时间内只让最后一次生效,每次触发都会引起重新计时,比如搜索框的防抖(只最最后一次变更进行搜索请求,自动保存(最后一次编辑变更,也有节流版本的自动保存))
/* * * * * * * * *
- Debounce -
* * * * * * * * */
function Debounce (fn, delay) {
let timer;
return function (...args) {
// 如果还有定时器,那么就清除当前定时器
if (timer) clearTimeout(timer)
// 此时的上一个定时器不会执行,重新绑定定时器
timer = setTimeout(() => {
fn.call(this, args)
}, delay)
}
}
const buy = document.querySelector('#buyIt')
function buyFn (msg) {
console.log("I'm buy it now! ", msg)
}
buy.addEventListener('click', Debounce(buyFn, 1000))
Consumer 和 isIterable (后续慢慢研究)
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);
}
// --------------
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');
编程范式
声明式 和 命令式
- 声明封装功能细节,主要进行方法调用,以业务为主
- 命令式则重功能细节,以过程为主
我们写代码要多以声明式的方式去实现,因为声明式的代码天生就具有可扩展性~