写好js的一些原则
- 各司其职
- HTML CSS JS职能分离
- 深夜食堂
- 组件封装
- 好的UI组件具备正确性,扩展性,复用性。
- 轮播图
- 过程抽象
- 应用函数式编程思想,拥有更好的扩展性,更好的通用能力
各司其职
结构、样式、行为分离。
示例 深夜食堂
例子:写一段js,控制一个网页,使他支持浅色和深色两种浏览模式。
在浅色模式(白色背景,黑色文字)下,点击'🌞',页面变成深色模式(黑色背景,白色文字),图标变为'🌜'。反之。
结构 & 样式
结构,采用语义化标签,头部,主部分:header,main。
头部包含标题和按钮;主要部分包含图片和一段描述。
<header>
<button id="modeBtn">🌞</button>
<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>
不使用浮动进行排版,使用flex布局,让float回归最原始的作用:图文混排。
header{
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
#modeBtn{
background-color:transparent;
border:none;
font-size:2em;
}
.pic img{
width:100%;
}
版本一
给图标按钮注册点击事件,根据点击的目标元素(e.target)得到其内部的图标,根据图标切换。
const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e)=>{
const body = document.body;
if(e.target.innerHTML == '🌞'){
//目前正处于浅色模式,需要切换成深色模式,改变图标
body.style.color = '#fff';
body.style.backgroundColor = '#000';
e.target.innerHTML = '🌜';
}else{
//目前正处于深色模式,需要切换成浅色模式,改变图标
body.style.color = '#000';
body.style.backgroundColor = '#fff';
e.target.innerHTML = '🌞';
}
})
- js直接操作body的style属性,修改了元素的css样式,使js去做了css的事情
- 开发人员只看代码无法理解原始需求含义,可读性差
版本二
在css中设置深色模式的规则-》class选择器,使用js为body添加/删除相应的class,从而实现切换,避免使用js直接操作css。
结构和样式改动:使用伪元素来制作图标,这样可以使用body类名的不同来对其内容进行切换,不再需要js操作html。
body{
transition: all 1s;
}
body.dark{
color:#fff;
background-color: #000;
transition: all 1s;
}
#modeBtn::after{
content:'🌞'
}
.dark #modeBtn::after{
content:'🌜'
}
const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e)=>{
const body = document.body;
if(body.className != 'dark'){
//目前正处于浅色模式,需要切换成深色模式,添加dark
body.className = 'dark'
}else{
//目前正处于深色模式,需要切换成浅色模式,删除dark
body.className = ''
}
})
- 浅色和深色的切换,只是去改变html的展现效果,没有其他的行为逻辑,本质上这个代码是控制样式的
- 纯css实现
版本三
纯展示类交互,零JS方案
使用多选框的选中/不选中两个状态来表示深色/浅色两种模式
使用label标签将多选框与图标关联。
使用伪类选择器匹配元素状态(:checked)。
使用兄弟选择器,选中.content。(mode:checked + .content)
总结
- HTML/CSS/JS各司其职
- 应当避免不必要的由JS直接操作样式和结构
- 可以用class来表示状态
- 纯展示类交互寻求零JS方案
组件封装
组件是指web页面上抽出来一个一个包含模板(html),功能(js)和样式(css)的单元。
好的组件具备封装性,正确性,扩展性,复用性。
版本一轮播图
原生JS写轮播图
结构
使用无序列表表示。
使用特殊的类名表示当前显示的元素
<div class="slider-list" id="my-slider">
<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>
样式
- 需要哪个元素显示,就为他加上slider-list__item--selected类名,设置opacity:1;
- 在ul上取消项目符号,删除列表自带的内边距和外边距
- 使用绝对定位让所有li元素重叠在一起,在一个位置出现。
- 使用opacity属性控制li元素的显示。
- 切换的过渡动画使用transition;
#my-slider{
width:790px;
position: relative;
}
#my-slider ul{
list-style: none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item--selected
,.slider-list__item{
position: absolute;
transition: opacity 1s;
opacity: 0;
}
.slider-list__item--selected{
transition: opacity 1s;
opacity: 1;
}
行为:API
控制轮播图功能的类:Slider,在类内部定义函数:职责单一,满足灵活性。
- getSelectedItem()
- getSelectedItemIndex()
- slideTo(idx)
- slideNext()
- slidePrevious()
class Slider{
constructor(id) {
//根据id获得外部容器
this.container = document.getElementById(id);
//获得所有的li元素;
this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected');
}
//得到当前选中的li元素
getSelectedItem(){
const selected = this.container.querySelector('.slider-list__item--selected')
return selected;
}
//得到当前选中的li元素的索引
getSelectedItemIndex(){
//this.items 不是数组,把他转化为数组,才可以用数组的方法
return Array.from(this.items).indexOf(this.getSelectedItem());
}
//切换到哪一张,参数为索引
slideTo(idx){
//切换class
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();
//减一防止为负数,故先加length
const previousIdx = (currentIdx - 1 + this.items.length) % this.items.length;
this.slideTo(previousIdx);
}
}
行为:控制流
在Slider类的构造函数内操作。
- 图片自动切换:定时器;移入/移出事件
- 箭头:上一张/下一张;点击事件
- 原点:切换到某一张;移入/移出事件;
- 原点与图片元素存在状态耦合,采用自定义事件 slide 使图片元素和原点状态对应。(slideTo的时候触发,改变原点状态)
箭头和原点都需要绝对定位,相对于ul元素。
因为箭头需要垂直居中,需要用到外部容器的高度,但是因为他们内容都是绝对定位,脱离了常规流,所以外部容器高度都为0;加高度;
#my-slider{
width:790px;
height:340px;
position: relative;
}
#my-slider ul{
list-style: none;
position: relative;
padding: 0;
margin: 0;
width:100%;
height: 100%;
}
箭头使用伪元素生成。
<a class="slider-list__next"></a>
<a class="slider-list__previous"></a>
<div class="slider-list__control">
<span class="slider-list__control-buttons--selected"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
</div>
箭头的样式
.slider-list__next
,.slider-list__previous{
/*定位*/
position: absolute;
top:50%;
margin-top:-25px;
/*设置宽高、背景、字体大小*/
width:30px;
height:50px;
color:#fff;
font-size:24px;
/*设置垂直居中和水平居中*/
/* 因为伪元素默认是inline,即行级元素*/
/*所以生成的是行级排版上下文,text-align,并不控制块元素自己的对齐,只控制它的行内内容的对齐*/
line-height:50px;
text-align: center;
opacity: 0;
transition: opacity 0.5s;
cursor: pointer;
background: rgba(0,0,0,0.3);
}
#my-slider:hover .slider-list__next,
#my-slider:hover .slider-list__previous{
opacity: 1;
}
.slider-list__previous{
left:0;
}
.slider-list__next{
right: 0;
}
.slider-list__next::after{
content:'>'
}
.slider-list__previous::after{
content:'<'
}
圆点样式
外部容器:
- display:table
- margin:auto
- position:relative
- bottom:40px;
.slider-list__control{
/*在ul下面生成,现在ul有高度了,所以他在显示在图片下面*/
position:relative;
bottom: 40px;
display: table;
background-color: rgba(255,255,255,0.5);
border-radius: 15px;
/*水平居中*/
margin: auto;
padding:5px;
}
.slider-list__control-buttons--selected
,.slider-list__control-buttons{
display: inline-block;
width:15px;
height:15px;
border-radius: 50%;
background-color: #fff;
margin:0 5px;
cursor: pointer;
}
.slider-list__control-buttons--selected{
background-color: #f40;
}
增加的API
- start
- stop
- 构造函数内部
- 左右箭头的点击事件
- 原点控制器
- 鼠标滑进事件
- 鼠标滑出事件
- 容器触发的自定义事件,为原点设置状态
- slideTo函数,增加自定义事件
- CustomEvent(type,Map配置)
- EventTarget.dispatchEvent(event)
```js
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
```
基本方法
- 结构设计
- 展现效果
- 行为设计
- API(功能)
- Event(控制流)
- 使用自定义事件进行数据解耦
版本2插件化
- 版本一中Slider构造函数内部太多代码
- 将next,previous,control等控制元素抽离出来,做成插件。
- 组件与插件之间通过依赖注入方式建立联系
function pluginNext(slide){
const next = slide.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click',(ev)=>{
//先停止动画
slide.stop();
slide.slideNext();
slide.start();
ev.preventDefault();
})
}
}
注册插件
pluginRegister(...plugins){
plugins.forEach((plugin)=>{
plugin(this);
})
}
版本3模板化
- render(images) 渲染html结构
- action(slide) 定义控制流插件功能
render(){
const images = this.opts.images;
const htmlStr = images.map((iamge)=>{
return `<li class="slider-list__item">
<img src="${iamge}"/>
</li>`
}).join('')
return `<ul>${htmlStr}</ul>`
}
插件注册,这里每一个plugin是一个对象,包含两个函数:render()和action()
pluginRegister(...plugins){
plugins.forEach((plugin)=>{
const container = document.createElement('div');
container.className = 'slide-list__plugin';
container.innerHTML = plugin.render(this.opts.images);
this.container.appendChild(container);
plugin.action(this);
})
}
const pluginNext = {
render(){
return `<a class="slide-list__next"></a>`;
},
action(slide){
const next = slide.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click',(ev)=>{
//先停止动画
slide.stop();
slide.slideNext();
slide.start();
ev.preventDefault();
})
}
}
}
版本4组件化
过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
操作次数限制 once函数
点击任务列表
- 点击后,button背景颜色变化,任务列表透明度变化(transition)
- 最后删除该任务
- 点击事件
const list = document.querySelector('ul'); const buttons = list.querySelectorAll('button'); buttons.forEach((button)=>{ button.addEventListener('click',(ev)=>{ const el = ev.target; //el-><i> iconfont元素 li->button->i el.parentNode.parentNode.className = "completed"; setTimeout(()=>{ list.removeChild(el.parentNode.parentNode); },1000) }) })
因为有setTimeout的存在,当点击后,该任务并不会立即消失,且opacity设置了过度,当连续点击时,会出发多次click事件,因此会发生错误
为了能够让“只执行一次”的需求覆盖不同的事件处理,需要将这个需求剥离出来,在各个过程叫过程抽象
function once(fn){
return function (...args){
if(fn){
const ret = fn.apply(this,...args);
fn = null;
return ret;
}
}
}
EventTarget.addEventListener中的第三个参数可以配置once;
高阶函数
- 以函数作为参数
- 以函数作为返回值
- 常用于函数装饰器
- once
- Throttle (节流)
- Debounce (防抖)
- 注意事项,防抖函数timer的赋值。
- 小鸟自动画-》扇动翅膀,使用toggle函数切换类名,不同的类对应着背景图片不同的位置-》精灵图
- 小鸟随鼠标移动;鼠标移动事件,需要防抖;
- 位置的确定
- 事件对象中的ev.clientX-》表示鼠标离可视化区域左边界,
- 但如果让bird.style.left = ev.clientX,bird.style.top = ev.clientY
- 对应的是左上顶点
- 需要小鸟的中心对应着鼠标的位置;
//节流函数;两次函数调用之间必须间隔time时间。在time时间内,触发,不管用
function Throttle(fn,time = 500){
const timer = null;
return function(...args){
if(timer == null){
fn.apply(this,args);
timer = setTimeout(()=>{
timer = null;
},time)
}
}
}
//防抖函数
//常用于鼠标滑动mousemove;滚动条滚动scroll事件;
//在time这一段时间中,只要有调用,就将计时器清掉,重新计时;
//只有当time时间内没有调用时,在time之后才可调用
//setTimeout的返回值代表了计时器的标志,一定要赋值到timer中,
//否则,前面清除不管用,清除的不是下面的计时器,会出现触发了好几个计时器的情况下,防抖无效。
function Debounce(fn,time){
time = time || 100
const timer = null;
return function(...args){
clearInterval(timer);
timer = setTimeout(()=>{
fn.apply(this,args);
})
}
}
//消费者 把一个同步的变成异步的,隔一段时间调用。
//延迟调用的一个效果。
function Consumer(fn,time){
const tasks = [],timer = null;
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)
}
}
}
//jquery中存在批量操作,即调用一个api同时操作一组元素
//处理迭代对象;
//如果subject是可迭代的,则让fn去处理subject每一个元素obj;
//如果是不可迭代的,就让fn只处理subject
const isInterable = obj => obj != null && typeof obj[Symbol.interator] == 'function';
function Iterative(fn){
return function(subject,...rest){
if(isInterable(obj)){
const ret = [];
for(const obj of subject){
ret.push(fn.apply(this,[obj,...rest]));
}
return ret;
}
return fn.apply(this,[subject,...rest])
}
}
纯函数
- 没有副作用的
- 结果是可预期的
- 输入确定,输出就确定了
- 容易确定他的正确性
编程范式
- 命令式-》过程,怎么做
- 面向过程
- 面向对象
- 声明式 做什么 过程抽象
- 逻辑式
- 函数式
toggle函数
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'
);