学习JS(1)
从原则,风格上讨论。
JavaScript -> Behavioral
CSS -> Presentational
HTML -> Structural
各司其责、组件封装、过程抽象
1. 白天黑夜模式转换
写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。
版本1
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 = '🌞';
}
});
版本2
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night'){
body.className = 'night';
} else {
body.className = '';
}
});
版本3
<body>
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
...
</main>
</div>
</body>
html的body中content前用隐藏的CheckBox,content下header中label可以for一个input元素,点击label时浏览器直接操作到CheckBox;状态伪类+兄弟选择器:checked状态+.content来控制选中的样式。
#modeCheckBox {
display: none;
}
#modeCheckBox: checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
太阳月亮是按钮
#modeBtn::after {
content:'🌞';
}
评价
版本1:正确性没有问题,但用js做了css应该做的事情,功能扩展改动逻辑不清楚。(重构为状态切换)
版本2:显而易见两种状态切换。
版本3:纯视觉展示类的功能可以不用js(js负责行为)实现,可以用纯html/css实现
版本3适应性没有2好,但是没有bug
总结
HTML/CSS/JS各司其责
避免不必要的由 JS 直接操作样式;
可以用class表示状态;
纯展示类交互寻求零JS方案。
2. 组件封装 - 原生JS写轮播图
用原生JS 写一个电商网站的轮播图,应该怎么实现?
组件:
组件是指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
使用CSS绝对定位将图片重叠在同一个位置;
轮播图切换的状态使用修饰符(modifier);
轮播图的切换动画使用CSS transition
#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;
}
行为设计:API
行为:JS API设计应保证原子操作,职责单一,满足灵活性。
设计Slider(当前选中的哪一个、当前是第几张、轮播到、下一张、前一张)
一个构造器 + 五个api实现操作
使用时new一个Slider
const slider = new slider('my-slider');
slider.slideTo(1);//显示第一张图
接下来加控制的点
行为:控制流
使用自定义事件来解耦。
添加左右两个a标签,下面四个点切换
四个点的操作与轮播是解耦的,便于修改需求。
四个小点注册mouseover方法,移走鼠标调用start方法。
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext.(),this.cycle);
}
stop(){
clearInterval(this._timer) ;
}
写start与stop两个方法,start用setInterval到下一张图,stop清掉定时器。
自动轮播与小圆点保持同步:监听slide自定义事件,idx更新。
更新:在slideTo中派发slide事件,浏览器中支持CustomEvent方法,将‘slide’当作参数派发,然后用container容器去dispatch这个event,上面就可以监听到。
总结:基本方法
结构设计;展现效果;行为设计【API(功能);Event(控制流)】
改进:更好的扩展性、复用性
重构:插件化
解耦:
将控制元素抽取成插件 插件与组件之间通过依赖注入方式建立联系
如两边框,四个点。
组件一般不超过十四行代码,最多二三十行,再多就需要重构拆分。
上述重构插件化:
constructor只有三行代码:
增加一个registerPlugins方法,注册插件,插件要依赖于主件Slider,但是不希望Slider知道插件的存在,依赖注入,将this实例当作参数传入插件。
这样将插件的逻辑从constructor中独立出来。
三个独立插件方法,注册三个插件再start。
以后不想使用插件可以注释掉,就不能使用了,相关html需要删掉,不影响其他功能。
重构:模板化
将HTML模板化,更易于扩展。
在主件和插件中都使用render方法实现。
HTML:只剩下单一元素
<div id="my-slider" class="slider-list"> </div>
内容:用JS模板化渲染,数据抽象,images封装为数组,根据数据渲染HTML。
插件修改:插件也有插件的容器,然后action初始化操作。
最终new一个Slider时把image传入,注册三个插件,start。
好处:现在如果不想要下面的圆点,只需要将组件注释掉,不需要修改其他地方即可。
(css也可以模板化)
重构:组件框架
再进一步
抽象:将通用的组件模型抽象出来
Slider继承Component,实现独有方法
抽象出一个Component类:(很小的组件框架)
主件、插件分开,可以有层级的,主件一级一级组合、父组件子组件等。
总结
组件设计的原则:封装性、正确性、扩展性、复用性。
实现组件的步骤︰结构设计、展现效果、行为设计。
三次重构:插件化;模板化;抽象化(组件框架)。
改进:CSS模板化;主件框架多层级,父组件子组件间状态同步,消息通信等
3. 过程抽象
用来处理局部细节控制的一些方法; 函数式编程思想的基础应用。
一个方法可以看作一个输入输出的黑盒,抽象。
操作次数限制
一些异步交互 一次性的HTTP请求
这种情况多次点击会报错,对只执行一次的过程抽象。
Once
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
function once(fn){
return function (...args) {
if(fn) {
const ret = fn.apply(this,args);
fn = null;
return ret;
}
};
}
const list = document.querySelector('ul');
const buttons = list.queryselectorAll('button') ;
buttons.forEach((button) => {
button.addEventListener('click', once((evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
},2000);
}));
});
只执行一次例子:
const foo = once(() => {
console.log('bar');
});
foo();foo();foo();
调用三次,但每次只执行一次。
高阶函数
以函数作为参数;
以函数作为返回值;
常用于作为函数装饰器。
等价范式:
function HOF0( fn) {
return function( ...args ) {
return fn.apply( this, args ) ;}
}
常用高阶函数
Once;Throttle;Debounce;Consumer/2;Iterative.
Throttle(节流函数)
没有必要将大量数据传到后台,每隔一段时间传一次。
对原始函数包装,在五百毫秒后才调用。
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);
});
不管点击多快,五百毫秒记录一次。
Debounce(防抖函数)
停顿才调用方法。
每次动后clear掉timeout,停下来超时才调用。
var i = 0;
setInterval(function(){
bird.className = "sprite" + 'bird' + ((i++) % 3);
}, 1000/10);
function debounce(fn, dur){
dur = dur || 100;
var timer;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, dur);
}
}
document.addEventListener('mousemove', debounce(function(evt){
var x = evt.clientX,
y = evt.clientY,
x0 = bird.offsetLeft,
y0 = bird.offsetTop;
console.log(x, y);
var a1 = new Animator(1000, function(ep){
bird.style.top = y0 + ep * (y - y0) + 'px';
bird.style.left = x0 + ep * (x - x0) + 'px';
}, p => p * p);
a1.animate();
}, 100));
Consumer
执行很多次很快的操作;
例子1:
每隔一秒计算并在控制台输出
例子2:
执行很多次很快的操作;将它们存在列表中,遍历列表,每隔一段时间执行一次,返回一个结果。
Iterative(批量操作元素)
批量操作元素,可迭代的操作
原本只操作一个参数,传入一个列表,可以批量操作。
iterative第一个参数,判断是否可迭代,如果可以,取出每一个参数,依次调用,把结果push到结果。
为什么要使用高阶函数?
高阶函数本身是纯函数。
纯函数(输入输出)比较好做单元测试,直接调用就可以,不需要构建运行时环境。
function add(x,y){
return x + y;
}
不管什么时候调用add(1,2)结果为3,不改变,只要传参数确定,返回值就确定。
例子:(不是纯函数)
function setColors(els, color) {//impure function
for(const el of els){
el.style.color = color;
}
}
const els = document.querySelectorAll( 'li:nth-child(2n+1)');
const els2 = document.querySelectorAll( 'li:nth-child(3n+1)');
setColors(els2,'blue' ) ;
setColors (els,'red' ) ;
结果:
调用次序不一样,结果不同。
改写
可以通过高阶函数减少非纯函数的数量,使系统的稳定性、可靠性增加。
改写:系统中只剩setColor一个非纯函数
测试:iterative
可以用add方法进行测试
const addList = iterative(add);
console.log(addList([1,2,3,4],1));
[1,2,3,4]每个数+1,得到[2,3,4,5],正确。
4. 编程范式
包括Javascript在内的很多现代脚本语言:混合的编程范式。(更加灵活)
命令式与声明式
命令式(过程式、面向对象)与声明式(逻辑式、函数式)
命令式与声明式没有优劣之分,都有适合的使用场景;
命令式:强调过程(怎么做);
声明式:强调是什么。
命令式(关心循环执行过程)
let list = [1,2,3,4];
let map1 = [];
for(let i = 0;i < list.length;i++){
map1.push(list[i] * 2);
}
声明式
let list = [1,2,3,4];
const double = x => x * 2;
list.map(double);
命令式关心循环执行过程,而声明式不关心过程。
例子
命令式
点击按钮切换
声明式
很多状态
如果有很多个状态,命令式需要扩展if/else分支,用声明式写法toggle不用改动,只添加状态即可。
点击按钮切换
总结
过程抽象/HOF(高阶函数)/装饰器;
命令式/声明式
函数式编程
总结
提示:这里对文章进行总结: 例如:以上就是今天的笔记内容,本文仅仅简单从原则、风格上讨论JS的写法,更多有关前端的知识参考后续文章。
|