跟着月影学 JavaScript(上) | 青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第4天
三原则
- 各司其职
让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操作了DOM数据,违背了原则:各司其职
- 后续维护麻烦,效果不直观
优化方向:
- 可读性
版本二
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
优化之处:
- 通过ClassName修改元素状态
- 通过night表明这是一个夜间效果切换
继续优化方向:
- 使用全CSS来控制样式
<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>
<style>
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
</style>
优点:
- 通过CSS控制样式
结论
- HTML/CSS/JS各司其职
- 应该避免不必要的由JS直接操作样式
- 可以用class来表示状态
- 纯展示类交互寻求零 JS 方案
组件封装
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
案例
轮播图
结构
<!-- 轮播图是一个典型的列表结构,我们可以使用无序列表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>
表现
#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{
/* 使用 CSS 绝对定位将图片重叠在同一个位置 */
position: absolute;
/* 切换效果 */
transition: opacity 1s;
/* 不透明度 即当前图片为透明 */
opacity: 0;
text-align: center;
}
/* 设置其显示 */
.slider-list__item--selected{
transition: opacity 1s;
opacity: 1;
}
行为
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);
控制
<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>
<script>
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
</script>
基本方法
分开三部分设计,尽量互不干扰职责
- 结构
- 展现
- 行为
- API (功能)
- Event (控制流)
优化
改进空间:
控制元素不够灵活,一处改变需要改对应的多个位置
插件化
解耦
- 将控制元素抽取成插件
- 插件和组件之间通过依赖注入的方式建立联系
class Slider{
// ...略去重复代码
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(this));
}
addEventListener(type, handler){
this.container.addEventListener(type, handler)
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
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();
模板化
根据模板生成元素
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>`;
}
// 略去重复代码
}
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});
抽象
将多个组件的通用模型抽象出来
// 父类
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{
// 具体实现及子类方法
}
总结
组件设计原则
- 封装性
- 正确性
- 扩展性
- 复用性
实现组件的步骤
结构设计 -> 展现效果 -> 行为设计
过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
高阶函数
HOF(Higher-order function)
- 函数作为参数传递
- 函数作为返回值返回
- 常用于作为函数装饰器
然后再调用生成的函数
使用场景:当需要对函数进行装饰时,可以使用高阶函数,例如限流器、防抖器等,提高可读性,减少非纯函数
案例
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);
});
编程范式
命令式
更强调怎么做
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'
);