这是我参与「第四届青训营 」笔记创作活动的的第3天
👋本笔记的基本内容:写好JS的三大原则
- 各司其责(让HTML、CSS和JavaScript职能分离)
- 组件封装(好的UI组件具备正确性、扩展性、复用性)
- 过程抽象(应用函数式编程思想)
各司其责
1.0 新手版
这段代码能够满足我们所需要的的要求,对于前段爱好者来说还是不错滴,但是如果是在多人维护的项目里呢?就有问题了。
问题: 直接修改了html和css(直接去操作了body.style),JavaScript做了css该干的的事情,违反了各司其责的原则。 单看这段代码,也许我们能猜到是完成网页深色与浅色模式的切换这样一个功能的,可是我们如果是要完成其他功能呢?每次都要去猜吗?这样敲代码的难度就大大提升了。
2.0 优化版
一般我们会通过class来定义html的状态,这个版本只是操作了html元素的状态(只是修改了classname),通过类名我们知道他们所要完成的功能,这样就解决了新手版所出现的问题了
1.0 vs 2.0 版本总结
html负责结构,css负责表现,JavaScript负责行为,结构、表现、行为分离是前端工程师所要掌握的基本原则。
如果没有遵守“各司其责"的原则,则代码会看起来很吃力,不知道代码所代表的含义,加大了代码后来的修改和维护的难度
✋那还有没有更好的版本呢?
有,因为完成网页深色与浅色模式的切换这样一个功能本质是改变html的展示效果,除了控制样式没有其他的行为逻辑在里面,一般来说,控制样式的代码可以用纯css来实现的,而根据各司其责的原则,用css来控制样式才是最合适的
3.0 无JS版
0. 切换:通过CheckBox进行切换,而用label代替button表示的🌞和🌜,通过for来绑定CheckBox实现切换功能。
1. 切换后触发伪类checked
深夜食堂--总结
组件封装
👋组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。
好的组件具备封装性、正确性、扩展性、复用性。
现在我们就来用轮播图来熟悉组件封装吧!
😁先从html、css、JavaScript来了解一下轮播图
1.0 无交互版本
1. 结构:HTML
轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现。
- 表现:CSS
- 使用 CSS 绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用 CSS transition
- 行为:JS
轮播图使用的API
-
Slider
- getSelectedItem( ) 得到当前选中图片元素
- getSelectedItemIndex( ) 得到当前选中图片元素在列表的下标
- slideTo( ) 跳转到特定的元素上
- slideNext( ) 轮播下一张图片
- slidePrevious( ) 轮播上一张图片
2.0 控制流交互版本
2.0 版本加上了两边控制前后翻图的箭头,下面控制选图的小圆点
行为:JS 【控制流(使用自定义事件来解耦)】
解释: 使用自定义事件是为了实现状态绑定(下面4个点与上面的图片是一一对应的) 解耦:各自独立,可灵活地替换
1.0 vs 2.0 基本方法总结
-
结构设计
-
展现效果
-
行为设计
- API (功能)
- Event (控制流)
🤔还有改进空间呢?如果我们要重构组件的话要怎么去做咧?
原来的组件功能实现是没问题的,但组件的代码不是很灵活,比如:要删掉一两张图片,则要改动html、css、JavaScript的代码,改动不灵活
3.1 重构:插件化
解耦
- 将控制元素抽取成插件(将两边控制前后翻图的箭头,下面控制选图的小圆点插件化 )
- 插件与组件之间通过依赖注入方式建立联系
//将小圆点的控制抽离成一个插件pluginController
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';
});
}
}
//将左翻页的控制抽离成插件pluginPrevious
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();
});
}
}
//将右翻页的控制抽离成插件pluginNext
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();
});
}
}
//通过注册插件registerPlugins来使用各种插件
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){
// 这里的this就是组件的实例对象
plugins.forEach(plugin => plugin(this));
}
}
好处:slider的代码变简单了(删除功能和新增功能都很方便)
- 如果要删掉底下的四个小圆点,就直接删除插件,取消注册,不会影响到其他的地方,代码修改量少。
- 如果要新增功能就再新建一个插件,注册轮播图插件。
🤔还有进一步优化的空间吗?
有,之前我们的结构是写死在html的代码里面的,但在JavaScript的UI组件里面首先要做到数据驱动,根据数据生成html的模板,不需要将结构写死在html。
3.2重构:html的模板化
解耦
- 将HTML模板化,(让JavaScript来渲染组件的HTML)更易于扩展
- render( )**渲染函数,用来渲染HTML
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>`;
}
}
- action( ) 用来完成它的初始化
const pluginController = {
render(images) {
return `
<div class="slide-list__control">
${images.map((image, i) => `
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
`).join('')}
</div>
`.trim();
},
action(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';
});
}
}
};
🤔而模板化是优化的终点吗?还能再优化吗?
3.3 重构:组件框架--抽象
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 ''
}
}
现在就形成了一个简单的组件框架了,支持定义一个组件,然后里面注册若干个插件
总结
-
组件设计的原则:封装性、正确性、扩展性、复用性
-
实现组件的步骤:结构设计、展现效果、行为设计
-
三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
🤔组件设计会破坏“各司其责”的原则吗?
并没有破坏,因为“各司其责”的原则是html、css、JavaScript各自做他们应当做的事情,并不是看他们写在哪里。JavaScript生成的html还是负责结构,生成的css还是负责控制样式,没有违背各司其责的原理😁
当然,组件框架--抽象也不是优化的终点,如:父子组件的嵌套等,还是拥有更多的改进空间的
过程抽象
👋 过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
前面的组件化是用来控制整体的UI组件的,过程抽象则通常是处理局部细节作用的方法。
🤔过程抽象要怎么解释呢?来看看下面这个例子
上图是一个人在开门,我们可以把这个门,这个人抽象出来形成一个结构,门和人都是数据,而open是一个过程,也是可以抽象出来的,因为我们开门,开窗,开冰箱都可以复用open这个动作。这个过程就是过程抽象
🤔有时候我们写代码的时候会遇到有关于操作次数限制的bug,涉及一些异步交互和一次性的HTTP请求,例如:我们想让绑定的事件只在第一次点击的时候执行,之后点击都不执行。有什么解决方法呢?
常见的方法有:
- 设置
addEventListener的once参数 ,表示listener在添加之后最多只调用一次,如果是true,listener会在其被调用后自动移除 - 在回调函数中添加
removeEventListener,在第一次执行后解除绑定事件 - 一次性执行函数Once(这就是我们要讲的过程抽象的一种情况)
高阶函数Once
为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
我们说,如果一个函数return另一个函数,那么这个函数就叫做高阶函数。
once函数接收的参数是一个函数fn,返回了一个新的函数,在返回的函数中,做了一件事,就是让fn只执行一次,第二次执行的时候给fn已经被赋值为null,if判断为false,就无法被再次执行里面的方法了。
高阶函数 (HOF)
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
原理图:
function HOF0(fn) {
return function(...args) {
return fn.apply(this, args);
}
}
HOF0就是一个默认的等价高阶函数,一般的高阶函数都是在HOF0的基础上去做了某些事情,比如改变某些参数/返回值等
常用的一些高阶函数(HOF)
- Once ( 一次性执行函数 )
- Throttle ( 节流函数 )
- Debounce ( 防抖函数 )
- Consumer / 2 ( 将同步操作变成一个异步的操作 )
- Iterative ( 可迭代方法 )
编程范式
编程语言其实是有主要的编程范式,大体上可以分为命令式编程语言和声明式编程语言,像JavaScript这样的语言既有命令式编程范式的特点,又有声明式编程范式的特点,也就意味着它可以使用两种风格来写命令式的代码和声明式的代码
🤔什么是命令式的代码,什么是声明式的代码呢?
我们有这样的一个需求,将[1, 2, 3, 4 ]这个列表里的数字乘2
命令式的方式来写代码:(命令式的话更强调怎么做)
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);
声明式的编程思想天然地比命令式有着更强的可扩展性
总结
- 过程抽象 / HOF / 装饰器
过程抽象要了解高阶函数和函数装饰器
- 命令式 / 声明式
编程语言是有命令式 / 声明式 两种风格的
JavaScript这样的语言既有命令式编程范式的特点,又有声明式编程范式的特点