前言
这是我参与 [第五届青训营] 伴学笔记创作活动的第 3 天,上回讲到前端页面是由 HTML、CSS、Javascript 组成的,现在就讲到Javascript
如何写好 Javascript
写好JS的一些原则
-
- 各司其职
- 让
HTML、CSS和Javascript职能分离
-
- 组件封装
- 好的
UI组件具备正确性、扩展性、复用性
-
- 过程抽象
- 应用函数式编程思想
各司其职案例
案例1
写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。如果是你来实现,你会怎么做?
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 = "🌞";
}
});
该代码是通过获取当前按钮,然后去改变body的颜色、字体颜色和图标
以上代码运用到javascript去操作css
如果你来优化,你会怎么做?
案例2
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if (body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
这一版比起上一版好在哪里?
以上代码思路是运用先定义一个标签给定
CSS,然后也是获取按钮,然后改变标签,比案例1,减少了js直接操作CSS代码,将CSS样式写在标签上
还有没有其他的方案?
案例3
<body>
<input id="modeCheckBox" type="checkbox"/>
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
...
</main>
</div>
</body>
#modeCheckBox {
display:none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all ls;
}
以上代码就没有
JS代码,就实现了各司其职的效果了
总结
HTML/CSS/JS各司其责- 应当避免不必要的由
JS直接操作样式。 - 可以用
class来表示状态 - 纯展示类交互寻求零
JS方案
组件封装案例
组件是指Web页面上抽出来一个个包含模版 (HTML)、功能(JS) 和样式(CSS) 的单元。好的组件具备封装性、正确性、扩展性、复用性。
用原生JS 写一个电商网站的轮播图,应该怎么实现?
案例1
- 结构: HTML
轮播图是一个典型的列表结构我们可以使用无序列表
- 元素来实现。
<div id="my-slider" class="slider-list"> <ul> <li class="slider-list__item--selected"><img src="" alt=""></li> <li class="slider-list__item"><img src="" alt=""></li> <li class="slider-list__item"><img src="" alt=""></li> <li class="slider-list__item"><img src="" alt=""></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 ls; opacity: 0; text-align: center; } .slider-list_item--selected { transition: opacity ls; opacity: 1; }行为:JS
API 设计应保证原子操作,职责单一,满足灵活性。- Slider
- getSelectedltem()
- getSelectedltemIndex()
- slideTo()
- slideNext()
- slidePrevious()
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.sliderTo(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-listo control-buttons"></span> </div>const detail = {index: idx} const event = new CustomEvent( 'slide', {bubbles:true, detail}) this.container.dispatchEvent(event)完整js
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; const controller = this.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){ this.slideTo(idx); this.stop(); } }); controller.addEventListener('mouseout', evt=>{ this.start(); }); 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'; }) } const previous = this.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { this.stop(); this.slidePrevious(); this.start(); evt.preventDefault(); }); } const next = this.container.querySelector('.slide-list__next'); if(next){ next.addEventListener('click', evt => { this.stop(); this.slideNext(); this.start(); evt.preventDefault(); }); } } getSelectedItem(){ let selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ let selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } let item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } slideNext(){ let currentIdx = this.getSelectedItemIndex(); let nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ let currentIdx = this.getSelectedItemIndex(); let previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } const slider = new Slider('my-slider'); slider.start();组件封装
总结:
基本方法- 结构设计
- 展现效果
- 行为设计
API (功能)
Event (控制流)
思考:
改进空间? 如果让你来重构这个轮播图组件,你会怎么做?让数据可变,让用户去填数据,我们代码进行行为控制和状态控制
重构:插件化
解耦
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
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){ plugins.forEach(plugin => plugin(this)); } 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'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } 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); } 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();此举就是将click事件进行封装,可让用户填入相应的参数就可实现功能,插件化
里面的参数分别对应三种状态- 默认状态
- 之前状态
- 下一个状态
缺点: 当插件要进行删除时,那么也需要把模块也要去手动删除
重构: 模板化
解耦
将HTML模板化,更易于扩展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>`; } ... }除了将
HTML模块化,还能怎么样进行优化呢?组件框架
抽象
将组件通用模型抽象出来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模板已经可以抽象复用,状态也通过参数传递达到想要的状态,功能插件化,可进行删除和添加,大模块进行分成若干个模块,然后用函数进行封装;缺点:
js和html已经处理,但css并没有进行处理过程抽象案例
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
Vue
MVVC运用了过程抽象思想
Reacthooks过程抽象 (状态管理)案例1
操作次数限制
- 一些异步交互
- 一次性的HTTP请求
const list = document.querySelector('ul'); const buttons = list.querySelectorAll('button'); buttons.forEach((button ) => { button.addEventListener('click',(evt) => { const target = evt.target; target.parentNode.className = 'completed' ; setTimeout(() => { list.removeChild(target.parentNode); },2000); }); });如果当用户无意点击多次按钮,就会出现这个报错,除此请求数据,这个数据如果请求多次,那我们会返回第一次结果,像这种多次调用,只执行一次这种过程;我们可以运用once去过程抽象
Once
- 为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象
function once(fn) { return function (...args) { if (fn) { const ret = fn.apply(this,args) fn = null return ret } } } button.addEventListener('click',once((evt) => { const target = evt.target; target.parentNode.className = 'completed' ; setTimeout(() => { list.removeChild(target.parentNode); },2000) }));高阶函数
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
function HOFO(fn) { return function (...args) { return fn.apply(this,args) } }常用高阶函数
- Once
- Throttle
- Debounce
- Consumer/2
- lterative
思考和讨论
- 为什么要使用高阶函数?
函数的范式有种函数叫纯函数,纯函数是无状态的,传入a就得到b
编程范式 命令式与声明式
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);例子
- Toggle- 命令式
- Toggle- 声明式
- Toggle- 三态 命令式
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' );三态
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 = 'warn', evt => evt.target.className = 'off', evt => evt.target.className = 'on' );声明式更具有多态,可让函数封装更灵活
总结
-
过程抽象/HOF/装饰器
-
命令式/声明式
写的
js代码应该各司其职、组件封装、过程抽象.
其中应该采用函数式编程思想,状态管理,从插件化到组件化到模块化到工程化